Files
hyperframes/packages/core/src/compiler/compositionScoping.test.ts
T
Miguel Ángel 45999226a3 fix(studio): server-side DOM patching, render CSS scoping, and resilience
Root-cause fix for edits being wiped after refresh: the studio's
inspector edits were patched client-side via regex matching in
sourcePatcher.ts, which silently failed for many compositions ("Unable
to patch" toast). Replaced with a server-side patch-element API endpoint
using linkedom for proper DOM parsing via querySelector.

Also fixes the WYSIWYG render bug where sub-composition CSS was not
applied. The CSS scoping generated descendant selectors when both
attributes coexist on the same host element. Fixed to use compound
selectors for the authored root.

Edit persistence:
- New POST /file-mutations/patch-element endpoint using linkedom
- persistDomEditOperations calls server instead of client regex
- 15 tests covering all patch operation types

Render CSS scoping:
- Compound selector for authored root on host element
- Regression test: wysiwyg-subcomp-css (baseline pending Docker)
- 3 unit tests + 1 integration test

GSAP CDN fallback:
- Preview: error-handler catches gsap 404 and loads from CDN
- Producer: rewrites missing local gsap paths to CDN before compile

Studio resilience:
- Error boundary with recoverable UI
- Lazy mediabunny import prevents crash cascade
- Hash routing listens for hashchange events
- Sub-composition duration reads data-hf-authored-duration fallback
- Save debounce 600ms to requestAnimationFrame

Observability:
- PostHog telemetry for crashes, save failures, tab switches, playback,
  toolbar actions, navigation, and render starts
2026-05-20 17:07:31 -04:00

617 lines
19 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { parseHTML } from "linkedom";
import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping";
describe("composition scoping", () => {
it("scopes regular selectors while preserving global at-rules", () => {
const scoped = scopeCssToComposition(
`
@import url("https://example.com/font.css");
.title, .card:hover { opacity: 0; }
@media (min-width: 800px) {
.title { transform: translateY(30px); }
}
@keyframes rise {
from { opacity: 0; }
to { opacity: 1; }
}
[data-composition-id="scene"] .already { color: red; }
body { margin: 0; }
`,
"scene",
);
expect(scoped).toContain('@import url("https://example.com/font.css");');
expect(scoped).toContain(
'[data-composition-id="scene"] .title, [data-composition-id="scene"] .card:hover',
);
expect(scoped).toContain('[data-composition-id="scene"] .title { transform');
expect(scoped).toContain("@keyframes rise");
expect(scoped).toContain("from { opacity: 0; }");
expect(scoped).toContain('[data-composition-id="scene"] .already { color: red; }');
expect(scoped).toContain("body { margin: 0; }");
});
it("wraps classic scripts without render-loop requestAnimationFrame waits", () => {
const wrapped = wrapScopedCompositionScript("window.__ran = true;", "scene");
expect(wrapped).toContain('var __hfCompId = "scene";');
expect(wrapped).toContain("new Proxy(window.document");
expect(wrapped).toContain("new Proxy(__hfBaseGsap");
expect(wrapped).not.toContain("requestAnimationFrame");
});
it("normalizes root timing attributes when scoping selectors", () => {
const scoped = scopeCssToComposition(
'[data-composition-id="scene"][data-start="0"] .title { opacity: 0; }',
"scene",
);
expect(scoped).toContain('[data-composition-id="scene"] .title { opacity: 0; }');
expect(scoped).not.toContain('[data-start="0"]');
});
it("exposes a scoped __hyperframes.getVariables that reads __hfVariablesByComp[compId]", () => {
const { document } = parseHTML(`<div data-composition-id="card-1"></div>`);
const fakeWindow: Record<string, unknown> = {
document,
__timelines: {},
__hfVariablesByComp: {
"card-1": { title: "Pro", price: "$29" },
"card-2": { title: "Enterprise", price: "Custom" },
},
__hyperframes: {
getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
fitTextFontSize: () => undefined,
},
};
const wrapped = wrapScopedCompositionScript(
`window.__captured = __hyperframes.getVariables();`,
"card-1",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__captured).toEqual({ title: "Pro", price: "$29" });
});
it("scoped getVariables reads from the runtime composition id when it differs", () => {
const { document } = parseHTML(`<div data-composition-id="scene"></div>`);
const fakeWindow: Record<string, unknown> = {
document,
__timelines: {},
__hfVariablesByComp: {
scene: { title: "Wrong" },
scene__hf1: { title: "Right" },
},
__hyperframes: {
getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
fitTextFontSize: () => undefined,
},
};
const wrapped = wrapScopedCompositionScript(
`window.__captured = __hyperframes.getVariables();`,
"scene",
"[HyperFrames] composition script error:",
undefined,
"scene__hf1",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__captured).toEqual({ title: "Right" });
});
it("scoped getVariables returns {} when __hfVariablesByComp has no entry for the comp", () => {
const { document } = parseHTML(`<div data-composition-id="missing"></div>`);
const fakeWindow: Record<string, unknown> = {
document,
__timelines: {},
__hyperframes: {
getVariables: () => ({ title: "TOP-LEVEL-LEAK" }),
fitTextFontSize: () => undefined,
},
};
const wrapped = wrapScopedCompositionScript(
`window.__captured = __hyperframes.getVariables();`,
"missing",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__captured).toEqual({});
});
it("scoped getVariables returns a fresh object — mutations don't leak into the shared table", () => {
const { document } = parseHTML(`<div data-composition-id="card-1"></div>`);
const variablesByComp: Record<string, Record<string, unknown>> = {
"card-1": { title: "Pro" },
};
const fakeWindow: Record<string, unknown> = {
document,
__timelines: {},
__hfVariablesByComp: variablesByComp,
__hyperframes: {
getVariables: () => ({}),
fitTextFontSize: () => undefined,
},
};
const wrapped = wrapScopedCompositionScript(
`var v = __hyperframes.getVariables(); v.title = "MUTATED"; v.added = "extra";`,
"card-1",
);
new Function("window", wrapped)(fakeWindow);
expect(variablesByComp["card-1"]).toEqual({ title: "Pro" });
});
it("executes document and GSAP selectors inside the composition root", () => {
const { document } = parseHTML(`
<div data-composition-id="scene" data-start="intro"><h1 class="title">Scene</h1></div>
<div data-composition-id="other"><h1 class="title">Other</h1></div>
`);
const gsapTargets: string[][] = [];
const fakeWindow = {
document,
__selectedTitle: "",
__selectedRootTitle: "",
__timelines: {},
gsap: {
timeline: () => ({
to(targets: Element[]) {
gsapTargets.push(Array.from(targets).map((target) => target.textContent || ""));
return this;
},
}),
},
};
const wrapped = wrapScopedCompositionScript(
`
const tl = gsap.timeline({ paused: true });
tl.to('.title', { opacity: 1 });
tl.to('[data-composition-id="scene"][data-start="0"] .title', { opacity: 1 });
window.__selectedTitle = document.querySelector('.title')?.textContent || '';
window.__selectedRootTitle = document.querySelector('[data-composition-id="scene"][data-start="0"] .title')?.textContent || '';
window.__timelines.scene = tl;
`,
"scene",
);
new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
expect(fakeWindow.__selectedTitle).toBe("Scene");
expect(fakeWindow.__selectedRootTitle).toBe("Scene");
expect(gsapTargets).toEqual([["Scene"], ["Scene"]]);
});
it("scopes getElementById when duplicate IDs exist across composition roots", () => {
const { document } = parseHTML(`
<div data-composition-id="scene-a"><canvas id="gl-canvas"></canvas></div>
<div data-composition-id="scene-b"><canvas id="gl-canvas"></canvas></div>
`);
const fakeWindow = {
document,
__selectedComp: "",
__timelines: {},
};
const wrapped = wrapScopedCompositionScript(
`
window.__selectedComp =
document.getElementById("gl-canvas")
?.closest("[data-composition-id]")
?.getAttribute("data-composition-id") || "null";
`,
"scene-b",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__selectedComp).toBe("scene-b");
});
it("scopes getElementById for IDs that need CSS selector escaping", () => {
const { document } = parseHTML(`
<div data-composition-id="scene-a"><div id="clip:1"></div></div>
<div data-composition-id="scene-b"><div id="clip:1"></div></div>
`);
const fakeWindow = {
document,
__selectedComp: "",
__timelines: {},
};
const wrapped = wrapScopedCompositionScript(
`
window.__selectedComp =
document.getElementById("clip:1")
?.closest("[data-composition-id]")
?.getAttribute("data-composition-id") || "null";
`,
"scene-b",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__selectedComp).toBe("scene-b");
});
it("scopes authored root id lookups after the flattened root drops its literal id", () => {
const { document } = parseHTML(`
<div data-composition-id="scene">
<div data-hf-authored-id="scene-root">
<h1 class="title">Scene</h1>
</div>
</div>
`);
const fakeWindow = {
document,
__selectedTitle: "",
__timelines: {},
};
const wrapped = wrapScopedCompositionScript(
`
window.__selectedTitle =
document.getElementById("scene-root")
?.querySelector(".title")
?.textContent || "missing";
`,
"scene",
"[HyperFrames] composition script error:",
undefined,
"scene",
"scene-root",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__selectedTitle).toBe("Scene");
});
it("does not rewrite authored root hash text inside CSS attribute values", () => {
const scoped = scopeCssToComposition(
'a[href="#scene-root"] { color: red; }',
"scene",
undefined,
"scene-root",
);
expect(scoped).toContain('[data-composition-id="scene"] a[href="#scene-root"]');
expect(scoped).not.toContain('[href="[data-hf-authored-id=');
});
it("does not rewrite authored root hash text inside querySelector attribute values", () => {
const { document } = parseHTML(`
<div data-composition-id="scene">
<a class="jump" href="#scene-root">Jump</a>
<div data-hf-authored-id="scene-root"></div>
</div>
`);
const fakeWindow = {
document,
__selectedHref: "",
__timelines: {},
};
const wrapped = wrapScopedCompositionScript(
`
window.__selectedHref =
document.querySelector('a[href="#scene-root"]')
?.getAttribute("href") || "missing";
`,
"scene",
"[HyperFrames] composition script error:",
undefined,
"scene",
"scene-root",
);
new Function("window", wrapped)(fakeWindow);
expect(fakeWindow.__selectedHref).toBe("#scene-root");
});
it("normalizes gsap.utils.selector() selectors for authored root ids and root timing attrs", () => {
const { document } = parseHTML(`
<div data-composition-id="scene" data-start="0">
<div data-hf-authored-id="scene-root">
<h1 class="title">Scene</h1>
</div>
</div>
<div data-composition-id="other" data-start="0">
<div data-hf-authored-id="scene-root">
<h1 class="title">Other</h1>
</div>
</div>
`);
const fakeWindow = {
document,
__selectedRootCount: 0,
__selectedTimedCount: 0,
__selectedTitle: "",
__timelines: {},
gsap: {
utils: {},
},
};
const wrapped = wrapScopedCompositionScript(
`
const select = gsap.utils.selector(document.querySelector('[data-composition-id="scene"]'));
window.__selectedRootCount = select('#scene-root').length;
window.__selectedTimedCount = select('[data-composition-id="scene"][data-start="0"] .title').length;
window.__selectedTitle = select('#scene-root .title')[0]?.textContent || "missing";
`,
"scene",
"[HyperFrames] composition script error:",
undefined,
"scene",
"scene-root",
);
new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
expect(fakeWindow.__selectedRootCount).toBe(1);
expect(fakeWindow.__selectedTimedCount).toBe(1);
expect(fakeWindow.__selectedTitle).toBe("Scene");
});
it("reads scoped proxy accessors with the original target receiver", () => {
const root = {
contains(node: unknown) {
return node === root;
},
};
const body = { tagName: "BODY" };
const fakeDocument = {
querySelector(selector: string) {
return selector === '[data-composition-id="scene"]' ? root : null;
},
querySelectorAll() {
return [];
},
getElementById() {
return null;
},
get body() {
if (this !== fakeDocument) {
throw new TypeError("Illegal invocation");
}
return body;
},
};
const location = { href: "https://example.test/scene" };
const fakeUtils = {
get marker() {
if (this !== fakeUtils) {
throw new TypeError("Illegal invocation");
}
return "utils-ok";
},
};
const fakeGsap = {
utils: fakeUtils,
get version() {
if (this !== fakeGsap) {
throw new TypeError("Illegal invocation");
}
return "gsap-ok";
},
};
const fakeWindow = {
document: fakeDocument,
__bodyTag: "",
__href: "",
__windowSet: "",
__gsapVersion: "",
__utilsMarker: "",
__timelines: {},
gsap: fakeGsap,
get location() {
if (this !== fakeWindow) {
throw new TypeError("Illegal invocation");
}
return location;
},
set customValue(value: string) {
if (this !== fakeWindow) {
throw new TypeError("Illegal invocation");
}
this.__windowSet = value;
},
};
const wrapped = wrapScopedCompositionScript(
`
window.__bodyTag = document.body.tagName;
window.__href = window.location.href;
window.customValue = "window-set-ok";
window.__gsapVersion = gsap.version;
window.__utilsMarker = gsap.utils.marker;
`,
"scene",
);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
} finally {
errorSpy.mockRestore();
}
expect(fakeWindow.__bodyTag).toBe("BODY");
expect(fakeWindow.__href).toBe("https://example.test/scene");
expect(fakeWindow.__windowSet).toBe("window-set-ok");
expect(fakeWindow.__gsapVersion).toBe("gsap-ok");
expect(fakeWindow.__utilsMarker).toBe("utils-ok");
expect(errorSpy).not.toHaveBeenCalled();
});
it("reads remapped timeline registry accessors with the original target receiver", () => {
let timeline = "initial";
const timelineRegistry = {
get host() {
if (this !== timelineRegistry) {
throw new TypeError("Illegal invocation");
}
return timeline;
},
set host(value: string) {
if (this !== timelineRegistry) {
throw new TypeError("Illegal invocation");
}
timeline = value;
},
};
const fakeWindow = {
document: {
querySelector() {
return null;
},
querySelectorAll() {
return [];
},
},
__timelines: timelineRegistry,
__beforeTimeline: "",
__afterTimeline: "",
gsap: {},
};
const wrapped = wrapScopedCompositionScript(
`
window.__beforeTimeline = window.__timelines.scene;
window.__timelines.scene = "updated";
window.__afterTimeline = window.__timelines.scene;
`,
"scene",
"[HyperFrames] composition script error:",
undefined,
"host",
);
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
} finally {
errorSpy.mockRestore();
}
expect(fakeWindow.__beforeTimeline).toBe("initial");
expect(fakeWindow.__afterTimeline).toBe("updated");
expect(errorSpy).not.toHaveBeenCalled();
});
it("uses compound selector when authored root is the scoped element itself", () => {
const scoped = scopeCssToComposition(
"#chrome-overlay-root { --primary: #FFDC8B; }",
"chrome-overlay",
undefined,
"chrome-overlay-root",
{ compoundAuthoredRoot: true },
);
// Both attributes are on the same element after inlining, so the selector
// must be compound (no space) to match.
expect(scoped).toContain(
'[data-composition-id="chrome-overlay"][data-hf-authored-id="chrome-overlay-root"]',
);
expect(scoped).not.toContain(
'[data-composition-id="chrome-overlay"] [data-hf-authored-id="chrome-overlay-root"]',
);
});
it("uses compound selector for authored root with descendant combinators", () => {
const scoped = scopeCssToComposition(
"#chrome-overlay-root .chrome { display: flex; }",
"chrome-overlay",
undefined,
"chrome-overlay-root",
{ compoundAuthoredRoot: true },
);
// The authored root part is compound with scope, .chrome is a descendant
expect(scoped).toContain(
'[data-composition-id="chrome-overlay"][data-hf-authored-id="chrome-overlay-root"] .chrome',
);
expect(scoped).not.toMatch(
/\[data-composition-id="chrome-overlay"\]\s+\[data-hf-authored-id="chrome-overlay-root"\]\s+\.chrome/,
);
});
it("still uses descendant selector for non-root selectors with authoredRootId", () => {
const scoped = scopeCssToComposition(
".child-element { color: red; }",
"chrome-overlay",
undefined,
"chrome-overlay-root",
);
// Regular child selectors still get a descendant combinator (space)
expect(scoped).toContain('[data-composition-id="chrome-overlay"] .child-element');
});
it("rewrites #id CSS selectors to [data-hf-authored-id] when authoredRootId is provided", () => {
const scoped = scopeCssToComposition(
`#intro { background: #111; }
#intro .title { font-size: 120px; color: #fff; }`,
"intro",
undefined,
"intro",
);
// #intro should become [data-hf-authored-id="intro"]
expect(scoped).toContain('[data-hf-authored-id="intro"]');
expect(scoped).toContain('[data-hf-authored-id="intro"] .title');
// Raw #intro selectors should be gone
expect(scoped).not.toMatch(/#intro\b/);
});
it('does not rewrite [id="intro"] attribute selectors', () => {
// The function only targets #intro hash selectors, not [id="intro"] attribute selectors
const result = scopeCssToComposition(
'[id="intro"] .title { color: red; }',
"intro",
undefined,
"intro",
);
expect(result).toContain('[id="intro"]');
});
it("wraps scripts with authored root id normalization for #id GSAP selectors", () => {
const { document } = parseHTML(`
<div data-composition-id="intro">
<div data-hf-authored-id="intro">
<div class="title">HELLO</div>
</div>
</div>
`);
const gsapTargets: string[][] = [];
const fakeWindow = {
document,
__timelines: {},
gsap: {
timeline: () => ({
fromTo(targets: Element[], _from: unknown, _to: unknown) {
gsapTargets.push(Array.from(targets).map((t) => t.textContent || ""));
return this;
},
}),
},
};
const wrapped = wrapScopedCompositionScript(
`
var tl = gsap.timeline({ paused: true });
tl.fromTo('#intro .title', { opacity: 0 }, { opacity: 1, duration: 0.5 }, 0.2);
window.__timelines['intro'] = tl;
`,
"intro",
"[HyperFrames] composition script error:",
undefined,
"intro",
"intro",
);
new Function("window", "gsap", wrapped)(fakeWindow, fakeWindow.gsap);
// The scoped script should resolve '#intro .title' against the
// data-hf-authored-id="intro" element, finding the .title child.
expect(gsapTargets).toEqual([["HELLO"]]);
});
});