fix(runtime): keep stamped flow children in document flow in preview (#1702)

Studio/preview stamps `data-start` onto ID'd and GSAP-targeted flow
children (eg. a <header>/<footer> in a flex column) so the design panel
can discover them. applyClipLayout then force-absolutized those stamped
elements as if they were authored overlay clips, collapsing the layout:
the footer shrink-wrapped and its `justify-content: space-between`
clustered into the top-left, while the rendered video — which never
stamps (production renders run as the top-level page, not in an iframe) —
stayed correct.

Mark runtime-stamped clips with `data-hf-autostamped` and skip them in
applyClipLayout so they remain in document flow. The preview now matches
the rendered video (true WYSIWYG). Authored overlay clips are unchanged,
so the golden regression suite is unaffected.
This commit is contained in:
Miguel Ángel
2026-06-24 20:12:02 -04:00
committed by GitHub
parent 1c389983de
commit 9bf1e1c298
2 changed files with 106 additions and 0 deletions
+91
View File
@@ -1279,4 +1279,95 @@ describe("initSandboxRuntimeModular", () => {
raf.step(16);
expect(seekTimes.length).toBeGreaterThan(beforeResume);
});
// applyClipLayout force-absolutizes authored root-level timed clips so they
// stack as overlays. But in Studio/preview the runtime also stamps `data-start`
// onto ID'd / GSAP-targeted *flow* children (a <header>/<footer> in a column)
// so the design panel can discover them — those must NOT be force-absolutized,
// or the layout collapses (footer shrink-wraps, `space-between` clusters). The
// marker `data-hf-autostamped` distinguishes them; these tests pin both halves.
describe("applyClipLayout: runtime-stamped clips stay in document flow", () => {
const makeRoot = () => {
const root = document.createElement("div");
root.setAttribute("data-composition-id", "main");
root.setAttribute("data-root", "true");
root.setAttribute("data-start", "0");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
return root;
};
// jsdom does no layout, so a static clip can report computed top "auto" or
// "" inconsistently. Pin the values the anchor gate keys on so the assertion
// reflects the real-browser path deterministically.
const overrideComputed = (
target: HTMLElement,
overrides: Partial<Record<"position" | "top" | "left" | "bottom" | "right", string>>,
) => {
const real = window.getComputedStyle.bind(window);
vi.spyOn(window, "getComputedStyle").mockImplementation(((
el: Element,
pseudo?: string | null,
) => {
const style = real(el as Element, pseudo ?? undefined);
if (el !== target) return style;
return new Proxy(style, {
get(t, prop) {
if (typeof prop === "string" && prop in overrides) {
return overrides[prop as keyof typeof overrides];
}
const value = Reflect.get(t, prop);
return typeof value === "function" ? value.bind(t) : value;
},
}) as CSSStyleDeclaration;
}) as typeof window.getComputedStyle);
};
it("force-absolutizes an authored data-start clip (baseline behavior preserved)", () => {
const root = makeRoot();
const clip = document.createElement("div");
clip.setAttribute("data-start", "0"); // authored clip, no autostamp marker
root.appendChild(clip);
overrideComputed(clip, {
position: "static",
top: "auto",
left: "auto",
bottom: "auto",
right: "auto",
});
window.__timelines = { main: createMockTimeline(10) };
initSandboxRuntimeModular();
expect(clip.style.position).toBe("absolute");
expect(clip.style.top).toBe("0px");
expect(clip.style.left).toBe("0px");
});
it("leaves a runtime-stamped flow child untouched so the layout is preserved", () => {
const root = makeRoot();
const footer = document.createElement("footer");
footer.setAttribute("data-start", "0");
footer.setAttribute("data-hf-autostamped", "1"); // stamped flow child, not an overlay clip
root.appendChild(footer);
overrideComputed(footer, {
position: "static",
top: "auto",
left: "auto",
bottom: "auto",
right: "auto",
});
window.__timelines = { main: createMockTimeline(10) };
initSandboxRuntimeModular();
// Skipped entirely: stays in document flow (no forced absolute, no anchor),
// so a flex-column footer keeps full width and `space-between` spreads — the
// preview then matches the rendered video, which never stamps.
expect(footer.style.position).toBe("");
expect(footer.style.top).toBe("");
expect(footer.style.left).toBe("");
});
});
});
+15
View File
@@ -315,6 +315,15 @@ export function initSandboxRuntimeModular(): void {
const tag = el.tagName.toLowerCase();
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") continue;
if (!el.hasAttribute("data-start")) continue;
// Runtime-stamped clips are NOT authored overlay clips. In Studio/preview
// the runtime stamps `data-start` onto ID'd or GSAP-targeted flow children
// (a <header>/<footer> in a flex column) so the design panel can discover
// them — see the stamping pass in bindCapturedTimeline. Forcing those out
// of document flow collapses the layout: the footer shrink-wraps and its
// `justify-content: space-between` clusters in the top-left. Leave them in
// flow so the preview matches the rendered video, which never stamps
// (production renders run as the top-level page, not in an iframe).
if (el.hasAttribute("data-hf-autostamped")) continue;
const hasLegacyAnchoredDefaults =
(el.style.top === "0px" || el.style.top === "0") &&
(el.style.left === "0px" || el.style.left === "0") &&
@@ -1126,6 +1135,9 @@ export function initSandboxRuntimeModular(): void {
seen.add(target);
target.setAttribute("data-start", "0");
target.setAttribute("data-duration", dur);
// Mark as runtime-stamped so applyClipLayout leaves it in document
// flow instead of treating it as an authored overlay clip.
target.setAttribute("data-hf-autostamped", "1");
}
}
} catch {
@@ -1147,6 +1159,9 @@ export function initSandboxRuntimeModular(): void {
seen.add(el);
el.setAttribute("data-start", "0");
el.setAttribute("data-duration", dur);
// Mark as runtime-stamped so applyClipLayout leaves it in document
// flow instead of treating it as an authored overlay clip.
el.setAttribute("data-hf-autostamped", "1");
}
}
}