fix(runtime): respect hidden ancestor clips in Studio preview (#1387) (#1395)

* fix(runtime): respect hidden ancestor clips in Studio preview (#1387)

Studio-stamped GSAP tween targets inside timed clips were getting
visibility:visible for the full composition, overriding hidden parent
panels. Skip stamping descendants of authored clips and suppress
visibility on children when an ancestor timed clip is hidden.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(runtime): scope ancestor visibility walk to Studio iframe only

Address review feedback: the hierarchical visibility guard now runs only
when window.parent !== window, matching the Studio-only stamping fix.
Render mode keeps prior per-element visibility semantics. Adds a render-mode
regression test and documents the null rootComp case in findTimedClipAncestor.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Manu Pareek
2026-06-12 20:28:31 -04:00
committed by GitHub
co-authored by Cursor
parent 8eac7e1cda
commit 7fa3696101
2 changed files with 296 additions and 39 deletions
+217
View File
@@ -71,6 +71,22 @@ function createManualRaf() {
};
}
function withStudioIframe(run: () => void): void {
const originalParent = window.parent;
Object.defineProperty(window, "parent", {
configurable: true,
value: {},
});
try {
run();
} finally {
Object.defineProperty(window, "parent", {
configurable: true,
value: originalParent,
});
}
}
describe("initSandboxRuntimeModular", () => {
const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame;
@@ -413,6 +429,207 @@ describe("initSandboxRuntimeModular", () => {
expect(sceneB.style.visibility).toBe("visible");
});
it("hides GSAP tween targets inside a hidden timed clip (issue #1387)", () => {
withStudioIframe(() => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "8");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const captionOne = document.createElement("div");
captionOne.id = "t01";
captionOne.setAttribute("data-start", "0");
captionOne.setAttribute("data-duration", "4");
root.appendChild(captionOne);
const lineOne = document.createElement("div");
lineOne.className = "line";
// Studio stamps full-duration pseudo-clips on GSAP tween targets.
lineOne.setAttribute("data-start", "0");
lineOne.setAttribute("data-duration", "8");
captionOne.appendChild(lineOne);
const captionTwo = document.createElement("div");
captionTwo.id = "t02";
captionTwo.setAttribute("data-start", "4");
captionTwo.setAttribute("data-duration", "4");
root.appendChild(captionTwo);
const lineTwo = document.createElement("div");
lineTwo.className = "line";
lineTwo.setAttribute("data-start", "0");
lineTwo.setAttribute("data-duration", "8");
captionTwo.appendChild(lineTwo);
window.__timelines = {
main: createMockTimeline(8),
};
initSandboxRuntimeModular();
const player = window.__player;
expect(player).toBeDefined();
player?.seek(1);
expect(captionOne.style.visibility).toBe("visible");
expect(lineOne.style.visibility).toBe("visible");
expect(captionTwo.style.visibility).toBe("hidden");
expect(lineTwo.style.visibility).toBe("hidden");
player?.seek(5);
expect(captionOne.style.visibility).toBe("hidden");
expect(lineOne.style.visibility).toBe("hidden");
expect(captionTwo.style.visibility).toBe("visible");
expect(lineTwo.style.visibility).toBe("visible");
});
});
it("does not suppress descendant visibility in render mode (top-level page)", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "8");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const panel = document.createElement("div");
panel.id = "panel";
panel.setAttribute("data-start", "0");
panel.setAttribute("data-duration", "2");
root.appendChild(panel);
const headline = document.createElement("h1");
headline.className = "headline";
// Authored child window outlives the parent clip — render keeps legacy behavior.
headline.setAttribute("data-start", "0");
headline.setAttribute("data-duration", "8");
panel.appendChild(headline);
window.__timelines = {
main: createMockTimeline(8),
};
initSandboxRuntimeModular();
const player = window.__player;
expect(player).toBeDefined();
player?.seek(3);
expect(panel.style.visibility).toBe("hidden");
expect(headline.style.visibility).toBe("visible");
});
it("does not stamp Studio timing on GSAP targets inside authored timed clips", () => {
withStudioIframe(() => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "8");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const caption = document.createElement("div");
caption.id = "t01";
caption.setAttribute("data-start", "0");
caption.setAttribute("data-duration", "4");
root.appendChild(caption);
const line = document.createElement("div");
line.className = "line";
caption.appendChild(line);
const tweenTarget = {
targets: () => [line],
};
const timeline = createMockTimeline(8) as RuntimeTimelineLike & {
getChildren: (nested?: boolean) => Array<{ targets: () => Element[] }>;
};
timeline.getChildren = () => [tweenTarget];
window.__timelines = {
main: timeline,
};
initSandboxRuntimeModular();
expect(line.hasAttribute("data-start")).toBe(false);
expect(line.hasAttribute("data-duration")).toBe(false);
});
});
it("hides tween targets inside inactive multi-panel beats (niemmo panel stack)", () => {
withStudioIframe(() => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "niemmo-launch-50");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-duration", "50");
root.setAttribute("data-width", "1280");
root.setAttribute("data-height", "720");
document.body.appendChild(root);
const panelA = document.createElement("div");
panelA.className = "panel clip";
panelA.setAttribute("data-composition-id", "cold-open");
panelA.setAttribute("data-start", "0");
panelA.setAttribute("data-duration", "2");
root.appendChild(panelA);
const headlineA = document.createElement("h1");
headlineA.className = "co-headline";
headlineA.setAttribute("data-start", "0");
headlineA.setAttribute("data-duration", "50");
panelA.appendChild(headlineA);
const panelB = document.createElement("div");
panelB.className = "panel clip";
panelB.setAttribute("data-composition-id", "problem-dev-beat");
panelB.setAttribute("data-start", "2");
panelB.setAttribute("data-duration", "2.5");
root.appendChild(panelB);
const headlineB = document.createElement("h1");
headlineB.className = "pb-headline";
headlineB.setAttribute("data-start", "0");
headlineB.setAttribute("data-duration", "50");
panelB.appendChild(headlineB);
window.__timelines = {
"niemmo-launch-50": createMockTimeline(50),
};
initSandboxRuntimeModular();
const player = window.__player;
expect(player).toBeDefined();
player?.seek(1);
expect(panelA.style.visibility).toBe("visible");
expect(headlineA.style.visibility).toBe("visible");
expect(panelB.style.visibility).toBe("hidden");
expect(headlineB.style.visibility).toBe("hidden");
player?.seek(3);
expect(panelA.style.visibility).toBe("hidden");
expect(headlineA.style.visibility).toBe("hidden");
expect(panelB.style.visibility).toBe("visible");
expect(headlineB.style.visibility).toBe("visible");
});
});
it("clamps nested media to the authored host window on seek", () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");