fix(render): surface structured outcomes (#2153)

This commit is contained in:
James Russo
2026-07-13 19:07:39 -04:00
committed by GitHub
parent 6892b62662
commit cb69d3fe00
29 changed files with 1137 additions and 205 deletions
@@ -582,7 +582,7 @@ window.__utilsMarker = gsap.utils.marker;
expect(errorSpy).not.toHaveBeenCalled();
});
it("reads remapped timeline registry accessors with the original target receiver", () => {
it("reads and dual-publishes remapped timelines without replacing the registry", () => {
let timeline = "initial";
const timelineRegistry = {
get host() {
@@ -633,6 +633,8 @@ window.__afterTimeline = window.__timelines.scene;
expect(fakeWindow.__beforeTimeline).toBe("initial");
expect(fakeWindow.__afterTimeline).toBe("updated");
expect(Reflect.get(timelineRegistry, "scene")).toBe("updated");
expect(fakeWindow.__timelines).toBe(timelineRegistry);
expect(errorSpy).not.toHaveBeenCalled();
});
@@ -408,10 +408,25 @@ export function wrapScopedCompositionScript(
if (!__hfTimelineRegistryProxy) {
__hfTimelineRegistryProxy = new Proxy(window.__timelines, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop === __hfCompId ? __hfTimelineCompId : prop, target);
if (prop !== __hfCompId) {
return Reflect.get(target, prop, target);
}
var authoredValue = Reflect.get(target, prop, target);
return authoredValue === undefined
? Reflect.get(target, __hfTimelineCompId, target)
: authoredValue;
},
set: function(target, prop, value, receiver) {
return Reflect.set(target, prop === __hfCompId ? __hfTimelineCompId : prop, value, target);
if (prop !== __hfCompId) {
return Reflect.set(target, prop, value, target);
}
// The authored node remains in the compiled DOM when its local id
// differs from the runtime mount id, so readiness legitimately sees
// both compositions. Publish the same timeline under both identities
// instead of replacing one with the other.
var authoredSet = Reflect.set(target, __hfCompId, value, target);
var runtimeSet = Reflect.set(target, __hfTimelineCompId, value, target);
return authoredSet && runtimeSet;
},
});
}
@@ -435,6 +450,11 @@ export function wrapScopedCompositionScript(
},
set: function(target, prop, value, receiver) {
if (prop === "__timelines") {
// Common authoring boilerplate assigns the registry back to
// itself (window.__timelines = window.__timelines || {}). The
// getter above returns our proxy; do not replace the canonical
// registry with that proxy or later wrappers will stack proxies.
if (value === __hfTimelineRegistryProxy) return true;
target.__timelines = value || {};
__hfTimelineRegistryProxy = null;
return true;
@@ -142,6 +142,32 @@ describe("inlineSubCompositions #ID selector scoping divergence", () => {
expect(wrappedScript).toContain('"intro"');
});
it("maps a template-local timeline id onto a differently named mount", () => {
const document = makeHostDocument("captions-comp");
const host = document.querySelector('[data-composition-src="intro.html"]')!;
const captionsHtml = `<template id="captions-template">
<div data-composition-id="captions" data-width="1920" data-height="1080">
<style>[data-composition-id="captions"] { opacity: 1; }</style>
<script>
window.__timelines = window.__timelines || {};
window.__timelines["captions"] = { duration: 4 };
</script>
</div>
</template>`;
const result = inlineSubCompositions(document, [host], {
resolveHtml: () => captionsHtml,
parseHtml: (html) => parseHTML(html).document,
});
expect(host.getAttribute("data-composition-id")).toBe("captions-comp");
expect(host.querySelector('[data-composition-id="captions"]')).not.toBeNull();
expect(result.styles.join("\n")).toContain('[data-composition-id="captions-comp"]');
const wrappedScript = result.scripts.join("\n");
expect(wrappedScript).toContain('var __hfCompId = "captions"');
expect(wrappedScript).toContain('var __hfTimelineCompId = "captions-comp"');
});
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"]')!;
@@ -228,13 +228,21 @@ export function inlineSubCompositions(
continue;
}
// Find the inner composition root
// Keep structural flattening tied to an exact mount-id match. A template
// may intentionally use a different local id (for example, a
// `captions-comp` host mounting a `captions` template); flattening that
// fallback root changes the compiled DOM and can invalidate selectors and
// regression goldens. Discover it separately so script timeline
// registration can still map the authored id onto the runtime mount id.
const innerRoot = compId
? queryByAttr(contentDoc, "data-composition-id", compId)
: contentDoc.querySelector("[data-composition-id]");
const inferredCompId = innerRoot?.getAttribute("data-composition-id")?.trim() || "";
const authoredCompositionRoot = innerRoot ?? contentDoc.querySelector("[data-composition-id]");
const inferredCompId =
authoredCompositionRoot?.getAttribute("data-composition-id")?.trim() || "";
const authoredRootId = innerRoot?.getAttribute("id")?.trim() || null;
const scopeCompId = compId || inferredCompId;
const scriptCompositionId = inferredCompId || scopeCompId;
const runtimeScope = runtimeCompId ? buildScopeSelector(runtimeCompId) : "";
// Variable merging (bundler feature). Read declared defaults from the
@@ -312,13 +320,13 @@ export function inlineSubCompositions(
}
scriptItems.push({ kind: "external", src: externalSrc });
} else {
const wrappedScript = scopeCompId
const wrappedScript = scriptCompositionId
? wrapScopedCompositionScript(
s.textContent || "",
scopeCompId,
scriptCompositionId,
scriptErrorLabel,
runtimeScope || undefined,
runtimeCompId || scopeCompId,
runtimeCompId || scopeCompId || scriptCompositionId,
authoredRootId,
)
: wrapInlineScriptWithErrorBoundary(s.textContent || "", scriptErrorLabel);