Files
hyperframes/packages/cli/src/commands/snapshot.test.ts
T
James Russo 3a6b7f0612 fix: align local WebGPU capture behavior (#2907)
* fix: align local WebGPU capture behavior

* fix: address WebGPU capture review feedback

* fix: retain overlapping GPU seek work

* fix: satisfy runtime seek completion types

* fix: drain concurrent GPU seek work

* fix: prevent WebGPU capture barrier starvation

* fix: keep WebGPU presentation active during render seeks
2026-07-30 21:52:30 -07:00

274 lines
8.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import {
computeSnapshotTimes,
formatSnapshotTimestamp,
parseZoomScale,
requireSnapshotFfmpeg,
resolveSnapshotVideoClipStart,
resolveSnapshotVideoFrameTime,
tailFrameTime,
} from "./snapshot.js";
describe("formatSnapshotTimestamp", () => {
it.each([
[1.12, "1.12s"],
[0.30000000000000004, "0.3s"],
])("formats %s without discarding useful precision", (time, expected) => {
expect(formatSnapshotTimestamp(time)).toBe(expected);
});
});
// --zoom's crop-region math (selector bbox + padding + clamp, exact region
// form, no-match error) is owned by and tested in
// ../capture/captureCompositionFrame.test.ts alongside its implementation.
describe("tailFrameTime", () => {
it("backs off ~3% of duration so the final frame isn't the blank exact-end", () => {
// Verified on the V4 3D artifact: t=8.0 of an 8s clip rendered blank white,
// t=7.76 rendered the final hero. 8 - 8*0.03 = 7.76.
expect(tailFrameTime(8)).toBeCloseTo(7.76, 5);
});
it("uses a 50ms floor for short clips", () => {
expect(tailFrameTime(1)).toBeCloseTo(0.95, 5); // 1 - 0.05 (floor beats 3%)
});
it("never goes negative", () => {
expect(tailFrameTime(0)).toBe(0);
});
});
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 })',
);
});
it("exposes --proxy/--no-proxy and forwards the override to the static server", () => {
const source = readFileSync(new URL("./snapshot.ts", import.meta.url), "utf8");
expect(source).toContain("proxy: {");
expect(source).toContain("autoProxy: args.proxy as boolean | undefined");
expect(source).toContain("opts.autoProxy");
});
it("resolves and forwards the shared local browser GPU policy", () => {
const source = readFileSync(new URL("./snapshot.ts", import.meta.url), "utf8");
expect(source).toContain("resolveLocalBrowserGpuMode");
expect(source).toContain("browserGpuMode: opts.browserGpuMode");
expect(source).toContain('"browser-gpu": {');
});
});
describe("resolveSnapshotVideoFrameTime", () => {
it("keeps media active at the inclusive clip end and samples its last decodable frame", () => {
expect(
resolveSnapshotVideoFrameTime({
globalTime: 15,
clipStart: 0,
clipDuration: 15,
relativeTime: 15,
sourceDuration: 15,
}),
).toBeCloseTo(15 - 1 / 30, 6);
});
it("keeps ordinary in-window media timestamps unchanged", () => {
expect(
resolveSnapshotVideoFrameTime({
globalTime: 7.5,
clipStart: 0,
clipDuration: 15,
relativeTime: 7.5,
sourceDuration: 15,
}),
).toBe(7.5);
});
it("does not activate media after the clip end", () => {
expect(
resolveSnapshotVideoFrameTime({
globalTime: 15.001,
clipStart: 0,
clipDuration: 15,
relativeTime: 15.001,
sourceDuration: 15,
}),
).toBeNull();
});
it.each([
{
name: "before clip start",
input: {
globalTime: 4.9,
clipStart: 5,
clipDuration: 10,
relativeTime: 0,
sourceDuration: 10,
},
expected: null,
},
{
name: "negative relative time",
input: {
globalTime: 5,
clipStart: 5,
clipDuration: 10,
relativeTime: -0.1,
sourceDuration: 10,
},
expected: null,
},
{
name: "unknown source duration",
input: {
globalTime: 15,
clipStart: 5,
clipDuration: 10,
relativeTime: 10,
sourceDuration: 0,
},
expected: 10 - 1 / 30,
},
{
name: "offset clip inclusive end",
input: {
globalTime: 15,
clipStart: 5,
clipDuration: 10,
relativeTime: 10,
sourceDuration: 10,
},
expected: 10 - 1 / 30,
},
{
name: "clip end within floating-point tolerance",
input: {
globalTime: 15 + 5e-10,
clipStart: 5,
clipDuration: 10,
relativeTime: 10,
sourceDuration: 10,
},
expected: 10 - 1 / 30,
},
])("handles $name", ({ input, expected }) => {
const result = resolveSnapshotVideoFrameTime(input);
if (expected === null) expect(result).toBeNull();
else expect(result).toBeCloseTo(expected, 6);
});
});
describe("resolveSnapshotVideoClipStart", () => {
it("offsets a scene-local video start by its later template host", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 0,
runtimeResolvedStart: 3,
}),
).toBe(3);
});
it("uses the runtime's recursively resolved start for deeply nested media", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 1,
runtimeResolvedStart: 8,
}),
).toBe(8);
});
it("keeps authored starts as a compatibility fallback", () => {
expect(
resolveSnapshotVideoClipStart({
authoredStart: 3,
runtimeResolvedStart: null,
}),
).toBe(3);
});
});
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 });
expect(times).toHaveLength(5);
expect(times[0]).toBe(0);
expect(times[times.length - 1]).toBeCloseTo(7.76, 5);
expect(times[times.length - 1]).toBeLessThan(8); // not the blank exact-end
expect(appendedTail).toBe(false);
});
it("single frame samples the midpoint", () => {
expect(computeSnapshotTimes(8, { frames: 1 }).times).toEqual([4]);
});
it("explicit --at: keeps the user's times AND appends an end-of-timeline frame", () => {
const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5, at: [1, 2, 3] });
expect(times.slice(0, 3)).toEqual([1, 2, 3]);
expect(times[times.length - 1]).toBeCloseTo(7.76, 5);
expect(appendedTail).toBe(true);
});
it("explicit --at: does not double-add when the user already sampled the tail", () => {
const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5, at: [1, 7.76] });
expect(times).toEqual([1, 7.76]);
expect(appendedTail).toBe(false);
});
it("explicit --at: a sample at exact duration counts as the tail (no append)", () => {
const { appendedTail } = computeSnapshotTimes(8, { frames: 5, at: [1, 8] });
expect(appendedTail).toBe(false);
});
it("respects includeEnd:false opt-out for --at", () => {
const { times, appendedTail } = computeSnapshotTimes(8, {
frames: 5,
at: [1, 2],
includeEnd: false,
});
expect(times).toEqual([1, 2]);
expect(appendedTail).toBe(false);
});
it("preserves exact explicit transition timestamps", () => {
const exactTransition = 3.3666666666666667;
const { times } = computeSnapshotTimes(8, {
frames: 5,
at: [exactTransition],
includeEnd: false,
});
expect(times).toEqual([exactTransition]);
});
});
describe("parseZoomScale (--zoom-scale)", () => {
it("defaults to 3 when unset", () => {
expect(parseZoomScale(undefined)).toBe(3);
});
it("honors an explicit scale", () => {
expect(parseZoomScale("2")).toBe(2);
});
it("falls back to the default for invalid or non-positive input", () => {
expect(parseZoomScale("abc")).toBe(3);
expect(parseZoomScale("0")).toBe(3);
expect(parseZoomScale("-1")).toBe(3);
});
});
describe("requireSnapshotFfmpeg", () => {
it("rejects video snapshot extraction when FFmpeg is unavailable", () => {
expect(() => requireSnapshotFfmpeg(undefined)).toThrow(
/FFmpeg is required to extract video frames for snapshots/,
);
});
it("preserves the resolved FFmpeg executable", () => {
expect(requireSnapshotFfmpeg("C:\\tools\\ffmpeg.exe")).toBe("C:\\tools\\ffmpeg.exe");
});
});