Skip to content

Commit 7f9e738

Browse files
feat: add unit tests for TweetQuoteCard component
1 parent b007172 commit 7f9e738

1 file changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { mount } from '@vue/test-utils';
3+
import TweetQuoteCard from '@/components/tweet/TweetQuoteCard.vue';
4+
import Avatar from '@/components/ui/Avatar.vue';
5+
import TweetMedia from '@/components/tweet/TweetMedia.vue';
6+
import type { Tweet } from '~~/shared/types/tweets';
7+
8+
// Mock useRouter to capture push calls
9+
let pushMock: ReturnType<typeof vi.fn> | undefined;
10+
vi.mock('vue-router', () => ({
11+
useRouter: () => ({
12+
push: (...args: unknown[]) => pushMock && pushMock(...args),
13+
}),
14+
}));
15+
16+
// Mock i18n
17+
const i18nMock = {
18+
locale: 'en',
19+
t: (key: string) => key,
20+
};
21+
22+
// Common stubs
23+
const stubs = {
24+
NuxtLink: {
25+
template: '<a :href="to"><slot /></a>',
26+
props: ['to'],
27+
},
28+
NuxtImg: { template: '<img />' },
29+
Icon: { template: '<i />' },
30+
VideoPlayer: { template: '<div class="video-player-stub"></div>' },
31+
Avatar: Avatar,
32+
TweetMedia: TweetMedia,
33+
};
34+
35+
const globalConfig = {
36+
stubs,
37+
mocks: {
38+
$i18n: i18nMock,
39+
},
40+
};
41+
42+
function makeTweet(overrides: Partial<Tweet> = {}): Tweet {
43+
const content = 'Quoted @user and #Tag in text';
44+
const tweet: Tweet = {
45+
id: 'qt-1',
46+
content,
47+
createdAt: new Date(Date.now() - 3 * 3600 * 1000).toISOString(), // 3h ago
48+
author: {
49+
username: 'quoteduser',
50+
displayName: 'Quoted User',
51+
avatarUrl: '/q-avatar.jpg',
52+
isFollowing: false,
53+
isFollower: false,
54+
},
55+
replyCount: 0,
56+
retweetCount: 0,
57+
likeCount: 0,
58+
isLiked: false,
59+
isRetweeted: false,
60+
entities: {
61+
mentions: [{ username: 'user', startPosition: content.indexOf('@user') }],
62+
hashtags: [{ hashtag: 'Tag', startPosition: content.indexOf('#Tag') }],
63+
},
64+
media: [{ type: 'IMAGE', url: '/image-1.jpg', altText: 'image', width: 600, height: 400 }],
65+
};
66+
return { ...tweet, ...overrides };
67+
}
68+
69+
describe('TweetQuoteCard.vue', () => {
70+
it('renders compact quote wrapper with avatar, names and time', () => {
71+
const tweet = makeTweet();
72+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
73+
74+
// Wrapper id
75+
const card = wrapper.find(`#quoted-tweet-${tweet.id}`);
76+
expect(card.exists()).toBe(true);
77+
78+
// Avatar
79+
const avatar = wrapper.findComponent(Avatar);
80+
expect(avatar.exists()).toBe(true);
81+
82+
// Names
83+
expect(wrapper.text()).toContain('Quoted User');
84+
expect(wrapper.text()).toContain('@quoteduser');
85+
86+
// Time element exists with required attributes
87+
const timeEl = wrapper.find('time');
88+
expect(timeEl.exists()).toBe(true);
89+
expect(timeEl.attributes('datetime')).toBe(tweet.createdAt);
90+
const title = timeEl.attributes('title');
91+
expect(title && title.length > 0).toBe(true);
92+
});
93+
94+
it('links author to profile and content segments to correct hrefs', () => {
95+
const tweet = makeTweet();
96+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
97+
98+
// Author profile link
99+
const profileLink = wrapper.find('a[href="/profile/quoteduser"]');
100+
expect(profileLink.exists()).toBe(true);
101+
102+
// Content mention and hashtag
103+
const mention = wrapper.find('a[href="/profile/user"]');
104+
expect(mention.exists()).toBe(true);
105+
expect(mention.text()).toContain('@user');
106+
107+
const hashtag = wrapper.find('a[href="/hashtag/Tag"]');
108+
expect(hashtag.exists()).toBe(true);
109+
expect(hashtag.text()).toContain('#Tag');
110+
});
111+
112+
it('renders media via TweetMedia in compact mode when media exists', () => {
113+
const tweet = makeTweet({
114+
media: [{ type: 'VIDEO', url: '/video.mp4', altText: 'v', width: 640, height: 360 }],
115+
});
116+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
117+
118+
const media = wrapper.findComponent(TweetMedia);
119+
expect(media.exists()).toBe(true);
120+
expect(media.props('media')).toEqual(tweet.media);
121+
// compact prop should be passed and truthy
122+
expect(media.props('compact')).toBe(true);
123+
});
124+
125+
it('does not render TweetMedia when media is empty', () => {
126+
const tweet = makeTweet({ media: [] });
127+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
128+
const media = wrapper.findComponent(TweetMedia);
129+
expect(media.exists()).toBe(false);
130+
});
131+
132+
it('renders plain text when there are no entities', () => {
133+
const tweet = makeTweet({
134+
content: 'Just quoted text',
135+
entities: { mentions: [], hashtags: [] },
136+
});
137+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
138+
const contentP = wrapper.find('p');
139+
expect(contentP.exists()).toBe(true);
140+
expect(contentP.text()).toContain('Just quoted text');
141+
expect(contentP.findAll('a').length).toBe(0);
142+
});
143+
144+
it('renders plain text when entities is undefined (early return path)', () => {
145+
const tweet = makeTweet({ content: 'No entities here' } as Partial<Tweet>);
146+
const tObj = tweet as unknown as { entities?: unknown };
147+
delete tObj.entities;
148+
const wrapper = mount(TweetQuoteCard, {
149+
props: { tweet: tObj as unknown as Tweet },
150+
global: globalConfig,
151+
});
152+
const contentP = wrapper.find('p');
153+
expect(contentP.exists()).toBe(true);
154+
expect(contentP.text()).toContain('No entities here');
155+
expect(contentP.findAll('a').length).toBe(0);
156+
});
157+
158+
it('handles only mentions when hashtags are undefined', () => {
159+
const content = 'Hello @user there';
160+
const tweet = makeTweet();
161+
tweet.content = content;
162+
const entities = {
163+
mentions: [{ username: 'user', startPosition: content.indexOf('@user') }],
164+
// hashtags omitted
165+
} as unknown as Tweet['entities'];
166+
tweet.entities = entities;
167+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
168+
const mention = wrapper.find('a[href="/profile/user"]');
169+
expect(mention.exists()).toBe(true);
170+
expect(wrapper.text()).toContain('Hello');
171+
expect(wrapper.text()).toContain('there');
172+
});
173+
174+
it('handles only hashtags when mentions are undefined', () => {
175+
const content = 'Hello #Tag there';
176+
const tweet = makeTweet();
177+
tweet.content = content;
178+
const entities = {
179+
hashtags: [{ hashtag: 'Tag', startPosition: content.indexOf('#Tag') }],
180+
// mentions omitted
181+
} as unknown as Tweet['entities'];
182+
tweet.entities = entities;
183+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
184+
const hashtag = wrapper.find('a[href="/hashtag/Tag"]');
185+
expect(hashtag.exists()).toBe(true);
186+
expect(wrapper.text()).toContain('Hello');
187+
expect(wrapper.text()).toContain('there');
188+
});
189+
190+
it('handles click on quote card without errors (click.stop)', async () => {
191+
const tweet = makeTweet();
192+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
193+
const card = wrapper.find(`#quoted-tweet-${tweet.id}`);
194+
expect(card.exists()).toBe(true);
195+
await expect(card.trigger('click')).resolves.toBeUndefined();
196+
// Ensure card still exists and DOM unchanged in a basic way
197+
expect(wrapper.find(`#quoted-tweet-${tweet.id}`).exists()).toBe(true);
198+
});
199+
200+
it('applies compact styling through TweetMedia (max width class present)', () => {
201+
const tweet = makeTweet({
202+
media: [{ type: 'GIF', url: '/gif.gif', altText: 'g', width: 200, height: 200 }],
203+
});
204+
const wrapper = mount(TweetQuoteCard, { props: { tweet }, global: globalConfig });
205+
const mediaWrap = wrapper.findComponent(TweetMedia);
206+
expect(mediaWrap.exists()).toBe(true);
207+
// Assert compact prop toggles component; internal class is applied at TweetMedia root
208+
expect(mediaWrap.props('compact')).toBe(true);
209+
});
210+
});

0 commit comments

Comments
 (0)