Skip to content

Commit 93f07da

Browse files
committed
test(useShareActions): create hooks/useShareActions.test.ts
1 parent a7f376b commit 93f07da

1 file changed

Lines changed: 134 additions & 19 deletions

File tree

hooks/useShareActions.test.ts

Lines changed: 134 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,90 @@
1-
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
21
import { renderHook, act } from '@testing-library/react';
2+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
33
import { useShareActions } from './useShareActions';
44
import type { DashboardExportData } from '@/types/dashboard';
55

66
vi.mock('html-to-image', () => ({
7+
toPng: vi.fn().mockResolvedValue('data:image/png;base64,mockPng'),
78
toCanvas: vi.fn().mockResolvedValue({
89
toBlob: (cb: (blob: Blob) => void) => {
910
cb(new Blob(['test'], { type: 'image/png' }));
1011
},
12+
toDataURL: vi.fn().mockReturnValue('data:image/webp;base64,mockWebp'),
1113
}),
1214
}));
13-
const mockExportData: DashboardExportData = {
14-
stats: { currentStreak: 5, peakStreak: 10, totalContributions: 100 },
15-
languages: [],
16-
};
1715

1816
describe('useShareActions', () => {
17+
const mockUsername = 'atharv96k';
18+
const mockClose = vi.fn();
19+
20+
const mockExportData: DashboardExportData = {
21+
stats: {
22+
currentStreak: 5,
23+
peakStreak: 12,
24+
totalContributions: 142,
25+
},
26+
languages: [
27+
{ name: 'Java', color: '#b07219', percentage: 70 },
28+
{ name: 'TypeScript', color: '#3178c6', percentage: 30 },
29+
],
30+
};
31+
32+
const originalCreateElement = document.createElement.bind(document);
33+
let mockLinkElement: HTMLAnchorElement;
34+
1935
beforeEach(() => {
36+
vi.clearAllMocks();
2037
vi.useFakeTimers();
21-
Object.assign(navigator, {
22-
clipboard: { writeText: vi.fn().mockResolvedValue(undefined) },
38+
39+
Object.defineProperty(navigator, 'clipboard', {
40+
value: {
41+
writeText: vi.fn().mockResolvedValue(undefined),
42+
},
43+
configurable: true,
2344
});
45+
46+
vi.spyOn(window, 'open').mockImplementation(() => null);
47+
48+
global.URL.createObjectURL = vi.fn().mockReturnValue('blob:mock-url');
49+
global.URL.revokeObjectURL = vi.fn();
50+
51+
mockLinkElement = originalCreateElement('a') as HTMLAnchorElement;
52+
vi.spyOn(mockLinkElement, 'click').mockImplementation(() => undefined);
53+
54+
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
55+
if (tagName === 'a') {
56+
return mockLinkElement;
57+
}
58+
return originalCreateElement(tagName);
59+
});
60+
61+
document.body.innerHTML = '<div id="dashboard-root">Dashboard</div>';
2462
});
2563

2664
afterEach(() => {
2765
vi.useRealTimers();
66+
vi.restoreAllMocks();
67+
document.body.innerHTML = '';
68+
Reflect.deleteProperty(globalThis, 'ClipboardItem');
2869
});
2970

30-
it('transitions copy state from loading to success after handleCopyLink', async () => {
31-
// Verifies the full lifecycle: idle → loading → success on a successful clipboard write
32-
const { result } = renderHook(() => useShareActions('testuser', mockExportData, vi.fn()));
71+
it('handleCopyLink calls navigator.clipboard.writeText with a profile URL containing the username', async () => {
72+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
3373

74+
let success;
3475
await act(async () => {
35-
await result.current.handleCopyLink();
76+
success = await result.current.handleCopyLink();
3677
});
3778

79+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
80+
expect.stringContaining(`/dashboard/${mockUsername}`)
81+
);
82+
expect(success).toBe(true);
3883
expect(result.current.states['copy']).toBe('success');
3984
});
4085

4186
it('resets copy state to idle after 2500ms', async () => {
42-
// Verifies the timeout reset behavior critical for UX — success must not persist forever
43-
const { result } = renderHook(() => useShareActions('testuser', mockExportData, vi.fn()));
87+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
4488

4589
await act(async () => {
4690
await result.current.handleCopyLink();
@@ -54,6 +98,7 @@ describe('useShareActions', () => {
5498

5599
expect(result.current.states['copy']).toBe('idle');
56100
});
101+
57102
it('copies dashboard image to clipboard successfully', async () => {
58103
const writeMock = vi.fn().mockResolvedValue(undefined);
59104
class MockClipboardItem {
@@ -62,25 +107,95 @@ describe('useShareActions', () => {
62107

63108
Object.defineProperty(globalThis, 'ClipboardItem', {
64109
value: MockClipboardItem,
110+
configurable: true,
65111
writable: true,
66112
});
67113

68-
Object.assign(navigator, {
69-
clipboard: {
114+
Object.defineProperty(navigator, 'clipboard', {
115+
value: {
70116
write: writeMock,
71117
writeText: vi.fn().mockResolvedValue(undefined),
72118
},
119+
configurable: true,
73120
});
74121

75-
document.body.innerHTML = '<div id="dashboard-root">Dashboard</div>';
76-
77-
const { result } = renderHook(() => useShareActions('testuser', mockExportData, vi.fn()));
122+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
78123

79124
await act(async () => {
80125
await result.current.handleCopyImage();
81126
});
82-
console.log('copyImage state:', result.current.states['copyImage']);
127+
83128
expect(writeMock).toHaveBeenCalled();
84129
expect(result.current.states['copyImage']).toBe('success');
85130
});
131+
132+
it('handleTwitter opens window to share on twitter/x', () => {
133+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
134+
135+
act(() => {
136+
result.current.handleTwitter();
137+
});
138+
139+
expect(window.open).toHaveBeenCalledWith(
140+
expect.stringContaining('twitter.com/intent/tweet'),
141+
'_blank',
142+
'noopener'
143+
);
144+
expect(mockClose).toHaveBeenCalled();
145+
});
146+
147+
it('handleLinkedIn opens window to share on linkedin', () => {
148+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
149+
150+
act(() => {
151+
result.current.handleLinkedIn();
152+
});
153+
154+
expect(window.open).toHaveBeenCalledWith(
155+
expect.stringContaining('linkedin.com/sharing/share-offsite'),
156+
'_blank',
157+
'noopener'
158+
);
159+
expect(mockClose).toHaveBeenCalled();
160+
});
161+
162+
it('handleReddit opens window to submit link on reddit', () => {
163+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
164+
165+
act(() => {
166+
result.current.handleReddit();
167+
});
168+
169+
expect(window.open).toHaveBeenCalledWith(
170+
expect.stringContaining('reddit.com/submit'),
171+
'_blank',
172+
'noopener,noreferrer'
173+
);
174+
expect(mockClose).toHaveBeenCalled();
175+
});
176+
177+
it('handleCopyMarkdown writes an interactive markdown string to clipboard', async () => {
178+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
179+
180+
await act(async () => {
181+
await result.current.handleCopyMarkdown();
182+
});
183+
184+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
185+
expect.stringContaining(`/api/streak?user=${mockUsername}`)
186+
);
187+
});
188+
189+
it('handleDownloadJSON bundles and formats a structured JSON data blob download', () => {
190+
const { result } = renderHook(() => useShareActions(mockUsername, mockExportData, mockClose));
191+
192+
act(() => {
193+
result.current.handleDownloadJSON();
194+
});
195+
196+
expect(mockLinkElement.download).toContain(`commitpulse-${mockUsername}-stats.json`);
197+
expect(mockLinkElement.href).toBe('blob:mock-url');
198+
expect(mockLinkElement.click).toHaveBeenCalled();
199+
expect(global.URL.createObjectURL).toHaveBeenCalled();
200+
});
86201
});

0 commit comments

Comments
 (0)