Skip to content

Commit 037ce4b

Browse files
committed
test: write unit tests for tweet composer composable
1 parent b80defe commit 037ce4b

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { ref } from 'vue';
3+
import { createI18n } from 'vue-i18n';
4+
import messages from '~~/i18n/locales/en.json';
5+
import { useTweetComposer } from '~/composables/useTweetComposer';
6+
import { uploadMediaService } from '@/services/tweet/uploadMediaService';
7+
import { createTweetService } from '@/services/tweet/createTweetService';
8+
import { showToaster } from '@/utils/showToaster';
9+
import type { MediaItem } from '~~/shared/types/shared';
10+
11+
// Mock dependencies
12+
vi.mock('@/services/tweet/uploadMediaService');
13+
vi.mock('@/services/tweet/createTweetService');
14+
vi.mock('@/utils/showToaster');
15+
vi.mock('vue-i18n', async () => {
16+
const actual = await vi.importActual('vue-i18n');
17+
return {
18+
...actual,
19+
useI18n: () => ({
20+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21+
t: (key: string, params?: Record<string, any>) => {
22+
const i18n = createI18n({
23+
locale: 'en',
24+
messages: { en: messages },
25+
});
26+
return i18n.global.t(key, params);
27+
},
28+
}),
29+
};
30+
});
31+
32+
describe('useTweetComposer', () => {
33+
let tweetContent: ReturnType<typeof ref<string>>;
34+
let media: ReturnType<typeof ref<MediaItem[]>>;
35+
let replyToTweetId: ReturnType<typeof ref<string | null>>;
36+
37+
const mockUploadImage = vi.fn();
38+
const mockUploadVideo = vi.fn();
39+
const mockCreateTweetService = vi.fn();
40+
const mockShowToaster = vi.fn();
41+
42+
beforeEach(() => {
43+
vi.clearAllMocks();
44+
45+
tweetContent = ref('');
46+
media = ref([]);
47+
replyToTweetId = ref(null);
48+
49+
(uploadMediaService as ReturnType<typeof vi.fn>).mockReturnValue({
50+
uploadImage: mockUploadImage,
51+
uploadVideo: mockUploadVideo,
52+
});
53+
54+
(createTweetService as ReturnType<typeof vi.fn>).mockImplementation(mockCreateTweetService);
55+
(showToaster as ReturnType<typeof vi.fn>).mockImplementation(mockShowToaster);
56+
57+
global.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
58+
global.URL.revokeObjectURL = vi.fn();
59+
});
60+
61+
afterEach(() => {
62+
vi.restoreAllMocks();
63+
});
64+
65+
describe('handlePost', () => {
66+
it('returns null when content is empty and no media', async () => {
67+
const composer = useTweetComposer(tweetContent, media);
68+
69+
const result = await composer.handlePost();
70+
71+
expect(result).toBeNull();
72+
expect(mockCreateTweetService).not.toHaveBeenCalled();
73+
});
74+
75+
it('returns null when over character limit', async () => {
76+
const composer = useTweetComposer(tweetContent, media);
77+
tweetContent.value = 'a'.repeat(281);
78+
79+
const result = await composer.handlePost();
80+
81+
expect(result).toBeNull();
82+
expect(mockCreateTweetService).not.toHaveBeenCalled();
83+
});
84+
85+
it('posts tweet with text content only', async () => {
86+
const composer = useTweetComposer(tweetContent, media);
87+
tweetContent.value = 'Test tweet';
88+
89+
const mockTweet = { id: '1', content: 'Test tweet' };
90+
mockCreateTweetService.mockResolvedValue(mockTweet);
91+
92+
const result = await composer.handlePost();
93+
94+
expect(mockCreateTweetService).toHaveBeenCalledWith({
95+
content: 'Test tweet',
96+
media: [],
97+
replyToTweetId: null,
98+
});
99+
expect(result).toEqual(mockTweet);
100+
expect(mockShowToaster).toHaveBeenCalledWith('success', expect.any(String));
101+
});
102+
103+
it('uploads images and posts tweet', async () => {
104+
const composer = useTweetComposer(tweetContent, media);
105+
tweetContent.value = 'Tweet with image';
106+
const mockFile = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
107+
composer.handleAddMedia([mockFile]);
108+
109+
mockUploadImage.mockResolvedValue('media-id-1');
110+
const mockTweet = { id: '1', content: 'Tweet with image', media: ['media-id-1'] };
111+
mockCreateTweetService.mockResolvedValue(mockTweet);
112+
113+
const result = await composer.handlePost();
114+
115+
expect(mockUploadImage).toHaveBeenCalledWith(mockFile, 'tweets');
116+
expect(mockCreateTweetService).toHaveBeenCalledWith({
117+
content: 'Tweet with image',
118+
media: ['media-id-1'],
119+
replyToTweetId: null,
120+
});
121+
expect(result).toEqual(mockTweet);
122+
});
123+
124+
it('uploads videos and posts tweet', async () => {
125+
const composer = useTweetComposer(tweetContent, media);
126+
tweetContent.value = 'Tweet with video';
127+
const mockFile = new File(['test'], 'test.mp4', { type: 'video/mp4' });
128+
composer.handleAddMedia([mockFile]);
129+
130+
mockUploadVideo.mockResolvedValue('media-id-1');
131+
const mockTweet = { id: '1', content: 'Tweet with video', media: ['media-id-1'] };
132+
mockCreateTweetService.mockResolvedValue(mockTweet);
133+
134+
const result = await composer.handlePost();
135+
136+
expect(mockUploadVideo).toHaveBeenCalledWith(mockFile, 'tweets');
137+
expect(result).toEqual(mockTweet);
138+
});
139+
140+
it('includes replyToTweetId when provided', async () => {
141+
replyToTweetId.value = 'parent-tweet-id';
142+
const composer = useTweetComposer(tweetContent, media, replyToTweetId);
143+
tweetContent.value = 'Reply tweet';
144+
145+
const mockTweet = { id: '2', content: 'Reply tweet', replyTo: 'parent-tweet-id' };
146+
mockCreateTweetService.mockResolvedValue(mockTweet);
147+
148+
const result = await composer.handlePost();
149+
150+
expect(mockCreateTweetService).toHaveBeenCalledWith({
151+
content: 'Reply tweet',
152+
media: [],
153+
replyToTweetId: 'parent-tweet-id',
154+
});
155+
expect(result).toEqual(mockTweet);
156+
});
157+
158+
it('handles upload error gracefully', async () => {
159+
const composer = useTweetComposer(tweetContent, media);
160+
const mockFile = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
161+
composer.handleAddMedia([mockFile]);
162+
163+
mockUploadImage.mockRejectedValue(new Error('Upload failed'));
164+
165+
const result = await composer.handlePost();
166+
167+
expect(result).toBeNull();
168+
expect(mockShowToaster).toHaveBeenCalledWith('error', expect.any(String));
169+
expect(composer.isPosting.value).toBe(false);
170+
});
171+
172+
it('handles post error gracefully', async () => {
173+
const composer = useTweetComposer(tweetContent, media);
174+
tweetContent.value = 'Test tweet';
175+
176+
mockCreateTweetService.mockRejectedValue(new Error('Post failed'));
177+
178+
const result = await composer.handlePost();
179+
180+
expect(result).toBeNull();
181+
expect(mockShowToaster).toHaveBeenCalledWith('error', expect.any(String));
182+
expect(composer.isPosting.value).toBe(false);
183+
});
184+
});
185+
});

0 commit comments

Comments
 (0)