@@ -14,6 +14,16 @@ const mockDialog = {
1414 showSaveDialog : vi . fn ( )
1515}
1616
17+ const mockClipboard = {
18+ writeImage : vi . fn ( ) ,
19+ }
20+
21+ const mockNativeImage = {
22+ createFromPath : vi . fn ( ) ,
23+ createFromDataURL : vi . fn ( ) ,
24+ createFromBuffer : vi . fn ( ) ,
25+ }
26+
1727const mockFs = {
1828 existsSync : vi . fn ( ) ,
1929 mkdirSync : vi . fn ( ) ,
@@ -36,7 +46,9 @@ const mockIpcMain = {
3646// Mock modules
3747vi . mock ( 'electron' , ( ) => ( {
3848 ipcMain : mockIpcMain ,
39- dialog : mockDialog
49+ dialog : mockDialog ,
50+ clipboard : mockClipboard ,
51+ nativeImage : mockNativeImage ,
4052} ) )
4153
4254vi . mock ( 'path' , ( ) => ( {
@@ -66,6 +78,7 @@ vi.mock('../../../../shared/ipc/channels.js', () => ({
6678 FILE : {
6779 GET_IMAGE_PATH : 'file:getImagePath' ,
6880 DOWNLOAD_MARKDOWN : 'file:downloadMarkdown' ,
81+ COPY_IMAGE_TO_CLIPBOARD : 'file:copyImageToClipboard' ,
6982 SELECT_DIALOG : 'file:selectDialog' ,
7083 UPLOAD : 'file:upload' ,
7184 UPLOAD_FILE_CONTENT : 'file:uploadFileContent'
@@ -87,11 +100,112 @@ describe('File Handler', () => {
87100 mockFileLogic . getUploadDir . mockReturnValue ( '/uploads' )
88101 mockFs . statSync . mockReturnValue ( { size : 1024 } )
89102 mockFs . existsSync . mockReturnValue ( true )
103+ const fakeImage = { isEmpty : vi . fn ( ( ) => false ) }
104+ mockNativeImage . createFromPath . mockReturnValue ( fakeImage )
105+ mockNativeImage . createFromDataURL . mockReturnValue ( fakeImage )
106+ mockNativeImage . createFromBuffer . mockReturnValue ( fakeImage )
90107
91108 const { registerFileHandlers } = await import ( '../file.handler.js' )
92109 registerFileHandlers ( )
93110 } )
94111
112+ describe ( 'file:copyImageToClipboard' , ( ) => {
113+ it ( 'should copy image from local path successfully' , async ( ) => {
114+ const handler = handlers . get ( 'file:copyImageToClipboard' )
115+ const result = await handler ! ( { } , '/tmp/page.png' )
116+
117+ expect ( result ) . toEqual ( {
118+ success : true ,
119+ data : { copied : true } ,
120+ } )
121+ expect ( mockNativeImage . createFromPath ) . toHaveBeenCalledWith ( '/tmp/page.png' )
122+ expect ( mockClipboard . writeImage ) . toHaveBeenCalledTimes ( 1 )
123+ } )
124+
125+ it ( 'should copy image from data URL successfully' , async ( ) => {
126+ const handler = handlers . get ( 'file:copyImageToClipboard' )
127+ const result = await handler ! ( { } , 'data:image/png;base64,abcd' )
128+
129+ expect ( result . success ) . toBe ( true )
130+ expect ( mockNativeImage . createFromDataURL ) . toHaveBeenCalledWith ( 'data:image/png;base64,abcd' )
131+ expect ( mockClipboard . writeImage ) . toHaveBeenCalledTimes ( 1 )
132+ } )
133+
134+ it ( 'should copy image from file URL successfully' , async ( ) => {
135+ const handler = handlers . get ( 'file:copyImageToClipboard' )
136+ const result = await handler ! ( { } , 'file:///tmp/page.png' )
137+
138+ expect ( result . success ) . toBe ( true )
139+ expect ( mockNativeImage . createFromPath ) . toHaveBeenCalledWith ( expect . stringContaining ( 'page.png' ) )
140+ expect ( mockClipboard . writeImage ) . toHaveBeenCalledTimes ( 1 )
141+ } )
142+
143+ it ( 'should return error when image source is missing' , async ( ) => {
144+ const handler = handlers . get ( 'file:copyImageToClipboard' )
145+ const result = await handler ! ( { } , '' )
146+
147+ expect ( result ) . toEqual ( {
148+ success : false ,
149+ error : 'Image source is required' ,
150+ } )
151+ } )
152+
153+ it ( 'should reject remote image URLs' , async ( ) => {
154+ const handler = handlers . get ( 'file:copyImageToClipboard' )
155+ const result = await handler ! ( { } , 'https://cdn.example.com/page.png' )
156+
157+ expect ( result ) . toEqual ( {
158+ success : false ,
159+ error : 'Remote image URLs are not allowed' ,
160+ } )
161+ expect ( mockClipboard . writeImage ) . not . toHaveBeenCalled ( )
162+ } )
163+
164+ it ( 'should return error when image is empty' , async ( ) => {
165+ mockNativeImage . createFromPath . mockReturnValueOnce ( {
166+ isEmpty : vi . fn ( ( ) => true ) ,
167+ } )
168+
169+ const handler = handlers . get ( 'file:copyImageToClipboard' )
170+ const result = await handler ! ( { } , '/tmp/empty.png' )
171+
172+ expect ( result ) . toEqual ( {
173+ success : false ,
174+ error : 'Image data is empty or invalid' ,
175+ } )
176+ expect ( mockClipboard . writeImage ) . not . toHaveBeenCalled ( )
177+ } )
178+
179+ it ( 'should return error when nativeImage creation throws' , async ( ) => {
180+ mockNativeImage . createFromPath . mockImplementationOnce ( ( ) => {
181+ throw new Error ( 'createFromPath failed' )
182+ } )
183+
184+ const handler = handlers . get ( 'file:copyImageToClipboard' )
185+ const result = await handler ! ( { } , '/tmp/bad.png' )
186+
187+ expect ( result ) . toEqual ( {
188+ success : false ,
189+ error : 'createFromPath failed' ,
190+ } )
191+ expect ( mockClipboard . writeImage ) . not . toHaveBeenCalled ( )
192+ } )
193+
194+ it ( 'should return error when clipboard write throws' , async ( ) => {
195+ mockClipboard . writeImage . mockImplementationOnce ( ( ) => {
196+ throw new Error ( 'clipboard failed' )
197+ } )
198+
199+ const handler = handlers . get ( 'file:copyImageToClipboard' )
200+ const result = await handler ! ( { } , '/tmp/page.png' )
201+
202+ expect ( result ) . toEqual ( {
203+ success : false ,
204+ error : 'clipboard failed' ,
205+ } )
206+ } )
207+ } )
208+
95209 describe ( 'file:getImagePath' , ( ) => {
96210 it ( 'should return image path and exists status' , async ( ) => {
97211 mockFs . existsSync . mockReturnValue ( true )
0 commit comments