import { describe, expect, it, vi } from "vitest"; import { parseHTML } from "linkedom"; import { scopeCssToComposition, wrapInlineScriptWithErrorBoundary, 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(`
`); const fakeWindow: Record = { 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(`
`); const fakeWindow: Record = { 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(`
`); const fakeWindow: Record = { 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(`
`); const variablesByComp: Record> = { "card-1": { title: "Pro" }, }; const fakeWindow: Record = { 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("preserves static methods on classes exposed through window", () => { const { document } = parseHTML(`
`); class FakeTexts { static mountChars() { return "ok"; } } const fakeWindow: Record = { document, __timelines: {}, Texts: FakeTexts, }; const wrapped = wrapScopedCompositionScript( `window.__capturedMountCharsType = typeof window.Texts?.mountChars;`, "scene", ); new Function("window", wrapped)(fakeWindow); expect(fakeWindow.__capturedMountCharsType).toBe("function"); }); it("executes document and GSAP selectors inside the composition root", () => { const { document } = parseHTML(`

Scene

Other

`); 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(`
`); 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(`
`); 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(`

Scene

`); 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(` `); 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(`

Scene

Other

`); 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("wraps scoped composition script source as a string literal", () => { const wrapped = wrapScopedCompositionScript( 'window.payload = "";', "scene", ); expect(wrapped).toContain('Function("document", "gsap", "window", "__hyperframes", '); expect(wrapped).toContain('\\"\\"'); }); it("wraps unscoped composition script source as a string literal", () => { const wrapped = wrapInlineScriptWithErrorBoundary( 'window.payload = "";', "[HyperFrames] composition script error:", ); expect(wrapped).toContain("Function("); expect(wrapped).toContain('\\"\\"'); }); 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(`
HELLO
`); 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"]]); }); });