Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions packages/core/src/runtime/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3631,6 +3631,100 @@ describe("initSandboxRuntimeModular", () => {
expect(video.style.visibility).toBe("hidden");
});

it("keeps a hidden clip's in-flow reading when another timed clip is added", async () => {
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", "10");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const clips = [0, 1, 2].map((i) => {
const clip = document.createElement("div");
clip.id = `clip-${i}`;
clip.style.position = "absolute";
clip.setAttribute("data-start", "5");
clip.setAttribute("data-duration", "2");
root.appendChild(clip);
return clip;
});
window.__timelines = { main: createMockTimeline(10) };
initSandboxRuntimeModular();
const player = window.__player;
player?.seek(0);
const added = document.createElement("div");
added.id = "added";
added.setAttribute("data-start", "8");
added.setAttribute("data-duration", "1");
root.appendChild(added);
await vi.waitFor(() => expect(JSON.stringify(window.__clipManifest)).toContain('"added"'));
const reads = vi.spyOn(window, "getComputedStyle");
player?.seek(1);
player?.seek(9);
const clipReads = reads.mock.calls.filter(([el]) => clips.includes(el as HTMLDivElement));
reads.mockRestore();
expect(clipReads).toHaveLength(0);
});

it("re-reads a clip's flow when its own inline position changes", () => {
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", "10");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const holder = document.createElement("div");
holder.style.position = "absolute";
root.appendChild(holder);
const clip = document.createElement("div");
clip.style.position = "absolute";
clip.setAttribute("data-start", "5");
clip.setAttribute("data-duration", "2");
holder.appendChild(clip);
window.__timelines = { main: createMockTimeline(10) };
initSandboxRuntimeModular();
const player = window.__player;
player?.seek(0);
expect(clip.style.display).not.toBe("none");
clip.style.position = "static";
player?.seek(1);
expect(clip.style.display).toBe("none");
});

it("re-reads a clip's flow when its classes change", () => {
const sheet = document.createElement("style");
sheet.textContent = ".placed { position: absolute }";
document.head.appendChild(sheet);
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", "10");
root.setAttribute("data-width", "1920");
root.setAttribute("data-height", "1080");
document.body.appendChild(root);
const holder = document.createElement("div");
holder.style.position = "absolute";
root.appendChild(holder);
const clip = document.createElement("div");
clip.className = "placed";
clip.setAttribute("data-start", "5");
clip.setAttribute("data-duration", "2");
holder.appendChild(clip);
window.__timelines = { main: createMockTimeline(10) };
initSandboxRuntimeModular();
const player = window.__player;
player?.seek(0);
expect(clip.style.display).not.toBe("none");
clip.className = "";
player?.seek(1);
expect(clip.style.display).toBe("none");
sheet.remove();
});

it("un-hides a later root-level video once active, even though it starts inactive and unstyled", () => {
// Root-level `[data-start]` children with no authored `position` start out
// `position: static` until `applyClipLayout` force-absolutizes them, so a
Expand Down
19 changes: 9 additions & 10 deletions packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2700,14 +2700,16 @@ export function initSandboxRuntimeModular(): void {
// In-flow clips must leave the flow when hidden — `visibility:hidden` reserves
// their layout box, so a split sibling would stack below the active half
// instead of overlapping it. Positioned clips keep `visibility:hidden` (cheaper,
// and avoids disturbing absolute media playback). Computed once per element.
let timedClipInFlow = new WeakMap<Element, boolean>();
// and avoids disturbing absolute media playback). Re-read only when the clip's
// own inline position or classes change: the read forces a style recalc.
const timedClipInFlow = new WeakMap<Element, { inputs: string; inFlow: boolean }>();
const isTimedClipInFlow = (el: Element): boolean => {
const inputs = `${(el as HTMLElement).style?.position ?? ""}|${el.getAttribute("class") ?? ""}`;
const cached = timedClipInFlow.get(el);
if (cached !== undefined) return cached;
if (cached?.inputs === inputs) return cached.inFlow;
const pos = window.getComputedStyle(el).position;
const inFlow = pos === "static" || pos === "relative" || pos === "sticky";
timedClipInFlow.set(el, inFlow);
timedClipInFlow.set(el, { inputs, inFlow });
return inFlow;
};

Expand All @@ -2726,13 +2728,10 @@ export function initSandboxRuntimeModular(): void {
return leaf;
};

// Both caches key on live DOM facts that change when the timed-element set
// changes: leaf status flips when a clip gains/loses a nested `[data-start]`
// descendant (sub-composition load/unload, studio insert/delete), and a swapped
// element can reuse an identity whose in-flow status differs. WeakMap has no
// `clear()`, so drop both maps wholesale — re-derived lazily on next access.
// Leaf status flips when a clip gains/loses a nested `[data-start]` descendant
// (sub-composition load/unload, studio insert/delete), so it is dropped
// wholesale when the timed-element set changes; WeakMap has no `clear()`.
const invalidateTimedClipCaches = () => {
timedClipInFlow = new WeakMap<Element, boolean>();
timedClipIsLeaf = new WeakMap<Element, boolean>();
};

Expand Down
Loading