1- import { describe , it , expect , beforeEach , vi , afterEach } from 'vitest' ;
21import { renderHook , act } from '@testing-library/react' ;
2+ import { describe , expect , it , vi , beforeEach , afterEach } from 'vitest' ;
33import { useShareActions } from './useShareActions' ;
44import type { DashboardExportData } from '@/types/dashboard' ;
55
66vi . 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
1816describe ( '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