authorgravatar for saphid@gmail.comAlex Southwell <saphid@gmail.com> 2026-10-06 20:22:39+11:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-06 04:22:39-05:00
log411cef0008fa8e1e5251a8aae2cc6a215f700a9c
treef21e1dc7a20d5d76ab9863cf0d70d99d52a47b66
parentd0dc45885c1441da5bd46dcf13124d94ed728b0a
signature Signed by PGP key B5690EEEBB952194

fix(desktop): prevent browser screenshot filename collisions (#14784)


2 files changed, 35 insertions(+), 1 deletions(-)

apps/desktop/src/preview/Manager.test.ts+34
...@@ -2367,6 +2367,40 @@ describe("PreviewManager", () => {...@@ -2367,6 +2367,40 @@ describe("PreviewManager", () => {
2367 ),2367 ),
2368 );2368 );
23692369
2370 effectIt.effect("preserves both screenshots of the same site in the same millisecond", () =>
2371 withManager((manager) =>
2372 Effect.gen(function* () {
2373 const firstPng = Buffer.from("first-preview-png");
2374 const secondPng = Buffer.from("second-preview-png");
2375 const pngImage = (png: Buffer) => ({
2376 toPNG: () => png,
2377 toJPEG: () => png,
2378 getSize: () => ({ width: 1280, height: 720 }),
2379 });
2380 const capturePage = vi
2381 .fn<() => Promise<ReturnType<typeof pngImage>>>()
2382 .mockResolvedValueOnce(pngImage(firstPng))
2383 .mockResolvedValueOnce(pngImage(secondPng));
2384 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
2385 yield* manager.createTab("tab_1");
2386 yield* manager.registerWebview("tab_1", 42);
2387 // it.effect keeps TestClock fixed until explicitly advanced.
2388 const first = yield* manager.captureScreenshot("tab_1");
2389 const second = yield* manager.captureScreenshot("tab_1");
2390
2391 expect(first.createdAt).toBe(second.createdAt);
2392 expect(first.id).not.toBe(second.id);
2393 expect(first.path).not.toBe(second.path);
2394 expect(first.id).toMatch(/^browser-screenshot-example-com-[a-z0-9]+-[0-9a-f]{8}$/);
2395 expect(second.id).toMatch(/^browser-screenshot-example-com-[a-z0-9]+-[0-9a-f]{8}$/);
2396 expect(writeFile.mock.calls).toEqual([
2397 [first.path, firstPng],
2398 [second.path, secondPng],
2399 ]);
2400 }),
2401 ),
2402 );
2403
2370 effectIt.effect("keeps every recorded guest unthrottled until its frame capture stops", () =>2404 effectIt.effect("keeps every recorded guest unthrottled until its frame capture stops", () =>
2371 withManager((manager) =>2405 withManager((manager) =>
2372 Effect.gen(function* () {2406 Effect.gen(function* () {
apps/desktop/src/preview/Manager.ts+1-1
...@@ -2974,7 +2974,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -2974,7 +2974,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
2974 wc,2974 wc,
2975 ),2975 ),
2976 ]);2976 ]);
2977 const id = `browser-screenshot-${artifactSiteSlug(wc.getURL())}-${millis.toString(36)}`;2977 const id = `browser-screenshot-${artifactSiteSlug(wc.getURL())}-${millis.toString(36)}-${NodeCrypto.randomUUID().slice(0, 8)}`;
2978 const artifactPath = path.join(resolvedArtifactDirectory, `${id}.png`);2978 const artifactPath = path.join(resolvedArtifactDirectory, `${id}.png`);
2979 const data = image.toPNG();2979 const data = image.toPNG();
2980 yield* fileSystem.makeDirectory(resolvedArtifactDirectory, { recursive: true }).pipe(2980 yield* fileSystem.makeDirectory(resolvedArtifactDirectory, { recursive: true }).pipe(