mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
* 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
274 lines
8.0 KiB
TypeScript
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");
|
|
});
|
|
});
|