diff --git a/packages/core/src/compiler/compositionScoping.test.ts b/packages/core/src/compiler/compositionScoping.test.ts index 338c1ca99..95110af8c 100644 --- a/packages/core/src/compiler/compositionScoping.test.ts +++ b/packages/core/src/compiler/compositionScoping.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { parseHTML } from "linkedom"; import { scopeCssToComposition, wrapScopedCompositionScript } from "./compositionScoping"; @@ -89,4 +89,148 @@ window.__timelines.scene = tl; expect(fakeWindow.__selectedRootTitle).toBe("Scene"); expect(gsapTargets).toEqual([["Scene"], ["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(); + }); }); diff --git a/packages/core/src/compiler/compositionScoping.ts b/packages/core/src/compiler/compositionScoping.ts index 6194723f2..34af7ba26 100644 --- a/packages/core/src/compiler/compositionScoping.ts +++ b/packages/core/src/compiler/compositionScoping.ts @@ -152,7 +152,7 @@ export function wrapScopedCompositionScript( return found && __hfContains(found) ? found : null; }; } - var value = Reflect.get(target, prop, receiver); + var value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; }, }) @@ -166,10 +166,10 @@ export function wrapScopedCompositionScript( if (!__hfTimelineRegistryProxy) { __hfTimelineRegistryProxy = new Proxy(window.__timelines, { get: function(target, prop, receiver) { - return Reflect.get(target, prop === __hfCompId ? __hfTimelineCompId : prop, receiver); + return Reflect.get(target, prop === __hfCompId ? __hfTimelineCompId : prop, target); }, set: function(target, prop, value, receiver) { - return Reflect.set(target, prop === __hfCompId ? __hfTimelineCompId : prop, value, receiver); + return Reflect.set(target, prop === __hfCompId ? __hfTimelineCompId : prop, value, target); }, }); } @@ -179,7 +179,7 @@ export function wrapScopedCompositionScript( ? new Proxy(window, { get: function(target, prop, receiver) { if (prop === "__timelines") return __hfGetTimelineRegistry(); - var value = Reflect.get(target, prop, receiver); + var value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; }, set: function(target, prop, value, receiver) { @@ -188,7 +188,7 @@ export function wrapScopedCompositionScript( __hfTimelineRegistryProxy = null; return true; } - return Reflect.set(target, prop, value, receiver); + return Reflect.set(target, prop, value, target); }, }) : window; @@ -252,12 +252,12 @@ export function wrapScopedCompositionScript( }; }; } - var value = Reflect.get(utilsTarget, utilsProp, utilsReceiver); + var value = Reflect.get(utilsTarget, utilsProp, utilsTarget); return typeof value === "function" ? value.bind(utilsTarget) : value; }, }); } - var value = Reflect.get(target, prop, receiver); + var value = Reflect.get(target, prop, target); return typeof value === "function" ? value.bind(target) : value; }, });