From e43fce3cf1d00c86cb728640dd33b7d9f2cc5158 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 8 Nov 2026 01:45:19 +0000 Subject: [PATCH] fix(scripts): capturing one screenshot scene keeps a device's other scenes A `--scene` run of the mobile showcase script cleared the whole capture folder for each planned device before capturing, so recapturing one scene deleted the device's other scene screenshots while still reporting success. Clearing now happens in `clearShowcaseCaptureOutputs`: a full capture set still clears the folder, stale files included, while a scene subset removes only the selected scenes' PNGs. --- scripts/mobile-showcase.test.ts | 51 +++++++++++++++++++++++++++++++-- scripts/mobile-showcase.ts | 37 +++++++++++++++--------- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/scripts/mobile-showcase.test.ts b/scripts/mobile-showcase.test.ts index 7e6c9e0f0e6df..25e926fcda5e4 100644 --- a/scripts/mobile-showcase.test.ts +++ b/scripts/mobile-showcase.test.ts @@ -1,13 +1,23 @@ -// @effect-diagnostics nodeBuiltinImport:off - expectations mirror the host path joins the script under test performs. +// @effect-diagnostics nodeBuiltinImport:off - Clears capture outputs in a real temporary folder. +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + import { assert, it } from "@effect/vitest"; import { PNG } from "pngjs"; -import type { ShowcaseStoreAssetSpec } from "./mobile-showcase.config.ts"; +import showcaseConfig, { + type ShowcaseScene, + type ShowcaseStoreAssetSpec, +} from "./mobile-showcase.config.ts"; import { + clearShowcaseCaptureOutputs, encodeAndroidPairingUrls, normalizeStorePng, parsePairingCredentialOutput, + planShowcaseCaptures, readPngMetadata, + showcaseCaptureDirectory, validateStoreAsset, } from "./mobile-showcase.ts"; @@ -91,3 +101,40 @@ it("encodes Android pairing URLs without shell-sensitive JSON quotes", () => { assert.deepStrictEqual(JSON.parse(decodeURIComponent(encoded.slice("json-uri:".length))), urls); assert.equal(encoded.includes('"'), false); }); + +it("keeps unselected scenes on a partial capture and clears the folder on a full one", async () => { + const pixel = showcaseConfig.devices.find((device) => device.id === "pixel"); + assert.ok(pixel); + const seeded = [...pixel.scenes.map((scene) => `${scene}.png`), "stale.png"]; + const cases = [ + { + scenes: new Set(["thread"]), + remaining: seeded.filter((file) => file !== "thread.png"), + }, + { scenes: new Set(), remaining: [] }, + ]; + const outputDirectory = await NodeFSP.mkdtemp( + NodePath.join(NodeOS.tmpdir(), "t3-showcase-test-"), + ); + try { + for (const { scenes, remaining } of cases) { + const [capture] = planShowcaseCaptures(showcaseConfig, { + platforms: new Set(), + deviceIds: new Set(["pixel"]), + scenes, + appearances: new Set(), + themes: new Set(), + }); + assert.ok(capture); + const directory = showcaseCaptureDirectory(outputDirectory, capture); + await NodeFSP.mkdir(directory, { recursive: true }); + for (const file of seeded) { + await NodeFSP.writeFile(NodePath.join(directory, file), "png"); + } + await clearShowcaseCaptureOutputs(outputDirectory, capture); + assert.deepStrictEqual((await NodeFSP.readdir(directory)).sort(), remaining.sort()); + } + } finally { + await NodeFSP.rm(outputDirectory, { recursive: true, force: true }); + } +}); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index 425c371fd0c2a..a49fab976b458 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -243,6 +243,27 @@ export function showcaseCaptureDirectory( ); } +function isFullCapture(capture: ShowcaseCapture): boolean { + return capture.scenes.length === capture.device.scenes.length; +} + +export async function clearShowcaseCaptureOutputs( + outputDirectory: string, + capture: ShowcaseCapture, +): Promise { + const directory = showcaseCaptureDirectory(outputDirectory, capture); + if (isFullCapture(capture)) { + await NodeFSP.rm(directory, { recursive: true, force: true }); + } else { + await Promise.all( + capture.scenes.map((scene) => + NodeFSP.rm(NodePath.join(directory, `${scene}.png`), { force: true }), + ), + ); + } + await NodeFSP.mkdir(directory, { recursive: true }); +} + async function finalizeCapture(destination: string, device: ShowcaseDevice): Promise { const normalized = normalizeStorePng(await NodeFSP.readFile(destination)); await NodeFSP.writeFile(destination, normalized); @@ -1537,11 +1558,7 @@ async function main(): Promise { const outputDirectory = NodePath.resolve(REPO_ROOT, showcaseConfig.outputDirectory); if (options.validateOnly) { for (const capture of captures) { - await validateCaptureSet( - capture, - outputDirectory, - capture.scenes.length === capture.device.scenes.length, - ); + await validateCaptureSet(capture, outputDirectory, isFullCapture(capture)); } return; } @@ -1550,9 +1567,7 @@ async function main(): Promise { const metroHost = hasIos ? lanIpv4Address() : "127.0.0.1"; await NodeFSP.mkdir(outputDirectory, { recursive: true }); for (const capture of captures) { - const directory = showcaseCaptureDirectory(outputDirectory, capture); - await NodeFSP.rm(directory, { recursive: true, force: true }); - await NodeFSP.mkdir(directory, { recursive: true }); + await clearShowcaseCaptureOutputs(outputDirectory, capture); } const showcaseRootDir = await NodeFSP.mkdtemp( @@ -1622,11 +1637,7 @@ async function main(): Promise { (cleanup) => androidCleanups.push(cleanup), ); } - await validateCaptureSet( - capture, - outputDirectory, - capture.scenes.length === capture.device.scenes.length, - ); + await validateCaptureSet(capture, outputDirectory, isFullCapture(capture)); } NodeProcess.stdout.write(