test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot) (#1263)

* test(studio): add T5b rotation+motion build-patches characterization

Extends manualEditsDomPatches.test.ts with rotation and motion pairs.
Same 4-pattern structure: populated, empty, clear restores originals,
build/clear symmetry. Merges duplicate manualEditsTypes import block.

* test(studio): add T5c review-fix gaps in manualEditsDomPatches characterization

Fixes four gaps identified in max-setting code review:
- Box-size clear: replace arrayContaining with full ordered toEqual (30 ops)
- Box-size / pathOffset / rotation clear: add empty-string coercion tests
  (origVal||null must produce null, not set property to "")
- Rotation clear: add test for absent STUDIO_ORIGINAL_ROTATION_TRANSFORM_ORIGIN_ATTR
- Motion clear: prove input-independence by calling with both empty and populated
  element and asserting identical output

* refactor(core): extract maxEndTime+serialize to parsers/test-utils.ts (TU)

Deduplicate helpers shared by T1 (htmlParser.roundtrip.test.ts) and T2
(stableIds.test.ts). Both files inline identical implementations; extract
to test-utils.ts so future parser tests (T6a…) import one copy.

Also fix lefthook fallow command to unset GIT_DIR+GIT_INDEX_FILE before
running — those vars are set by git in worktree hook context and block
fallow’s internal temp-worktree creation.

* test(core): add T10 PreviewAdapter contract stubs (spec for R7)

All 14 tests are it.todo, following the T4 pattern. The stubs define the
full createPreviewAdapter interface — elementAtPoint (root exclusion,
hf-id ancestor walk, opacity filter), applyDraft/revertDraft (draft
marker lifecycle), commitPreview (patch derivation), and getElementTimings
(data-start/data-end reader).

createPreviewAdapter does not exist yet; R7 implements it and converts
these stubs to real assertions.

* test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot)

6 toMatchFileSnapshot tests across 3 representative scripts (minimal,
moderate, complex). Captures parseGsapScript + serializeGsapAnimations
output before the Recast → Meriyah swap so any parser change is detected
as a golden diff rather than a silent behavioral regression.

Goldens live in src/parsers/__goldens__/ and are checked in. Add
__goldens__/** to fallow ignorePatterns (data files, not modules) and to
.prettierignore so oxfmt does not reformat vitest-written snapshot files.
This commit is contained in:
Vance Ingalls
2026-06-07 19:23:06 -07:00
committed by GitHub
parent 1fdce71c8b
commit 184ef03317
9 changed files with 322 additions and 0 deletions
@@ -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;"
}
@@ -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;
@@ -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;"
}
@@ -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;
@@ -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;"
}
@@ -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;
@@ -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"));
});
});
});