diff --git a/packages/cli/src/capture/captureCompositionFrame.test.ts b/packages/cli/src/capture/captureCompositionFrame.test.ts index c2e83396cd..d7fd69ef0c 100644 --- a/packages/cli/src/capture/captureCompositionFrame.test.ts +++ b/packages/cli/src/capture/captureCompositionFrame.test.ts @@ -293,7 +293,11 @@ describe("captureRegionCrop", () => { const buffer = await captureRegionCrop(page, region, 3); expect(setViewport).toHaveBeenNthCalledWith(1, { ...original, deviceScaleFactor: 3 }); - expect(screenshot).toHaveBeenCalledWith({ clip: region, type: "png" }); + expect(screenshot).toHaveBeenCalledWith({ + clip: region, + type: "png", + omitBackground: true, + }); expect(setViewport).toHaveBeenNthCalledWith(2, original); expect(buffer).toBeInstanceOf(Buffer); expect(Array.from(buffer)).toEqual([1, 2, 3]); diff --git a/packages/cli/src/capture/captureCompositionFrame.ts b/packages/cli/src/capture/captureCompositionFrame.ts index 2b0d164094..64ba22303f 100644 --- a/packages/cli/src/capture/captureCompositionFrame.ts +++ b/packages/cli/src/capture/captureCompositionFrame.ts @@ -421,7 +421,7 @@ export interface CropCapturePage { height: number; deviceScaleFactor?: number; }): Promise; - screenshot(options: { clip: CropRegion; type: "png" }): Promise; + screenshot(options: { clip: CropRegion; type: "png"; omitBackground: true }): Promise; } /** @@ -440,7 +440,7 @@ export async function captureRegionCrop( const original = page.viewport(); if (original) await page.setViewport({ ...original, deviceScaleFactor: scale }); try { - const shot = await page.screenshot({ clip: region, type: "png" }); + const shot = await page.screenshot({ clip: region, type: "png", omitBackground: true }); return Buffer.isBuffer(shot) ? shot : Buffer.from(shot); } finally { if (original) await page.setViewport(original); diff --git a/packages/cli/src/commands/keyframes.test.ts b/packages/cli/src/commands/keyframes.test.ts index 14e489dbd5..dd010bfe07 100644 --- a/packages/cli/src/commands/keyframes.test.ts +++ b/packages/cli/src/commands/keyframes.test.ts @@ -1,9 +1,10 @@ -import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; import { ensureDOMParser } from "../utils/dom.js"; import { collectShotSelectors, resolveScope, surfaceComposition } from "./keyframes.js"; +import { ensureShotOutputDir } from "./motionShot.js"; beforeAll(() => ensureDOMParser()); @@ -26,6 +27,15 @@ describe("keyframes direct composition scope", () => { }); }); +describe("keyframes shot output", () => { + it("creates a missing parent directory before writing --shot", () => { + const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-dir-")); + const outputDir = join(projectDir, "nested", "proofs"); + ensureShotOutputDir(join(outputDir, "shot.png")); + expect(existsSync(outputDir)).toBe(true); + }); +}); + describe("keyframes multi-stroke traces", () => { it("composites ≥2 position strokes on one element into a single trace", () => { const html = wrap(` diff --git a/packages/cli/src/commands/motionShot.ts b/packages/cli/src/commands/motionShot.ts index 0bf1061c6b..f5e101984c 100644 --- a/packages/cli/src/commands/motionShot.ts +++ b/packages/cli/src/commands/motionShot.ts @@ -10,7 +10,8 @@ // exactly what it's editing. All geometry + SVG live in ./motionShotLayout.ts // (pure, tested); this file only drives the browser and SAMPLES. -import { writeFileSync } from "node:fs"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; import { buildOnionSvg, ghostAlphas, @@ -25,6 +26,10 @@ export interface ShotRequest { selector: string; } +export function ensureShotOutputDir(outPath: string): void { + mkdirSync(dirname(outPath), { recursive: true }); +} + /** Returned by the in-browser selector resolver: which animated selectors a * `--selector SCOPE` actually resolves to (scope itself, or its descendants), * plus diagnostic context when nothing under the scope animates. */ @@ -611,6 +616,7 @@ export async function captureMotionPathShot( outPath: string, opts: ShotOptions = {}, ): Promise { + ensureShotOutputDir(outPath); let requests = requestsIn; const samples = Math.max(1, Math.min(60, opts.samples ?? 9)); const layout = opts.layout ?? "path"; diff --git a/packages/cli/src/commands/snapshot.test.ts b/packages/cli/src/commands/snapshot.test.ts index 4cb0d394c7..c88e1baf5b 100644 --- a/packages/cli/src/commands/snapshot.test.ts +++ b/packages/cli/src/commands/snapshot.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { computeSnapshotTimes, parseZoomScale, tailFrameTime } from "./snapshot.js"; +import { readFileSync } from "node:fs"; // --zoom's crop-region math (selector bbox + padding + clamp, exact region // form, no-match error) is owned by and tested in @@ -21,6 +22,15 @@ describe("tailFrameTime", () => { }); }); +describe("transparent snapshot capture", () => { + it("asks Chrome to retain the alpha channel in review PNGs", () => { + const source = readFileSync(new URL("./snapshot.ts", import.meta.url), "utf8"); + expect(source).toContain( + 'page.screenshot({ path: framePath, type: "png", omitBackground: true })', + ); + }); +}); + describe("computeSnapshotTimes (FINDING [7]: tail is always captured)", () => { it("default frames: last point is the readable tail, never exact duration", () => { const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5 }); diff --git a/packages/cli/src/commands/snapshot.ts b/packages/cli/src/commands/snapshot.ts index e1f34f9615..20597cac3e 100644 --- a/packages/cli/src/commands/snapshot.ts +++ b/packages/cli/src/commands/snapshot.ts @@ -465,7 +465,7 @@ async function captureSnapshots( ); writeFileSync(framePath, buffer); } else { - await page.screenshot({ path: framePath, type: "png" }); + await page.screenshot({ path: framePath, type: "png", omitBackground: true }); } const rel = relative(projectDir, framePath); savedPaths.push(rel.startsWith("..") || isAbsolute(rel) ? framePath : rel);