99 * observations (Attio/Linear/Gmail with no screenshot) must not appear. Asserted
1010 * against the live crm:replay-* IPC: every moment an entity returns lines up with a
1111 * real captured frame.
12- * 2. Text crosses the real OS clipboard, encrypted history, and restore IPC.
12+ * 2. Text and pixel images cross the real OS clipboard, encrypted history, and restore IPC.
1313 * 3. Restoring a copied FILE (of any type, including images) puts a file-url (Finder
1414 * pastes the file), the path as plain text (terminal pastes the path), and the file's
1515 * native bytes (an editor pastes the content) on the OS clipboard. Driven through the
@@ -43,11 +43,12 @@ const nav = async (label: string): Promise<void> => {
4343}
4444
4545const waitForCapturedClip = async (
46- contentType : 'text' | 'file' ,
47- textContent : string
48- ) : Promise < string | null > =>
46+ contentType : 'text' | 'image' | 'file' ,
47+ textContent : string | null ,
48+ excludedIds : string [ ] = [ ]
49+ ) : Promise < string > =>
4950 page . evaluate (
50- async ( { expectedContentType, expectedTextContent } ) => {
51+ async ( { expectedContentType, expectedTextContent, excludedClipIds } ) => {
5152 const api = (
5253 window as unknown as {
5354 api : { proInvoke : ( c : string , ...a : unknown [ ] ) => Promise < unknown > }
@@ -62,14 +63,22 @@ const waitForCapturedClip = async (
6263 } [ ]
6364 const hit = items . find (
6465 ( item ) =>
65- item . contentType === expectedContentType && item . textContent === expectedTextContent
66+ item . contentType === expectedContentType &&
67+ item . textContent === expectedTextContent &&
68+ ! excludedClipIds . includes ( item . id )
6669 )
6770 if ( hit ) return hit . id
6871 await new Promise ( ( resolve ) => setTimeout ( resolve , 300 ) )
6972 }
70- return null
73+ throw new Error (
74+ `Timed out waiting for ${ expectedContentType } clipboard item ${ String ( expectedTextContent ) } `
75+ )
7176 } ,
72- { expectedContentType : contentType , expectedTextContent : textContent }
77+ {
78+ expectedContentType : contentType ,
79+ expectedTextContent : textContent ,
80+ excludedClipIds : excludedIds
81+ }
7382 )
7483
7584const restoreCapturedClip = async ( id : string ) : Promise < boolean > =>
@@ -192,15 +201,47 @@ test('Capturing and restoring text crosses the real OS clipboard and encrypted h
192201 } , payload )
193202
194203 const capturedId = await waitForCapturedClip ( 'text' , payload )
195- expect ( capturedId , 'text clip was captured into history' ) . not . toBeNull ( )
196204
197205 await app . evaluate ( async ( { clipboard } ) => {
198206 clipboard . writeText ( 'clipboard overwritten before restore' )
199207 } )
200- expect ( await restoreCapturedClip ( capturedId ! ) ) . toBe ( true )
208+ expect ( await restoreCapturedClip ( capturedId ) ) . toBe ( true )
201209 expect ( await app . evaluate ( async ( { clipboard } ) => clipboard . readText ( ) ) ) . toBe ( payload )
202210} )
203211
212+ test ( 'Capturing a pixel image persists its bytes and restores the bitmap' , async ( ) => {
213+ const existingImageIds = await page . evaluate ( async ( ) => {
214+ const api = (
215+ window as unknown as { api : { proInvoke : ( c : string , ...a : unknown [ ] ) => Promise < unknown > } }
216+ ) . api
217+ const items = ( await api . proInvoke ( 'clipboard:list' , 50 , 'image' ) ) as { id : string } [ ]
218+ return items . map ( ( item ) => item . id )
219+ } )
220+ const sharp = ( await import ( 'sharp' ) ) . default
221+ const png = await sharp ( {
222+ create : { width : 37 , height : 29 , channels : 3 , background : { r : 28 , g : 211 , b : 153 } }
223+ } )
224+ . png ( )
225+ . toBuffer ( )
226+ await app . evaluate ( async ( { clipboard, nativeImage } , base64 ) => {
227+ clipboard . clear ( )
228+ clipboard . writeImage ( nativeImage . createFromBuffer ( Buffer . from ( base64 , 'base64' ) ) )
229+ } , png . toString ( 'base64' ) )
230+
231+ const capturedId = await waitForCapturedClip ( 'image' , null , existingImageIds )
232+ await app . evaluate ( async ( { clipboard } ) =>
233+ clipboard . writeText ( 'image overwritten before restore' )
234+ )
235+ expect ( await restoreCapturedClip ( capturedId ) ) . toBe ( true )
236+
237+ const restored = await app . evaluate ( async ( { clipboard } ) => {
238+ const image = clipboard . readImage ( )
239+ return { empty : image . isEmpty ( ) , size : image . getSize ( ) }
240+ } )
241+ expect ( restored . empty ) . toBe ( false )
242+ expect ( restored . size ) . toEqual ( { width : 37 , height : 29 } )
243+ } )
244+
204245test ( 'Restoring a copied file puts BOTH the path text and the file-url on the clipboard' , async ( ) => {
205246 // 1. A real file on disk (written from the test process — Playwright's evaluate
206247 // sandbox has no `require`), then simulate a Finder "copy file" by putting its
@@ -217,8 +258,7 @@ test('Restoring a copied file puts BOTH the path text and the file-url on the cl
217258 // 2. Wait for capture, then restore that item via the real IPC.
218259 const restoredId = await waitForCapturedClip ( 'file' , copied . basename )
219260
220- expect ( restoredId , 'file clip was captured' ) . not . toBeNull ( )
221- expect ( await restoreCapturedClip ( restoredId ! ) ) . toBe ( true )
261+ expect ( await restoreCapturedClip ( restoredId ) ) . toBe ( true )
222262
223263 // 3. The multi-flavor write runs through osascript (async); poll the pasteboard.
224264 const pb = await app . evaluate ( async ( { clipboard } ) => {
@@ -261,8 +301,7 @@ test('Restoring a copied IMAGE file still gives the terminal a path (plus pixels
261301 } , copied . fileUrl )
262302
263303 const restoredId = await waitForCapturedClip ( 'file' , copied . basename )
264- expect ( restoredId , 'image clip was captured' ) . not . toBeNull ( )
265- expect ( await restoreCapturedClip ( restoredId ! ) ) . toBe ( true )
304+ expect ( await restoreCapturedClip ( restoredId ) ) . toBe ( true )
266305
267306 const pb = await app . evaluate ( async ( { clipboard } ) => {
268307 const deadline = Date . now ( ) + 6000
0 commit comments