diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 4703fa525..34abacd29 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -39,6 +39,8 @@ "packages/engine/tests/**", "skills/**/test-corpus/**", "skills/**/scripts/**", + // Golden snapshot files: data consumed by toMatchFileSnapshot, not importable modules. + "packages/**/__goldens__/**", "registry/**", "examples/**", ".github/workflows/fixtures/**", diff --git a/.prettierignore b/.prettierignore index 9fba17dc3..d7fb4ef1b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,6 @@ packages/producer/tests/ # Cloud Workflows GCL — uses ${...} expressions that are not standard YAML. packages/gcp-cloud-run/terraform/workflow.yaml + +# toMatchFileSnapshot golden files — vitest writes these; oxfmt must not reformat them. +packages/**/__goldens__/ diff --git a/packages/core/src/parsers/__goldens__/complex.parsed.json b/packages/core/src/parsers/__goldens__/complex.parsed.json new file mode 100644 index 000000000..92c5def44 --- /dev/null +++ b/packages/core/src/parsers/__goldens__/complex.parsed.json @@ -0,0 +1,58 @@ +{ + "animations": [ + { + "targetSelector": ".ambient-line", + "method": "from", + "position": 0.16, + "properties": { + "scaleX": 0, + "opacity": 0 + }, + "duration": 0.42, + "extras": { + "stagger": "__raw:0.08" + }, + "id": ".ambient-line-from-160" + }, + { + "targetSelector": ".ambient-word", + "method": "from", + "position": 0.08, + "properties": { + "scale": 0.92, + "opacity": 0 + }, + "duration": 0.5, + "id": ".ambient-word-from-80" + }, + { + "targetSelector": ".headline .sub", + "method": "from", + "position": 0.2, + "properties": { + "y": 20, + "opacity": 0 + }, + "duration": 0.28, + "id": ".headline .sub-from-200" + }, + { + "targetSelector": ".headline span", + "method": "from", + "position": 0.05, + "properties": { + "y": 46, + "opacity": 0 + }, + "duration": 0.38, + "ease": "back.out(1.35)", + "extras": { + "stagger": "__raw:0.055" + }, + "id": ".headline span-from-50" + } + ], + "timelineVar": "tl", + "preamble": "window.__timelines = window.__timelines || {};\ngsap.defaults({ force3D: true });\nconst tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: \"power3.out\" } });", + "postamble": "window.__timelines[\"vpn-youtube-spot\"] = tl;" +} \ No newline at end of file diff --git a/packages/core/src/parsers/__goldens__/complex.serialized.js b/packages/core/src/parsers/__goldens__/complex.serialized.js new file mode 100644 index 000000000..2a1516fee --- /dev/null +++ b/packages/core/src/parsers/__goldens__/complex.serialized.js @@ -0,0 +1,10 @@ + + window.__timelines = window.__timelines || {}; +gsap.defaults({ force3D: true }); +const tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: "power3.out" } }); + tl.from(".headline span", { y: 46, opacity: 0, duration: 0.38, ease: "back.out(1.35)", stagger: 0.055 }, 0.05); + tl.from(".ambient-word", { scale: 0.92, opacity: 0, duration: 0.5 }, 0.08); + tl.from(".ambient-line", { scaleX: 0, opacity: 0, duration: 0.42, stagger: 0.08 }, 0.16); + tl.from(".headline .sub", { y: 20, opacity: 0, duration: 0.28 }, 0.2); + window.__timelines["vpn-youtube-spot"] = tl; + \ No newline at end of file diff --git a/packages/core/src/parsers/__goldens__/minimal.parsed.json b/packages/core/src/parsers/__goldens__/minimal.parsed.json new file mode 100644 index 000000000..f20fb8fb7 --- /dev/null +++ b/packages/core/src/parsers/__goldens__/minimal.parsed.json @@ -0,0 +1,31 @@ +{ + "animations": [ + { + "targetSelector": "#notification", + "method": "to", + "position": 0.2, + "properties": { + "x": 0, + "opacity": 1 + }, + "duration": 0.5, + "ease": "power3.out", + "id": "#notification-to-200" + }, + { + "targetSelector": "#notification", + "method": "to", + "position": 4.2, + "properties": { + "x": 420, + "opacity": 0 + }, + "duration": 0.3, + "ease": "power3.in", + "id": "#notification-to-4200" + } + ], + "timelineVar": "tl", + "preamble": "var tl = gsap.timeline({ paused: true });", + "postamble": "window.__timelines[\"macos-notification\"] = tl;" +} \ No newline at end of file diff --git a/packages/core/src/parsers/__goldens__/minimal.serialized.js b/packages/core/src/parsers/__goldens__/minimal.serialized.js new file mode 100644 index 000000000..8c8dd8d71 --- /dev/null +++ b/packages/core/src/parsers/__goldens__/minimal.serialized.js @@ -0,0 +1,6 @@ + + var tl = gsap.timeline({ paused: true }); + tl.to("#notification", { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.2); + tl.to("#notification", { x: 420, opacity: 0, duration: 0.3, ease: "power3.in" }, 4.2); + window.__timelines["macos-notification"] = tl; + \ No newline at end of file diff --git a/packages/core/src/parsers/__goldens__/moderate.parsed.json b/packages/core/src/parsers/__goldens__/moderate.parsed.json new file mode 100644 index 000000000..1c4e39127 --- /dev/null +++ b/packages/core/src/parsers/__goldens__/moderate.parsed.json @@ -0,0 +1,75 @@ +{ + "animations": [ + { + "targetSelector": "#card", + "method": "to", + "position": 0.1, + "properties": { + "y": 0, + "opacity": 1 + }, + "duration": 0.5, + "ease": "power3.out", + "id": "#card-to-100" + }, + { + "targetSelector": "#subscribe-btn", + "method": "to", + "position": 1, + "properties": { + "scale": 0.92 + }, + "duration": 0.15, + "ease": "power2.out", + "id": "#subscribe-btn-to-1000" + }, + { + "targetSelector": "#subscribe-btn", + "method": "to", + "position": 1.15, + "properties": { + "scale": 1 + }, + "duration": 0.4, + "ease": "elastic.out(1, 0.4)", + "id": "#subscribe-btn-to-1150" + }, + { + "targetSelector": "#btn-subscribe", + "method": "to", + "position": 1.15, + "properties": { + "opacity": 0 + }, + "duration": 0.08, + "ease": "none", + "id": "#btn-subscribe-to-1150" + }, + { + "targetSelector": "#btn-subscribed", + "method": "to", + "position": 1.18, + "properties": { + "opacity": 1 + }, + "duration": 0.08, + "ease": "none", + "id": "#btn-subscribed-to-1180" + }, + { + "targetSelector": "#card", + "method": "to", + "position": 3.8, + "properties": { + "y": 300, + "opacity": 0 + }, + "duration": 0.25, + "ease": "power3.in", + "id": "#card-to-3800" + } + ], + "timelineVar": "tl", + "preamble": "window.__timelines = window.__timelines || {};\nvar tl = gsap.timeline({ paused: true });", + "postamble": "window.__timelines[\"yt-lower-third\"] = tl;" +} \ No newline at end of file diff --git a/packages/core/src/parsers/__goldens__/moderate.serialized.js b/packages/core/src/parsers/__goldens__/moderate.serialized.js new file mode 100644 index 000000000..3093cc419 --- /dev/null +++ b/packages/core/src/parsers/__goldens__/moderate.serialized.js @@ -0,0 +1,11 @@ + + window.__timelines = window.__timelines || {}; +var tl = gsap.timeline({ paused: true }); + tl.to("#card", { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1); + tl.to("#subscribe-btn", { scale: 0.92, duration: 0.15, ease: "power2.out" }, 1); + tl.to("#subscribe-btn", { scale: 1, duration: 0.4, ease: "elastic.out(1, 0.4)" }, 1.15); + tl.to("#btn-subscribe", { opacity: 0, duration: 0.08, ease: "none" }, 1.15); + tl.to("#btn-subscribed", { opacity: 1, duration: 0.08, ease: "none" }, 1.18); + tl.to("#card", { y: 300, opacity: 0, duration: 0.25, ease: "power3.in" }, 3.8); + window.__timelines["yt-lower-third"] = tl; + \ No newline at end of file diff --git a/packages/core/src/parsers/gsapParser.golden.test.ts b/packages/core/src/parsers/gsapParser.golden.test.ts new file mode 100644 index 000000000..97e915668 --- /dev/null +++ b/packages/core/src/parsers/gsapParser.golden.test.ts @@ -0,0 +1,126 @@ +/** + * T6a — GSAP parser golden tests (baseline for the Recast → Meriyah swap). + * + * These snapshots capture the exact output of parseGsapScript + + * serializeGsapAnimations under Recast/Babel before any parser change. + * When the Meriyah swap lands, run `vitest --update-snapshots` to regenerate + * and diff the goldens — any change is a regression candidate. + * + * Three representative scripts: + * minimal — 2 tl.to calls, simple numeric selectors (macos-notification) + * moderate — 6 tl.to calls, multiple selectors (yt-lower-third) + * complex — stagger, chained .from()/.to(), const/defaults (vpn-youtube-spot) + */ +import { beforeAll, describe, expect, it } from "vitest"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseGsapScript, serializeGsapAnimations } from "./gsapParser.js"; + +const __goldens__ = join(fileURLToPath(import.meta.url), "..", "__goldens__"); +const g = (name: string) => join(__goldens__, name); + +// --------------------------------------------------------------------------- +// Corpus scripts (inline so goldens are not coupled to registry file changes) +// --------------------------------------------------------------------------- + +const MINIMAL_SCRIPT = `\ +var tl = gsap.timeline({ paused: true }); +var notification = document.getElementById("notification"); +gsap.set(notification, { x: 420, opacity: 0 }); +tl.to(notification, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.2); +tl.to(notification, { x: 420, opacity: 0, duration: 0.3, ease: "power3.in" }, 4.2); +window.__timelines["macos-notification"] = tl;`; + +const MODERATE_SCRIPT = `\ +window.__timelines = window.__timelines || {}; +var tl = gsap.timeline({ paused: true }); +var card = document.getElementById("card"); +var btn = document.getElementById("subscribe-btn"); +var textSub = document.getElementById("btn-subscribe"); +var textSubd = document.getElementById("btn-subscribed"); +gsap.set(card, { y: 300, opacity: 0 }); +tl.to(card, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1); +tl.to(btn, { scale: 0.92, duration: 0.15, ease: "power2.out" }, 1.0); +tl.to(btn, { scale: 1, duration: 0.4, ease: "elastic.out(1, 0.4)" }, 1.15); +tl.to(textSub, { opacity: 0, duration: 0.08, ease: "none" }, 1.15); +tl.to(textSubd, { opacity: 1, duration: 0.08, ease: "none" }, 1.18); +tl.to(card, { y: 300, opacity: 0, duration: 0.25, ease: "power3.in" }, 3.8); +window.__timelines["yt-lower-third"] = tl;`; + +const COMPLEX_SCRIPT = `\ +window.__timelines = window.__timelines || {}; +gsap.defaults({ force3D: true }); +const tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: "power3.out" } }); +const breatheRepeats = Math.ceil(7 / 2.4) - 1; +tl.from(".headline span", { y: 46, opacity: 0, stagger: 0.055, duration: 0.38, ease: "back.out(1.35)" }, 0.05) + .from(".headline .sub", { y: 20, opacity: 0, duration: 0.28 }, 0.2) + .from(".ambient-word", { scale: 0.92, opacity: 0, duration: 0.5 }, 0.08) + .from(".ambient-line", { scaleX: 0, opacity: 0, stagger: 0.08, duration: 0.42 }, 0.16); +window.__timelines["vpn-youtube-spot"] = tl;`; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function parseAndSerialize(script: string): { parsed: string; serialized: string } { + const result = parseGsapScript(script); + const serialized = serializeGsapAnimations(result.animations, result.timelineVar, { + preamble: result.preamble, + postamble: result.postamble, + }); + return { parsed: JSON.stringify(result, null, 2), serialized }; +} + +// --------------------------------------------------------------------------- +// Golden tests +// --------------------------------------------------------------------------- + +describe("T6a — GSAP parser golden tests (Recast/Babel baseline)", () => { + describe("minimal — 2 tl.to calls (macos-notification)", () => { + let parsed: string; + let serialized: string; + beforeAll(() => { + ({ parsed, serialized } = parseAndSerialize(MINIMAL_SCRIPT)); + }); + + it("parseGsapScript output matches golden", async () => { + await expect(parsed).toMatchFileSnapshot(g("minimal.parsed.json")); + }); + + it("serializeGsapAnimations output matches golden", async () => { + await expect(serialized).toMatchFileSnapshot(g("minimal.serialized.js")); + }); + }); + + describe("moderate — 6 tl.to calls, multiple selectors (yt-lower-third)", () => { + let parsed: string; + let serialized: string; + beforeAll(() => { + ({ parsed, serialized } = parseAndSerialize(MODERATE_SCRIPT)); + }); + + it("parseGsapScript output matches golden", async () => { + await expect(parsed).toMatchFileSnapshot(g("moderate.parsed.json")); + }); + + it("serializeGsapAnimations output matches golden", async () => { + await expect(serialized).toMatchFileSnapshot(g("moderate.serialized.js")); + }); + }); + + describe("complex — stagger + chained .from() calls (vpn-youtube-spot)", () => { + let parsed: string; + let serialized: string; + beforeAll(() => { + ({ parsed, serialized } = parseAndSerialize(COMPLEX_SCRIPT)); + }); + + it("parseGsapScript output matches golden", async () => { + await expect(parsed).toMatchFileSnapshot(g("complex.parsed.json")); + }); + + it("serializeGsapAnimations output matches golden", async () => { + await expect(serialized).toMatchFileSnapshot(g("complex.serialized.js")); + }); + }); +});