Skip to content

Commit eb663a9

Browse files
test(clipboard): cover image and file capture
1 parent 3ccc09d commit eb663a9

3 files changed

Lines changed: 61 additions & 21 deletions

File tree

docs/FUNCTIONAL_TEST_STRATEGY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ really a test of a third-party framework or engine we merely depend on:
4646
dependency (binary/model) is absent, so it runs on a dev Mac + release builds and SKIPS in a
4747
plain `npm ci` CI rather than failing. This is where "the feature works" is proven.
4848
- **swift unit (XCTest / swift-testing via SwiftPM)** - pure logic inside a Swift package.
49-
- **e2e (Playwright Electron)** - the rendered app tour (e2e/, 22 tests today).
49+
- **e2e (Playwright Electron)** - the rendered app tour (e2e/, 23 tests today).
5050
- **gateway smoke (`npm run smoke`)** - the OpenAI `/v1` surface against a running app.
5151

5252
## Surface matrix (status - keep current)
@@ -65,7 +65,7 @@ really a test of a third-party framework or engine we merely depend on:
6565
| Capture -> OCR -> memory ingest seam | integration | vitest: feed a fixture frame through the ingest path into a temp DB | TODO |
6666
| RAG end-to-end (retrieve -> prompt -> answer) | integration | vitest: seed a temp SQLite, run retrieval, assert grounded context | TODO |
6767
| Vault (kdbx + Argon2) | integration | vitest vs temp dir | DONE (`vault-service.test.ts`) |
68-
| Renderer surfaces render | e2e | Playwright tour | DONE (22) |
68+
| Renderer surfaces render | e2e | Playwright tour | DONE (23) |
6969
| Pro dictation / clipboard / replay | e2e + unit | Playwright (`pro.spec.ts`) + pro unit | PARTIAL |
7070

7171
## Rules

docs/P0_P2_INTEGRATION_COVERAGE.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ manual claim does not count as complete integration coverage.
88
## Current status - 2026-07-17
99

1010
- Scope: 155 journeys - 74 P0, 71 P1, and 10 P2.
11-
- Covered by direct integration or E2E evidence: 25 - 18 P0, 6 P1, and 1 P2.
12-
- Left: 130 - including partially covered journeys whose exact release behavior is not yet proven.
11+
- Covered by direct integration or E2E evidence: 26 - 19 P0, 6 P1, and 1 P2.
12+
- Left: 129 - including partially covered journeys whose exact release behavior is not yet proven.
1313
- Green gates today:
1414
- `npm test`: 202 files passed, 1 skipped; 2,190 tests passed, 1 skipped.
1515
- `npm run test:coverage`: 96.75% statements, 91.54% branches, 96.14% functions,
1616
97.52% lines.
1717
- `npm run test:db`: 13 files and 105 real SQLite integration tests passed; Electron ABI
1818
restored afterward.
19-
- `npm run test:e2e`: 22 Playwright Electron tests passed against fresh synthetic temp profiles.
19+
- `npm run test:e2e`: 23 Playwright Electron tests passed against fresh synthetic temp profiles.
2020
- Both TypeScript projects pass.
21-
- Not yet a clean handoff: the exact P0-P2 journey count is 25/155, and strict ESLint currently
21+
- Not yet a clean handoff: the exact P0-P2 journey count is 26/155, and strict ESLint currently
2222
exposes a legacy backlog. Neither is hidden by the coverage percentage.
2323

2424
## Covered P0 journeys
@@ -45,6 +45,8 @@ manual claim does not count as complete integration coverage.
4545
decision, execution, failure, and audit persistence against SQLite.
4646
- #117 - Clipboard records text. `e2e/pro.spec.ts` writes unique text to the real OS clipboard and
4747
waits for the production poller and encrypted history store to expose that exact item over IPC.
48+
- #119 - Clipboard records images and files. `e2e/pro.spec.ts` captures real file URLs and a new
49+
pixel bitmap, then restores the bitmap bytes and verifies their exact dimensions.
4850
- #121 - Clipboard restore text. The same E2E journey overwrites the OS clipboard after capture,
4951
restores the stored item through production IPC, and verifies the original text returns.
5052
- #122 - Clipboard restore file. `e2e/pro.spec.ts` drives the real capture-to-restore path and
@@ -222,7 +224,6 @@ manual claim does not count as complete integration coverage.
222224

223225
## Left - clipboard and vault
224226

225-
- #119 - Clipboard records images and files.
226227
- #123 - Clipboard popup hotkey.
227228
- #127 - Vault copy actions.
228229

e2e/pro.spec.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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

4545
const 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

7584
const 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+
204245
test('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

Comments
 (0)