mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
test: add regression fixture for sub-comp #ID selector scoping divergence
The producer inlining path strips the inner root element (taking innerHTML when compId matches), losing the id attribute. The bundler path preserves it via flattenInnerRoot + data-hf-authored-root-id. This causes #ID selectors in sub-comp CSS and GSAP to fail silently during render while working in preview. Adds a minimal fixture with a sub-comp using #intro scope to catch this divergence in future compiler changes.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseHTML } from "linkedom";
|
||||
import { inlineSubCompositions } from "./inlineSubCompositions";
|
||||
|
||||
/**
|
||||
* Minimal sub-composition HTML that uses `#intro` as its CSS and GSAP scope.
|
||||
* This is the pattern that breaks when the producer path strips the inner root.
|
||||
*/
|
||||
const SUB_COMP_HTML = `<template id="intro-template">
|
||||
<div id="intro" data-composition-id="intro" data-width="1920" data-height="1080">
|
||||
<div class="title" style="opacity:0;">HELLO WORLD</div>
|
||||
<style>
|
||||
#intro { position:relative; width:1920px; height:1080px; background:#111; }
|
||||
#intro .title { font-size:120px; color:#fff; }
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.fromTo('#intro .title', { opacity:0 }, { opacity:1, duration:0.5 }, 0.2);
|
||||
window.__timelines['intro'] = tl;
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</template>`;
|
||||
|
||||
function makeHostDocument(compId: string) {
|
||||
const { document } = parseHTML(`<!DOCTYPE html>
|
||||
<html><body>
|
||||
<div data-composition-id="main">
|
||||
<div data-composition-id="${compId}" data-composition-src="intro.html"
|
||||
data-start="0" data-duration="4" data-track-index="0"></div>
|
||||
</div>
|
||||
</body></html>`);
|
||||
return document;
|
||||
}
|
||||
|
||||
describe("inlineSubCompositions – #ID selector scoping divergence", () => {
|
||||
it("producer path (no flattenInnerRoot): strips inner root, losing #id attribute", () => {
|
||||
const document = makeHostDocument("intro");
|
||||
const host = document.querySelector('[data-composition-src="intro.html"]')!;
|
||||
|
||||
const result = inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP_HTML,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
});
|
||||
|
||||
// The producer path takes innerHTML when compId matches, stripping the
|
||||
// wrapper <div id="intro" ...>. The host element should NOT contain a
|
||||
// child with id="intro" — the id attribute is lost.
|
||||
const innerRootById = host.querySelector("#intro");
|
||||
expect(innerRootById).toBeNull();
|
||||
|
||||
// The host itself still has data-composition-id="intro" (from the
|
||||
// original markup), but no element inside has id="intro".
|
||||
expect(host.getAttribute("data-composition-id")).toBe("intro");
|
||||
|
||||
// CSS was scoped: #intro selectors should be rewritten to use
|
||||
// data-hf-authored-id attribute selector so they still resolve.
|
||||
const scopedCss = result.styles.join("\n");
|
||||
expect(scopedCss).toContain('[data-hf-authored-id="intro"]');
|
||||
expect(scopedCss).not.toContain("#intro");
|
||||
});
|
||||
|
||||
it("producer path: scoped CSS rewrites #id selectors to [data-hf-authored-id] attribute", () => {
|
||||
const document = makeHostDocument("intro");
|
||||
const host = document.querySelector('[data-composition-src="intro.html"]')!;
|
||||
|
||||
const result = inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP_HTML,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
});
|
||||
|
||||
// The CSS scoper rewrites `#intro` to `[data-hf-authored-id="intro"]`
|
||||
// so that the selector resolves against the flattened structure.
|
||||
const scopedCss = result.styles.join("\n");
|
||||
expect(scopedCss).toContain('[data-hf-authored-id="intro"]');
|
||||
expect(scopedCss).toContain('[data-hf-authored-id="intro"] .title');
|
||||
});
|
||||
|
||||
it("producer path: scoped scripts rewrite #intro selectors for GSAP targets", () => {
|
||||
const document = makeHostDocument("intro");
|
||||
const host = document.querySelector('[data-composition-src="intro.html"]')!;
|
||||
|
||||
const result = inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP_HTML,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
});
|
||||
|
||||
// The wrapped script should contain the authored root id normalization
|
||||
// logic so that runtime querySelector('#intro .title') maps to the
|
||||
// data-hf-authored-id attribute selector.
|
||||
const wrappedScript = result.scripts.join("\n");
|
||||
expect(wrappedScript).toContain("__hfAuthoredRootId");
|
||||
expect(wrappedScript).toContain('"intro"');
|
||||
});
|
||||
|
||||
it("bundler path (with flattenInnerRoot): preserves inner root as a child element", () => {
|
||||
const document = makeHostDocument("intro");
|
||||
const host = document.querySelector('[data-composition-src="intro.html"]')!;
|
||||
|
||||
// Simulate the bundler's flattenInnerRoot: clone the element, add
|
||||
// data-hf-authored-id, strip timing attrs (simplified here).
|
||||
function flattenInnerRoot(innerRoot: Element): Element {
|
||||
const clone = innerRoot.cloneNode(true) as Element;
|
||||
const authoredId = clone.getAttribute("id");
|
||||
if (authoredId) {
|
||||
clone.setAttribute("data-hf-authored-id", authoredId);
|
||||
clone.removeAttribute("id");
|
||||
}
|
||||
clone.removeAttribute("data-start");
|
||||
clone.removeAttribute("data-duration");
|
||||
return clone;
|
||||
}
|
||||
|
||||
const result = inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP_HTML,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
flattenInnerRoot,
|
||||
});
|
||||
|
||||
// With flattenInnerRoot, the inner root is preserved as a child of the
|
||||
// host via outerHTML. The data-hf-authored-id attribute is present.
|
||||
const authoredRoot = host.querySelector('[data-hf-authored-id="intro"]');
|
||||
expect(authoredRoot).not.toBeNull();
|
||||
|
||||
// CSS is still rewritten to use the attribute selector.
|
||||
const scopedCss = result.styles.join("\n");
|
||||
expect(scopedCss).toContain('[data-hf-authored-id="intro"]');
|
||||
});
|
||||
|
||||
/**
|
||||
* Known divergence: the producer path strips the inner root element via
|
||||
* innerHTML (line 307 of inlineSubCompositions.ts), losing the id attribute.
|
||||
* The bundler path preserves it via flattenInnerRoot + data-hf-authored-id.
|
||||
*
|
||||
* Both paths rewrite CSS and GSAP selectors from `#intro` to
|
||||
* `[data-hf-authored-id="intro"]`, but only the bundler path actually adds
|
||||
* that attribute to an element. In the producer path, no element carries
|
||||
* `data-hf-authored-id`, so the rewritten selectors match nothing.
|
||||
*
|
||||
* Workaround: use [data-composition-id="name"] as scope in sub-compositions
|
||||
* instead of #name.
|
||||
*
|
||||
* Proper fix (follow-up): make the producer path add data-hf-authored-id
|
||||
* to the host element when the inner root has an id attribute.
|
||||
*/
|
||||
it("documents the divergence: producer path lacks data-hf-authored-id element", () => {
|
||||
const document = makeHostDocument("intro");
|
||||
const host = document.querySelector('[data-composition-src="intro.html"]')!;
|
||||
|
||||
// Producer path: no flattenInnerRoot
|
||||
inlineSubCompositions(document, [host], {
|
||||
resolveHtml: () => SUB_COMP_HTML,
|
||||
parseHtml: (html) => parseHTML(html).document,
|
||||
});
|
||||
|
||||
// After producer inlining, no element inside the host has
|
||||
// data-hf-authored-id="intro". The rewritten CSS/GSAP selectors
|
||||
// targeting [data-hf-authored-id="intro"] will match nothing.
|
||||
const authoredIdElement = host.querySelector('[data-hf-authored-id="intro"]');
|
||||
expect(authoredIdElement).toBeNull();
|
||||
|
||||
// The original #intro element is gone — innerHTML stripped it.
|
||||
const introById = host.querySelector("#intro");
|
||||
expect(introById).toBeNull();
|
||||
|
||||
// Only the host itself has data-composition-id="intro", which is the
|
||||
// correct workaround scope to use.
|
||||
expect(host.getAttribute("data-composition-id")).toBe("intro");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user