Files
hyperframes/packages/parsers/src/gsapParserAcorn.inline.test.ts
T
Miguel Ángel 2e02bcf77a feat(gsap): read timelines authored inline (acorn read path) (#1760)
* feat(studio): element groups — source mutations

Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements
routes) that the studio group feature is built on. Studio UI lands in the next PR.

* fix(studio): hoverable group interior + non-sticky drill-in

Two group selection bugs with animated members:

1) Empty space inside a group's overlay didn't hover/select the group. Members
   animated outside the wrapper's static box (110px box vs 340px member union),
   so elementsFromPoint hit only the full-bleed background there. Add a
   member-union hit-test fallback: a point inside a group's live member bounds
   resolves to that group (innermost wins).

2) After drilling into a group and selecting a child, nothing else was
   selectable — out-of-scope resolved to null. Make drill-in non-sticky:
   interacting outside the drilled group re-resolves normally and exits the
   drill-in, so a later click on the group selects it as a unit again.

* feat(studio): enable animation editing for static inline timelines

The unsupported-pattern banner now clears for static window.__timelines["id"] =
gsap.timeline() (the parser reports it editable), and the banner copy is retargeted
to the genuinely-unsupported case: computed/dynamic keys (window.__timelines[var]).
2026-06-27 11:27:53 -04:00

76 lines
3.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseGsapScriptAcorn } from "./gsapParserAcorn.js";
// U1+U2: the editor must read timelines authored inline as
// `window.__timelines["id"] = gsap.timeline()` — not just the canonical
// `const tl = gsap.timeline(); window.__timelines[id] = tl` form.
const wrap = (decl: string, tweens: string) =>
`window.__timelines = window.__timelines || {};\n${decl}\n${tweens}`;
describe("inline timeline assignment — read", () => {
it("reads tweens from a double-quoted inline timeline", () => {
const src = wrap(
`window.__timelines["scene"] = gsap.timeline({ paused: true });`,
`window.__timelines["scene"].to("#a", { x: 100, duration: 1 }, 0);\n` +
`window.__timelines["scene"].to("#b", { y: 50, duration: 1 }, 0.5);`,
);
const parsed = parseGsapScriptAcorn(src);
expect(parsed.unsupportedTimelinePattern).toBeFalsy();
expect(parsed.animations).toHaveLength(2);
expect(parsed.animations[0]!.targetSelector).toBe("#a");
expect(parsed.animations[1]!.targetSelector).toBe("#b");
});
it("reads a single-quoted inline timeline", () => {
const src = wrap(
`window.__timelines['scene'] = gsap.timeline();`,
`window.__timelines['scene'].to('#a', { x: 10, duration: 1 }, 0);`,
);
const parsed = parseGsapScriptAcorn(src);
expect(parsed.unsupportedTimelinePattern).toBeFalsy();
expect(parsed.animations).toHaveLength(1);
expect(parsed.animations[0]!.targetSelector).toBe("#a");
});
it("reads a static dot-access inline timeline", () => {
const src = wrap(
`window.__timelines.scene = gsap.timeline();`,
`window.__timelines.scene.to("#a", { x: 10, duration: 1 }, 0);`,
);
const parsed = parseGsapScriptAcorn(src);
expect(parsed.unsupportedTimelinePattern).toBeFalsy();
expect(parsed.animations).toHaveLength(1);
});
it("flags a computed-key timeline as unsupported (cannot statically resolve)", () => {
const src = wrap(
`const id = "scene";\nwindow.__timelines[id] = gsap.timeline();`,
`window.__timelines[id].to("#a", { x: 10, duration: 1 }, 0);`,
);
const parsed = parseGsapScriptAcorn(src);
expect(parsed.unsupportedTimelinePattern).toBe(true);
});
it("does not cross-attribute tweens of a different member slot", () => {
const src = wrap(
`window.__timelines["a"] = gsap.timeline();\nwindow.__timelines["b"] = gsap.timeline();`,
`window.__timelines["a"].to("#a", { x: 1, duration: 1 }, 0);\n` +
`window.__timelines["b"].to("#b", { x: 2, duration: 1 }, 0);`,
);
const parsed = parseGsapScriptAcorn(src);
// First detected timeline is "a"; only its tween should be attributed here.
expect(parsed.multipleTimelines).toBe(true);
expect(parsed.animations.some((a) => a.targetSelector === "#a")).toBe(true);
expect(parsed.animations.every((a) => a.targetSelector !== "#b")).toBe(true);
});
it("leaves the canonical const form working", () => {
const src = `const tl = gsap.timeline();\nwindow.__timelines["scene"] = tl;\ntl.to("#a", { x: 5, duration: 1 }, 0);`;
const parsed = parseGsapScriptAcorn(src);
expect(parsed.unsupportedTimelinePattern).toBeFalsy();
expect(parsed.animations).toHaveLength(1);
expect(parsed.timelineVar).toBe("tl");
});
});