Files
hyperframes/packages/engine/src/services/screenshotService.test.ts
T
Varo 76204ec630 fix(engine): pre-create __render_frame__ siblings in initializeSession (#2006)
* fix(engine): commit render-frame siblings with a visual BeginFrame at init

Chunk-lambda renders drop a periodic near-black frame — one every
chunk_frames/worker_count frames (every 60 on a 4-worker single-video chunk),
YAVG ~22 against YMAX ~240 in signalstats. Local single-process renders don't
show it because they don't run under BeginFrame.

It's the isNewImage branch in injectVideoFramesBatch: the first time a session
paints a given videoId there's no __render_frame__ sibling yet, so it creates
the <img> on the spot (createElement + insertBefore) right before capture.
Under HeadlessExperimental.BeginFrame the compositor doesn't have that fresh
layer in the immediately-next frame, so the first captured frame per session
paints only body background + already-composited overlays. Each lambda worker
is its own session, hence the worker-boundary periodicity.

Pre-create the hidden sibling at the end of initializeSession, then drive one
non-capture visual BeginFrame (noDisplayUpdates: false) to composite the new
layers before the first real capture. The warmup ticks are noDisplayUpdates:
true (they advance the clock but don't paint) and the per-frame seek doesn't
tick, so this explicit visual frame is what actually commits the layers; its
tick sits in the gap between warmup and frame 0 so ticks stay monotonic and no
render frame is consumed. Every subsequent inject then takes the hasImg=true
(src-update) path; the isNewImage branch stays as a fallback for callers that
don't go through initializeSession.

* fix(engine): place the render-frame commit tick before the liveness probe

The commit tick at init sends its BeginFrame at `beginFrameTimeTicks - 1·interval`.
The producer's liveness probe then fires right after init at
`beginFrameTimeTicks - 5·interval` — an earlier tick. Per-session BeginFrame time
has to be monotonic, so the probe running backwards past the commit tick stalls
chrome-headless-shell indefinitely; the engine reads that timeout as a SwiftShader
heavy-layer stall and routes the render to screenshot capture, which then dies
relaunching and hangs the shard to the job timeout.

Reproduced on a native x86 SwiftShader host and bisected: with the commit tick
present the probe times out even with zero render-frame siblings created, so it's
the tick ordering, not layer count. Moving the commit tick to `-6·interval` (below
the probe, above the warmup ticks) keeps warmup < commit < probe < capture
monotonic and clears the stall on every affected comp — sub-composition-video,
chat, style-5-prod — while a healthy comp (style-18-prod) is unchanged. The commit
tick itself is untouched, so the black-frame fix it exists for still holds.
2026-07-07 16:21:59 -04:00

835 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment node
// fallow-ignore-file code-duplication
import { describe, it, expect, vi } from "vitest";
import { parseHTML } from "linkedom";
import { type Page } from "puppeteer-core";
import {
pageScreenshotCapture,
cdpSessionCache,
ensureRenderFrameSiblings,
applyDomLayerMask,
removeDomLayerMask,
injectVideoFramesBatch,
syncVideoFrameVisibility,
shouldDefaultCaptureBeyondViewport,
DOM_LAYER_MASK_STYLE_ID,
} from "./screenshotService.js";
// Stub a Page + CDPSession just enough that pageScreenshotCapture can call
// `client.send("Page.captureScreenshot", ...)` and we can inspect the args.
function makeFakePageWithCdp(send: (method: string, params: object) => Promise<{ data: string }>) {
const fakeSession = { send } as unknown as import("puppeteer-core").CDPSession;
// Stub a Page object — the WeakMap cache is the only Page-thing used in the
// path under test, so we can pre-seed it and skip page.createCDPSession().
const fakePage = {} as Page;
cdpSessionCache.set(fakePage, fakeSession);
return fakePage;
}
describe("pageScreenshotCapture supersample plumbing", () => {
// Minimal 1×1 transparent PNG, base64. The function returns Buffer.from(data, "base64")
// and we never inspect the bytes — only the params we pass to client.send.
const ONE_PIXEL_PNG_B64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
it("passes `clip` with scale 1 when deviceScaleFactor is undefined (default 1)", async () => {
const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
const page = makeFakePageWithCdp(send);
await pageScreenshotCapture(page, {
width: 1920,
height: 1080,
fps: { num: 30, den: 1 },
format: "jpeg",
quality: 80,
});
expect(send).toHaveBeenCalledWith(
"Page.captureScreenshot",
expect.objectContaining({
captureBeyondViewport: false,
clip: { x: 0, y: 0, width: 1920, height: 1080, scale: 1 },
}),
);
});
it("uses captureBeyondViewport only when callers opt in", async () => {
const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
const page = makeFakePageWithCdp(send);
await pageScreenshotCapture(page, {
width: 1080,
height: 1920,
fps: { num: 30, den: 1 },
format: "jpeg",
captureBeyondViewport: true,
});
expect(send).toHaveBeenCalledWith(
"Page.captureScreenshot",
expect.objectContaining({
captureBeyondViewport: true,
clip: { x: 0, y: 0, width: 1080, height: 1920, scale: 1 },
}),
);
});
it("passes `clip` with scale 1 when deviceScaleFactor is exactly 1", async () => {
const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
const page = makeFakePageWithCdp(send);
await pageScreenshotCapture(page, {
width: 1920,
height: 1080,
fps: { num: 30, den: 1 },
format: "jpeg",
deviceScaleFactor: 1,
});
const params = send.mock.calls[0]?.[1] as { clip?: { scale: number } };
expect(params.clip).toEqual({ x: 0, y: 0, width: 1920, height: 1080, scale: 1 });
});
it("passes `clip` with `scale = dpr` when deviceScaleFactor > 1 (the supersample contract)", async () => {
const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
const page = makeFakePageWithCdp(send);
await pageScreenshotCapture(page, {
width: 1920,
height: 1080,
fps: { num: 30, den: 1 },
format: "jpeg",
deviceScaleFactor: 2,
});
expect(send).toHaveBeenCalledWith(
"Page.captureScreenshot",
expect.objectContaining({
clip: { x: 0, y: 0, width: 1920, height: 1080, scale: 2 },
}),
);
});
it("propagates a non-2 supersample factor (e.g. 720p → 4K = 3×)", async () => {
const send = vi.fn().mockResolvedValue({ data: ONE_PIXEL_PNG_B64 });
const page = makeFakePageWithCdp(send);
await pageScreenshotCapture(page, {
width: 1280,
height: 720,
fps: { num: 30, den: 1 },
format: "jpeg",
deviceScaleFactor: 3,
});
const params = send.mock.calls[0]?.[1] as { clip?: { scale: number } };
expect(params.clip?.scale).toBe(3);
});
});
describe("shouldDefaultCaptureBeyondViewport", () => {
it("guards regular Chrome on macOS", () => {
expect(shouldDefaultCaptureBeyondViewport("Chrome/149.0.7827.155", "darwin")).toBe(true);
});
it("keeps chrome-headless-shell on the faster viewport-bound path", () => {
expect(shouldDefaultCaptureBeyondViewport("HeadlessChrome/148.0.7778.97", "darwin")).toBe(
false,
);
});
it("does not change regular Chrome defaults on non-macOS platforms", () => {
expect(shouldDefaultCaptureBeyondViewport("Chrome/149.0.7827.155", "linux")).toBe(false);
});
});
describe("injectVideoFramesBatch replacement layout", () => {
it("does not copy opposing inset constraints onto the injected frame image", async () => {
const { window, document } = parseHTML(
'<html><body><div id="root"><video id="clip" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"></video></div></body></html>',
);
Object.defineProperty(window.HTMLImageElement.prototype, "decode", {
configurable: true,
value: () => Promise.resolve(),
});
const video = document.getElementById("clip") as HTMLVideoElement;
Object.defineProperties(video, {
offsetLeft: { configurable: true, get: () => 0 },
offsetTop: { configurable: true, get: () => 0 },
offsetWidth: { configurable: true, get: () => 1920 },
offsetHeight: { configurable: true, get: () => 1080 },
});
video.getBoundingClientRect = () =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: 1920,
bottom: 1080,
width: 1920,
height: 1080,
toJSON: () => ({}),
}) as DOMRect;
const computedStyle = document.createElement("div").style;
computedStyle.position = "absolute";
computedStyle.width = "1920px";
computedStyle.height = "1080px";
computedStyle.top = "0px";
computedStyle.left = "0px";
computedStyle.right = "0px";
computedStyle.bottom = "0px";
computedStyle.inset = "0px";
computedStyle.objectFit = "cover";
computedStyle.objectPosition = "center center";
computedStyle.zIndex = "3";
computedStyle.opacity = "1";
Object.defineProperty(window, "getComputedStyle", {
configurable: true,
value: () => computedStyle,
});
const globals = globalThis as unknown as {
window?: typeof window;
document?: Document;
};
const previousWindow = globals.window;
const previousDocument = globals.document;
globals.window = window;
globals.document = document;
try {
const page = {
evaluate: async (
fn: (
updates: Array<{ videoId: string; dataUri: string }>,
visualProperties: string[],
) => Promise<void>,
updates: Array<{ videoId: string; dataUri: string }>,
visualProperties: string[],
) => fn(updates, visualProperties),
} as unknown as Page;
await injectVideoFramesBatch(page, [
{
videoId: "clip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
globals.window = previousWindow;
globals.document = previousDocument;
}
const img = video.nextElementSibling as HTMLImageElement | null;
expect(img).not.toBeNull();
expect(img?.style.position).toBe("absolute");
expect(img?.style.left).toBe("0px");
expect(img?.style.top).toBe("0px");
expect(img?.style.width).toBe("1920px");
expect(img?.style.height).toBe("1080px");
expect(img?.style.right).toBe("auto");
expect(img?.style.bottom).toBe("auto");
expect(img?.style.inset).toBe("auto");
});
});
describe("video-frame injection respects ancestor visibility", () => {
// Regression guard: the runtime's `[data-start]` lifecycle hides
// out-of-window sub-composition hosts with `visibility:hidden`, but the
// injector used to ignore that and paint a replacement <img> for every
// active `<video data-start>` element. Inner-PIP videos inside *other*
// moments still appear active in the raw time-window check (their auto-
// injected `data-start="0"` + probed full-source duration cover the
// whole timeline), so the bug produced one full-bleed speaker overlay
// per inactive sub-comp — covering whichever moment was actually visible.
//
// The skip is intentionally narrow: `visibility:hidden` on a regular
// `[data-start]` container must NOT skip injection, because the
// replacement <img>'s explicit `visibility:visible` overrides the
// ancestor (CSS spec) and consumers rely on that to hold the final
// GSAP-driven frame when an authored `data-duration` outlives the
// composition's GSAP timeline. We therefore only treat
// `visibility:hidden` as a skip signal on sub-composition hosts
// (`[data-composition-src]` / `[data-composition-file]`). `display:none`,
// by contrast, takes the whole subtree out of layout regardless of any
// child override, so it always triggers the skip.
type StyleLike = {
display?: string;
visibility?: string;
opacity?: string;
objectFit?: string;
objectPosition?: string;
zIndex?: string;
};
type HostAttribute = "data-composition-src" | "data-composition-file" | "data-start";
function setupHostHiddenScenario(
hostStyle: StyleLike,
options: { hostAttribute?: HostAttribute; videoStyle?: StyleLike } = {},
) {
const hostAttribute = options.hostAttribute ?? "data-composition-src";
const hostAttrMarkup =
hostAttribute === "data-start"
? 'data-start="0" data-duration="10"'
: `${hostAttribute}="sub.html"`;
const { window, document } = parseHTML(
`<html><body><div id="host" ${hostAttrMarkup}><div id="pip-frame"><video id="pip" data-start="0" data-duration="10"></video></div></div></body></html>`,
);
Object.defineProperty(window.HTMLImageElement.prototype, "decode", {
configurable: true,
value: () => Promise.resolve(),
});
const host = document.getElementById("host") as HTMLElement;
const pipFrame = document.getElementById("pip-frame") as HTMLElement;
const video = document.getElementById("pip") as HTMLVideoElement;
Object.defineProperties(video, {
offsetLeft: { configurable: true, get: () => 0 },
offsetTop: { configurable: true, get: () => 0 },
offsetWidth: { configurable: true, get: () => 1080 },
offsetHeight: { configurable: true, get: () => 1920 },
});
video.getBoundingClientRect = () =>
({
x: 0,
y: 0,
left: 0,
top: 0,
right: 1080,
bottom: 1920,
width: 1080,
height: 1920,
toJSON: () => ({}),
}) as DOMRect;
const styles = new Map<Element, StyleLike>();
styles.set(host, hostStyle);
styles.set(pipFrame, {});
styles.set(video, {
opacity: "1",
objectFit: "cover",
objectPosition: "center",
zIndex: "1",
...options.videoStyle,
});
Object.defineProperty(window, "getComputedStyle", {
configurable: true,
value: (el: Element) => {
const declared = styles.get(el) ?? {};
return {
display: declared.display ?? "block",
visibility: declared.visibility ?? "visible",
opacity: declared.opacity ?? "1",
objectFit: declared.objectFit ?? "fill",
objectPosition: declared.objectPosition ?? "50% 50%",
zIndex: declared.zIndex ?? "auto",
getPropertyValue: (prop: string) => {
const camel = prop.replace(/-([a-z])/g, (_, c: string) =>
c.toUpperCase(),
) as keyof StyleLike;
return declared[camel] ?? "";
},
};
},
});
return { window, document, video, host, pipFrame };
}
function withGlobals<T extends { window: Window; document: Document; video: HTMLVideoElement }>(
setup: T,
): { teardown: () => void; setup: T } {
const globals = globalThis as unknown as { window?: Window; document?: Document };
const previousWindow = globals.window;
const previousDocument = globals.document;
globals.window = setup.window;
globals.document = setup.document;
return {
setup,
teardown: () => {
globals.window = previousWindow;
globals.document = previousDocument;
},
};
}
function passthroughPage(): Page {
return {
evaluate: async (fn: (...args: unknown[]) => unknown, ...args: unknown[]) =>
// The implementation is built to run inside the page sandbox via
// `page.evaluate`, but linkedom gives us a DOM compatible enough to
// execute the function body directly in Node.
Promise.resolve((fn as (...a: unknown[]) => unknown)(...args)),
} as unknown as Page;
}
function installDomMaskGlobals(setup: { window: Window; document: Document }): () => void {
const globals = globalThis as unknown as {
window?: Window;
document?: Document;
HTMLElement?: typeof HTMLElement;
CSS?: typeof CSS;
};
const previousWindow = globals.window;
const previousDocument = globals.document;
const previousHTMLElement = globals.HTMLElement;
const previousCSS = globals.CSS;
globals.window = setup.window;
globals.document = setup.document;
globals.HTMLElement = setup.window.HTMLElement;
globals.CSS = { escape: (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "\\$&") } as CSS;
return () => {
globals.window = previousWindow;
globals.document = previousDocument;
globals.HTMLElement = previousHTMLElement;
globals.CSS = previousCSS;
};
}
it("skips replacement-frame creation when the video's host has visibility:hidden", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
// No replacement <img> should be injected next to the video — the host is
// currently hidden, so painting a frame over it would bleed onto whichever
// sibling host is actually visible on this seek.
const sibling = setup.video.nextElementSibling as HTMLElement | null;
expect(sibling).toBeNull();
});
it("skips replacement-frame creation when the video's host has display:none", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ display: "none" }));
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
const sibling = setup.video.nextElementSibling as HTMLElement | null;
expect(sibling).toBeNull();
});
it("hides an existing replacement <img> when the host becomes visibility:hidden", async () => {
// First seed an existing __render_frame__ <img> next to the video (the
// state the page is in after a previous seek when the host was visible).
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.visibility = "visible";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
expect(seededImg.style.visibility).toBe("hidden");
});
it("syncVideoFrameVisibility hides the replacement <img> for ancestor-hidden actives", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.visibility = "visible";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
try {
// "pip" IS in the active set (per the raw time-window check) but the
// host is hidden. sync must keep the <img> hidden, not flip it to
// `visibility: visible`.
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
} finally {
teardown();
}
expect(seededImg.style.visibility).toBe("hidden");
});
it("still injects when a plain [data-start] host is visibility:hidden (CSS-escapable)", async () => {
// Regression guard for the style-9-prod symptom: a regular
// `[data-start]` container whose GSAP timeline is shorter than its
// authored `data-duration` ends up `visibility: hidden` past the
// timeline end. The replacement <img>'s explicit `visibility: visible`
// correctly overrides that per CSS spec, so the injector must NOT
// short-circuit — it would otherwise drop the final-state frame and
// produce blank tail frames.
const { teardown, setup } = withGlobals(
setupHostHiddenScenario({ visibility: "hidden" }, { hostAttribute: "data-start" }),
);
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
const sibling = setup.video.nextElementSibling as HTMLElement | null;
expect(sibling).not.toBeNull();
expect(sibling?.classList.contains("__render_frame__")).toBe(true);
expect(sibling?.style.visibility).toBe("visible");
});
it("does not copy color-grading source suppression opacity to the injected frame", async () => {
const { teardown, setup } = withGlobals(
setupHostHiddenScenario({}, { videoStyle: { opacity: "0" } }),
);
setup.video.setAttribute("data-hf-color-grading-source-hidden", "true");
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
const sibling = setup.video.nextElementSibling as HTMLElement | null;
expect(sibling?.classList.contains("__render_frame__")).toBe(true);
expect(sibling?.style.opacity).toBe("1");
});
it("repairs stale injected-frame opacity while syncing color-graded active videos", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({}));
setup.video.setAttribute("data-hf-color-grading-source-hidden", "true");
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.opacity = "0";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
try {
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
} finally {
teardown();
}
expect(seededImg.style.opacity).toBe("1");
expect(seededImg.style.visibility).toBe("visible");
});
it("syncVideoFrameVisibility shows the replacement <img> when a plain [data-start] host is visibility:hidden", async () => {
const { teardown, setup } = withGlobals(
setupHostHiddenScenario({ visibility: "hidden" }, { hostAttribute: "data-start" }),
);
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.visibility = "hidden";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
try {
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
} finally {
teardown();
}
// The host's `visibility: hidden` is escapable; sync must flip the
// <img> to `visibility: visible` so it overrides the ancestor.
expect(seededImg.style.visibility).toBe("visible");
});
// Regression for the layered/HDR mask path: `applyDomLayerMask` writes an
// `!important` stylesheet rule `#${showId} *{visibility:visible !important}`
// which, if a sub-comp host id appears in the show set, would revive a
// plain (non-important) inline `visibility: hidden` on a descendant
// `__render_frame__` — the cascade rule is "important stylesheet author
// beats non-important inline author". To stay safe regardless of which
// layer ends up in `show`, the ancestor-hidden hide must be written with
// `!important` so inline `!important` beats stylesheet `!important`.
//
// linkedom strips `!important` from `cssText`/`getPropertyPriority`, so we
// pin the contract on the API call site instead: a `setProperty(name,
// value, "important")` invocation on the live `<img>`'s style.
it("injectVideoFramesBatch hides a stale <img> with !important so the layer mask cannot revive it", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.visibility = "visible";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
const setPropertySpy = vi.spyOn(seededImg.style, "setProperty");
try {
await injectVideoFramesBatch(passthroughPage(), [
{
videoId: "pip",
dataUri:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=",
},
]);
} finally {
teardown();
}
expect(seededImg.style.visibility).toBe("hidden");
expect(setPropertySpy).toHaveBeenCalledWith("visibility", "hidden", "important");
});
it("syncVideoFrameVisibility hides an existing <img> with !important so the layer mask cannot revive it", async () => {
const { teardown, setup } = withGlobals(setupHostHiddenScenario({ visibility: "hidden" }));
const seededImg = setup.document.createElement("img");
seededImg.classList.add("__render_frame__");
seededImg.style.visibility = "visible";
setup.video.parentNode?.insertBefore(seededImg, setup.video.nextSibling);
const setPropertySpy = vi.spyOn(seededImg.style, "setProperty");
try {
await syncVideoFrameVisibility(passthroughPage(), ["pip"]);
} finally {
teardown();
}
expect(seededImg.style.visibility).toBe("hidden");
expect(setPropertySpy).toHaveBeenCalledWith("visibility", "hidden", "important");
});
it("applyDomLayerMask does not revive hidden idless timed descendants of a shown layer", async () => {
const { window, document } = parseHTML(
`<html><head></head><body>
<div id="scene" data-start="0" data-duration="6">
<div class="label" data-start="4.5" data-duration="1.5">late label</div>
</div>
</body></html>`,
);
const scene = document.getElementById("scene") as HTMLElement;
const label = document.querySelector(".label") as HTMLElement;
label.style.visibility = "hidden";
Object.defineProperty(window, "getComputedStyle", {
configurable: true,
value: (el: Element) => ({
display: (el as HTMLElement).style.display || "block",
visibility: (el as HTMLElement).style.visibility || "visible",
}),
});
const teardown = installDomMaskGlobals({ window, document });
try {
await applyDomLayerMask(passthroughPage(), ["scene"], []);
expect(scene.style.visibility || "").toBe("");
expect(label.style.visibility).toBe("hidden");
await removeDomLayerMask(passthroughPage(), []);
expect(label.style.visibility).toBe("hidden");
expect(label.hasAttribute("data-hf-dom-layer-mask-hidden")).toBe(false);
} finally {
teardown();
}
});
it("removeDomLayerMask keeps hidden timed descendants hidden when they are also extraHideIds", async () => {
const { window, document } = parseHTML(
`<html><head></head><body>
<div id="scene" data-start="0" data-duration="6">
<div id="caption" data-start="4.5" data-duration="1.5">late label</div>
</div>
</body></html>`,
);
const caption = document.getElementById("caption") as HTMLElement;
caption.style.visibility = "hidden";
Object.defineProperty(window, "getComputedStyle", {
configurable: true,
value: (el: Element) => ({
display: (el as HTMLElement).style.display || "block",
visibility: (el as HTMLElement).style.visibility || "visible",
}),
});
const teardown = installDomMaskGlobals({ window, document });
try {
await applyDomLayerMask(passthroughPage(), ["scene"], ["caption"]);
await removeDomLayerMask(passthroughPage(), ["caption"]);
expect(caption.style.visibility).toBe("hidden");
expect(caption.hasAttribute("data-hf-dom-layer-mask-hidden")).toBe(false);
} finally {
teardown();
}
});
it("removeDomLayerMask restores extraHideIds and render frames to previous visibility", async () => {
const { window, document } = parseHTML(
`<html><head></head><body>
<div id="visible-caption" data-start="0" data-duration="1">current</div>
<video id="clip" data-start="0" data-duration="1"></video>
<img id="__render_frame_clip__" />
</body></html>`,
);
const visibleCaption = document.getElementById("visible-caption") as HTMLElement;
const clip = document.getElementById("clip") as HTMLElement;
const renderFrame = document.getElementById("__render_frame_clip__") as HTMLElement;
visibleCaption.style.visibility = "visible";
clip.style.visibility = "hidden";
renderFrame.style.setProperty("visibility", "hidden", "important");
Object.defineProperty(window, "getComputedStyle", {
configurable: true,
value: (el: Element) => ({
display: (el as HTMLElement).style.display || "block",
visibility: (el as HTMLElement).style.visibility || "visible",
}),
});
const teardown = installDomMaskGlobals({ window, document });
try {
await applyDomLayerMask(passthroughPage(), [], ["visible-caption", "clip"]);
await removeDomLayerMask(passthroughPage(), ["visible-caption", "clip"]);
expect(visibleCaption.style.visibility).toBe("visible");
expect(clip.style.visibility).toBe("hidden");
expect(renderFrame.style.visibility).toBe("hidden");
} finally {
teardown();
}
});
it("applyDomLayerMask carries color grading canvases with their media element", async () => {
const { window, document } = parseHTML(
'<html><head></head><body><div id="root"><video id="pip"></video><canvas id="__hf_color_grading_pip"></canvas></div></body></html>',
);
const teardown = installDomMaskGlobals({ window, document });
try {
await applyDomLayerMask(passthroughPage(), ["pip"], []);
expect(document.getElementById(DOM_LAYER_MASK_STYLE_ID)?.textContent).toContain(
"#__hf_color_grading_pip",
);
await applyDomLayerMask(passthroughPage(), ["root"], ["pip"]);
const canvas = document.getElementById("__hf_color_grading_pip") as HTMLCanvasElement;
expect(canvas.style.visibility).toBe("hidden");
await removeDomLayerMask(passthroughPage(), ["pip"]);
expect(canvas.style.getPropertyValue("visibility") || "").toBe("");
} finally {
teardown();
}
});
});
describe("ensureRenderFrameSiblings", () => {
function passthroughPage(): Page {
return {
evaluate: async (fn: (...args: unknown[]) => unknown, ...args: unknown[]) =>
Promise.resolve((fn as (...a: unknown[]) => unknown)(...args)),
} as unknown as Page;
}
function withGlobalDom(setup: { window: Window; document: Document }): { teardown: () => void } {
const globals = globalThis as unknown as { window?: Window; document?: Document };
const previousWindow = globals.window;
const previousDocument = globals.document;
globals.window = setup.window;
globals.document = setup.document;
return {
teardown: () => {
globals.window = previousWindow;
globals.document = previousDocument;
},
};
}
it("creates a hidden __render_frame__ sibling for every video[data-start]", async () => {
const { window, document } = parseHTML(
`<html><body>
<div id="root">
<video id="v1" data-start="0" data-duration="2"></video>
<video id="v2" data-start="2" data-duration="2"></video>
</div>
</body></html>`,
);
const { teardown } = withGlobalDom({ window, document });
try {
await ensureRenderFrameSiblings(passthroughPage());
} finally {
teardown();
}
for (const id of ["v1", "v2"]) {
const video = document.getElementById(id) as HTMLVideoElement;
const sibling = video.nextElementSibling as HTMLElement | null;
expect(sibling).not.toBeNull();
expect(sibling?.tagName.toLowerCase()).toBe("img");
expect(sibling?.classList.contains("__render_frame__")).toBe(true);
expect(sibling?.id).toBe(`__render_frame_${id}__`);
expect(sibling?.style.visibility).toBe("hidden");
}
});
it("skips videos that already have a __render_frame__ sibling", async () => {
const { window, document } = parseHTML(
`<html><body>
<div id="root">
<video id="clip" data-start="0" data-duration="2"></video>
<img id="__render_frame_clip__" class="__render_frame__">
</div>
</body></html>`,
);
const preExisting = document.getElementById("__render_frame_clip__") as HTMLImageElement;
const { teardown } = withGlobalDom({ window, document });
try {
await ensureRenderFrameSiblings(passthroughPage());
} finally {
teardown();
}
const video = document.getElementById("clip") as HTMLVideoElement;
expect(video.nextElementSibling).toBe(preExisting);
expect(document.querySelectorAll(".__render_frame__").length).toBe(1);
});
it("does not touch <video> elements without data-start", async () => {
const { window, document } = parseHTML(
`<html><body><div id="root"><video id="raw"></video></div></body></html>`,
);
const { teardown } = withGlobalDom({ window, document });
try {
await ensureRenderFrameSiblings(passthroughPage());
} finally {
teardown();
}
const video = document.getElementById("raw") as HTMLVideoElement;
expect(video.nextElementSibling).toBeNull();
});
});