diff --git a/packages/engine/src/services/screenshotService.test.ts b/packages/engine/src/services/screenshotService.test.ts
index cb896c68d2..348a0221b0 100644
--- a/packages/engine/src/services/screenshotService.test.ts
+++ b/packages/engine/src/services/screenshotService.test.ts
@@ -363,6 +363,29 @@ describe("video-frame injection respects ancestor visibility", () => {
} 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 {
@@ -569,20 +592,7 @@ describe("video-frame injection respects ancestor visibility", () => {
}),
});
- 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 = window;
- globals.document = document;
- globals.HTMLElement = window.HTMLElement;
- globals.CSS = { escape: (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "\\$&") } as CSS;
+ const teardown = installDomMaskGlobals({ window, document });
try {
await applyDomLayerMask(passthroughPage(), ["scene"], []);
expect(scene.style.visibility || "").toBe("");
@@ -592,10 +602,74 @@ describe("video-frame injection respects ancestor visibility", () => {
expect(label.style.visibility).toBe("hidden");
expect(label.hasAttribute("data-hf-dom-layer-mask-hidden")).toBe(false);
} finally {
- globals.window = previousWindow;
- globals.document = previousDocument;
- globals.HTMLElement = previousHTMLElement;
- globals.CSS = previousCSS;
+ teardown();
+ }
+ });
+
+ it("removeDomLayerMask keeps hidden timed descendants hidden when they are also extraHideIds", async () => {
+ const { window, document } = parseHTML(
+ `
+
+ `,
+ );
+ 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(
+ `
+ current
+
+
+ `,
+ );
+ 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();
}
});
});
diff --git a/packages/engine/src/services/screenshotService.ts b/packages/engine/src/services/screenshotService.ts
index 1608b9e123..0a197005cd 100644
--- a/packages/engine/src/services/screenshotService.ts
+++ b/packages/engine/src/services/screenshotService.ts
@@ -285,12 +285,12 @@ const DOM_LAYER_MASK_PREV_PRIORITY_ATTR = "data-hf-dom-layer-mask-prev-priority"
* layer elements remain visible even though intermediate parents are
* hidden by the mass-hide rule.
* 2. Inline-hide each `extraHideId` (and its `__render_frame_*` sibling) with
- * `visibility: hidden !important`. Inline `!important` beats stylesheet
- * `!important`, so this overrides the show rule for elements that fall
- * under a show selector but should NOT paint — typically other-layer
- * elements that are descendants of a container layer (for example HDR
- * videos and other-layer SDR videos are descendants of `#root` when we
- * capture the root DOM layer).
+ * `visibility: hidden !important`, while first recording its previous
+ * inline visibility. Inline `!important` beats stylesheet `!important`,
+ * so this overrides the show rule for elements that fall under a show
+ * selector but should NOT paint — typically other-layer elements that are
+ * descendants of a container layer (for example HDR videos and other-layer
+ * SDR videos are descendants of `#root` when we capture the root DOM layer).
* 3. Inline-hide timed descendants of shown elements that were hidden before
* the mask was installed. This covers idless child clips and same-layer
* descendants that the `extraHideIds` id list cannot represent.
@@ -332,7 +332,7 @@ export async function applyDomLayerMask(
const existing = document.getElementById(args.styleId);
if (existing) existing.remove();
- const restoreMaskedTimedDescendants = () => {
+ const restoreMaskedElements = () => {
const masked = document.querySelectorAll(`[${args.hiddenAttr}="1"]`);
for (const node of masked) {
if (!(node instanceof HTMLElement)) continue;
@@ -348,7 +348,29 @@ export async function applyDomLayerMask(
node.removeAttribute(args.prevPriorityAttr);
}
};
- restoreMaskedTimedDescendants();
+ restoreMaskedElements();
+
+ const rememberAndHideElement = (el: HTMLElement) => {
+ if (el.getAttribute(args.hiddenAttr) !== "1") {
+ const prevVisibility = el.style.getPropertyValue("visibility");
+ const prevPriority =
+ typeof el.style.getPropertyPriority === "function"
+ ? el.style.getPropertyPriority("visibility")
+ : "";
+ if (prevVisibility) {
+ el.setAttribute(args.prevVisibilityAttr, prevVisibility);
+ } else {
+ el.removeAttribute(args.prevVisibilityAttr);
+ }
+ if (prevPriority) {
+ el.setAttribute(args.prevPriorityAttr, prevPriority);
+ } else {
+ el.removeAttribute(args.prevPriorityAttr);
+ }
+ el.setAttribute(args.hiddenAttr, "1");
+ }
+ el.style.setProperty("visibility", "hidden", "important");
+ };
const hiddenTimedDescendants: HTMLElement[] = [];
const rememberHiddenTimedDescendants = (root: Element) => {
@@ -382,34 +404,17 @@ export async function applyDomLayerMask(
document.head.appendChild(style);
for (const el of hiddenTimedDescendants) {
- if (el.getAttribute(args.hiddenAttr) === "1") continue;
- const prevVisibility = el.style.getPropertyValue("visibility");
- const prevPriority =
- typeof el.style.getPropertyPriority === "function"
- ? el.style.getPropertyPriority("visibility")
- : "";
- if (prevVisibility) {
- el.setAttribute(args.prevVisibilityAttr, prevVisibility);
- } else {
- el.removeAttribute(args.prevVisibilityAttr);
- }
- if (prevPriority) {
- el.setAttribute(args.prevPriorityAttr, prevPriority);
- } else {
- el.removeAttribute(args.prevPriorityAttr);
- }
- el.setAttribute(args.hiddenAttr, "1");
- el.style.setProperty("visibility", "hidden", "important");
+ rememberAndHideElement(el);
}
for (const id of args.hide) {
const el = document.getElementById(id);
if (el) {
- el.style.setProperty("visibility", "hidden", "important");
+ rememberAndHideElement(el);
}
const img = document.getElementById(`__render_frame_${id}__`);
if (img) {
- img.style.setProperty("visibility", "hidden", "important");
+ rememberAndHideElement(img);
}
}
},
@@ -427,8 +432,9 @@ export async function applyDomLayerMask(
/**
* Tear down the mask installed by applyDomLayerMask.
*
- * Removes the mask stylesheet and clears the inline `visibility` properties
- * set on `extraHideIds` (and their `__render_frame_*` siblings).
+ * Removes the mask stylesheet and restores the inline `visibility` values
+ * temporarily overwritten for hidden timed descendants, `extraHideIds`, and
+ * their `__render_frame_*` siblings.
*
* IMPORTANT: We do NOT strip inline `opacity` here. applyDomLayerMask only
* ever sets `visibility` (never `opacity`), so any inline opacity present on
@@ -438,10 +444,9 @@ export async function applyDomLayerMask(
* we strip opacity here and then seek to the same time for the next layer,
* GSAP won't put it back and the wrapper will render fully opaque.
*/
-export async function removeDomLayerMask(page: Page, extraHideIds: string[]): Promise {
+export async function removeDomLayerMask(page: Page, _extraHideIds: string[]): Promise {
await page.evaluate(
(args: {
- hide: string[];
styleId: string;
hiddenAttr: string;
prevVisibilityAttr: string;
@@ -463,17 +468,8 @@ export async function removeDomLayerMask(page: Page, extraHideIds: string[]): Pr
node.removeAttribute(args.prevVisibilityAttr);
node.removeAttribute(args.prevPriorityAttr);
}
- for (const id of args.hide) {
- const el = document.getElementById(id);
- if (el) {
- el.style.removeProperty("visibility");
- }
- const img = document.getElementById(`__render_frame_${id}__`);
- if (img) img.style.removeProperty("visibility");
- }
},
{
- hide: extraHideIds,
styleId: DOM_LAYER_MASK_STYLE_ID,
hiddenAttr: DOM_LAYER_MASK_HIDDEN_ATTR,
prevVisibilityAttr: DOM_LAYER_MASK_PREV_VISIBILITY_ATTR,