Skip to content

Commit 184ef03

Browse files
authored
test(core): add T6a GSAP parser golden baselines (Recast/Babel snapshot) (heygen-com#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.
1 parent 1fdce71 commit 184ef03

9 files changed

Lines changed: 322 additions & 0 deletions

.fallowrc.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
"packages/engine/tests/**",
4040
"skills/**/test-corpus/**",
4141
"skills/**/scripts/**",
42+
// Golden snapshot files: data consumed by toMatchFileSnapshot, not importable modules.
43+
"packages/**/__goldens__/**",
4244
"registry/**",
4345
"examples/**",
4446
".github/workflows/fixtures/**",

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ packages/producer/tests/
55

66
# Cloud Workflows GCL — uses ${...} expressions that are not standard YAML.
77
packages/gcp-cloud-run/terraform/workflow.yaml
8+
9+
# toMatchFileSnapshot golden files — vitest writes these; oxfmt must not reformat them.
10+
packages/**/__goldens__/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"animations": [
3+
{
4+
"targetSelector": ".ambient-line",
5+
"method": "from",
6+
"position": 0.16,
7+
"properties": {
8+
"scaleX": 0,
9+
"opacity": 0
10+
},
11+
"duration": 0.42,
12+
"extras": {
13+
"stagger": "__raw:0.08"
14+
},
15+
"id": ".ambient-line-from-160"
16+
},
17+
{
18+
"targetSelector": ".ambient-word",
19+
"method": "from",
20+
"position": 0.08,
21+
"properties": {
22+
"scale": 0.92,
23+
"opacity": 0
24+
},
25+
"duration": 0.5,
26+
"id": ".ambient-word-from-80"
27+
},
28+
{
29+
"targetSelector": ".headline .sub",
30+
"method": "from",
31+
"position": 0.2,
32+
"properties": {
33+
"y": 20,
34+
"opacity": 0
35+
},
36+
"duration": 0.28,
37+
"id": ".headline .sub-from-200"
38+
},
39+
{
40+
"targetSelector": ".headline span",
41+
"method": "from",
42+
"position": 0.05,
43+
"properties": {
44+
"y": 46,
45+
"opacity": 0
46+
},
47+
"duration": 0.38,
48+
"ease": "back.out(1.35)",
49+
"extras": {
50+
"stagger": "__raw:0.055"
51+
},
52+
"id": ".headline span-from-50"
53+
}
54+
],
55+
"timelineVar": "tl",
56+
"preamble": "window.__timelines = window.__timelines || {};\ngsap.defaults({ force3D: true });\nconst tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: \"power3.out\" } });",
57+
"postamble": "window.__timelines[\"vpn-youtube-spot\"] = tl;"
58+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
window.__timelines = window.__timelines || {};
3+
gsap.defaults({ force3D: true });
4+
const tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: "power3.out" } });
5+
tl.from(".headline span", { y: 46, opacity: 0, duration: 0.38, ease: "back.out(1.35)", stagger: 0.055 }, 0.05);
6+
tl.from(".ambient-word", { scale: 0.92, opacity: 0, duration: 0.5 }, 0.08);
7+
tl.from(".ambient-line", { scaleX: 0, opacity: 0, duration: 0.42, stagger: 0.08 }, 0.16);
8+
tl.from(".headline .sub", { y: 20, opacity: 0, duration: 0.28 }, 0.2);
9+
window.__timelines["vpn-youtube-spot"] = tl;
10+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"animations": [
3+
{
4+
"targetSelector": "#notification",
5+
"method": "to",
6+
"position": 0.2,
7+
"properties": {
8+
"x": 0,
9+
"opacity": 1
10+
},
11+
"duration": 0.5,
12+
"ease": "power3.out",
13+
"id": "#notification-to-200"
14+
},
15+
{
16+
"targetSelector": "#notification",
17+
"method": "to",
18+
"position": 4.2,
19+
"properties": {
20+
"x": 420,
21+
"opacity": 0
22+
},
23+
"duration": 0.3,
24+
"ease": "power3.in",
25+
"id": "#notification-to-4200"
26+
}
27+
],
28+
"timelineVar": "tl",
29+
"preamble": "var tl = gsap.timeline({ paused: true });",
30+
"postamble": "window.__timelines[\"macos-notification\"] = tl;"
31+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
var tl = gsap.timeline({ paused: true });
3+
tl.to("#notification", { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.2);
4+
tl.to("#notification", { x: 420, opacity: 0, duration: 0.3, ease: "power3.in" }, 4.2);
5+
window.__timelines["macos-notification"] = tl;
6+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"animations": [
3+
{
4+
"targetSelector": "#card",
5+
"method": "to",
6+
"position": 0.1,
7+
"properties": {
8+
"y": 0,
9+
"opacity": 1
10+
},
11+
"duration": 0.5,
12+
"ease": "power3.out",
13+
"id": "#card-to-100"
14+
},
15+
{
16+
"targetSelector": "#subscribe-btn",
17+
"method": "to",
18+
"position": 1,
19+
"properties": {
20+
"scale": 0.92
21+
},
22+
"duration": 0.15,
23+
"ease": "power2.out",
24+
"id": "#subscribe-btn-to-1000"
25+
},
26+
{
27+
"targetSelector": "#subscribe-btn",
28+
"method": "to",
29+
"position": 1.15,
30+
"properties": {
31+
"scale": 1
32+
},
33+
"duration": 0.4,
34+
"ease": "elastic.out(1, 0.4)",
35+
"id": "#subscribe-btn-to-1150"
36+
},
37+
{
38+
"targetSelector": "#btn-subscribe",
39+
"method": "to",
40+
"position": 1.15,
41+
"properties": {
42+
"opacity": 0
43+
},
44+
"duration": 0.08,
45+
"ease": "none",
46+
"id": "#btn-subscribe-to-1150"
47+
},
48+
{
49+
"targetSelector": "#btn-subscribed",
50+
"method": "to",
51+
"position": 1.18,
52+
"properties": {
53+
"opacity": 1
54+
},
55+
"duration": 0.08,
56+
"ease": "none",
57+
"id": "#btn-subscribed-to-1180"
58+
},
59+
{
60+
"targetSelector": "#card",
61+
"method": "to",
62+
"position": 3.8,
63+
"properties": {
64+
"y": 300,
65+
"opacity": 0
66+
},
67+
"duration": 0.25,
68+
"ease": "power3.in",
69+
"id": "#card-to-3800"
70+
}
71+
],
72+
"timelineVar": "tl",
73+
"preamble": "window.__timelines = window.__timelines || {};\nvar tl = gsap.timeline({ paused: true });",
74+
"postamble": "window.__timelines[\"yt-lower-third\"] = tl;"
75+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
window.__timelines = window.__timelines || {};
3+
var tl = gsap.timeline({ paused: true });
4+
tl.to("#card", { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1);
5+
tl.to("#subscribe-btn", { scale: 0.92, duration: 0.15, ease: "power2.out" }, 1);
6+
tl.to("#subscribe-btn", { scale: 1, duration: 0.4, ease: "elastic.out(1, 0.4)" }, 1.15);
7+
tl.to("#btn-subscribe", { opacity: 0, duration: 0.08, ease: "none" }, 1.15);
8+
tl.to("#btn-subscribed", { opacity: 1, duration: 0.08, ease: "none" }, 1.18);
9+
tl.to("#card", { y: 300, opacity: 0, duration: 0.25, ease: "power3.in" }, 3.8);
10+
window.__timelines["yt-lower-third"] = tl;
11+
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* T6a — GSAP parser golden tests (baseline for the Recast → Meriyah swap).
3+
*
4+
* These snapshots capture the exact output of parseGsapScript +
5+
* serializeGsapAnimations under Recast/Babel before any parser change.
6+
* When the Meriyah swap lands, run `vitest --update-snapshots` to regenerate
7+
* and diff the goldens — any change is a regression candidate.
8+
*
9+
* Three representative scripts:
10+
* minimal — 2 tl.to calls, simple numeric selectors (macos-notification)
11+
* moderate — 6 tl.to calls, multiple selectors (yt-lower-third)
12+
* complex — stagger, chained .from()/.to(), const/defaults (vpn-youtube-spot)
13+
*/
14+
import { beforeAll, describe, expect, it } from "vitest";
15+
import { join } from "node:path";
16+
import { fileURLToPath } from "node:url";
17+
import { parseGsapScript, serializeGsapAnimations } from "./gsapParser.js";
18+
19+
const __goldens__ = join(fileURLToPath(import.meta.url), "..", "__goldens__");
20+
const g = (name: string) => join(__goldens__, name);
21+
22+
// ---------------------------------------------------------------------------
23+
// Corpus scripts (inline so goldens are not coupled to registry file changes)
24+
// ---------------------------------------------------------------------------
25+
26+
const MINIMAL_SCRIPT = `\
27+
var tl = gsap.timeline({ paused: true });
28+
var notification = document.getElementById("notification");
29+
gsap.set(notification, { x: 420, opacity: 0 });
30+
tl.to(notification, { x: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.2);
31+
tl.to(notification, { x: 420, opacity: 0, duration: 0.3, ease: "power3.in" }, 4.2);
32+
window.__timelines["macos-notification"] = tl;`;
33+
34+
const MODERATE_SCRIPT = `\
35+
window.__timelines = window.__timelines || {};
36+
var tl = gsap.timeline({ paused: true });
37+
var card = document.getElementById("card");
38+
var btn = document.getElementById("subscribe-btn");
39+
var textSub = document.getElementById("btn-subscribe");
40+
var textSubd = document.getElementById("btn-subscribed");
41+
gsap.set(card, { y: 300, opacity: 0 });
42+
tl.to(card, { y: 0, opacity: 1, duration: 0.5, ease: "power3.out" }, 0.1);
43+
tl.to(btn, { scale: 0.92, duration: 0.15, ease: "power2.out" }, 1.0);
44+
tl.to(btn, { scale: 1, duration: 0.4, ease: "elastic.out(1, 0.4)" }, 1.15);
45+
tl.to(textSub, { opacity: 0, duration: 0.08, ease: "none" }, 1.15);
46+
tl.to(textSubd, { opacity: 1, duration: 0.08, ease: "none" }, 1.18);
47+
tl.to(card, { y: 300, opacity: 0, duration: 0.25, ease: "power3.in" }, 3.8);
48+
window.__timelines["yt-lower-third"] = tl;`;
49+
50+
const COMPLEX_SCRIPT = `\
51+
window.__timelines = window.__timelines || {};
52+
gsap.defaults({ force3D: true });
53+
const tl = gsap.timeline({ paused: true, defaults: { duration: 0.45, ease: "power3.out" } });
54+
const breatheRepeats = Math.ceil(7 / 2.4) - 1;
55+
tl.from(".headline span", { y: 46, opacity: 0, stagger: 0.055, duration: 0.38, ease: "back.out(1.35)" }, 0.05)
56+
.from(".headline .sub", { y: 20, opacity: 0, duration: 0.28 }, 0.2)
57+
.from(".ambient-word", { scale: 0.92, opacity: 0, duration: 0.5 }, 0.08)
58+
.from(".ambient-line", { scaleX: 0, opacity: 0, stagger: 0.08, duration: 0.42 }, 0.16);
59+
window.__timelines["vpn-youtube-spot"] = tl;`;
60+
61+
// ---------------------------------------------------------------------------
62+
// Helpers
63+
// ---------------------------------------------------------------------------
64+
65+
function parseAndSerialize(script: string): { parsed: string; serialized: string } {
66+
const result = parseGsapScript(script);
67+
const serialized = serializeGsapAnimations(result.animations, result.timelineVar, {
68+
preamble: result.preamble,
69+
postamble: result.postamble,
70+
});
71+
return { parsed: JSON.stringify(result, null, 2), serialized };
72+
}
73+
74+
// ---------------------------------------------------------------------------
75+
// Golden tests
76+
// ---------------------------------------------------------------------------
77+
78+
describe("T6a — GSAP parser golden tests (Recast/Babel baseline)", () => {
79+
describe("minimal — 2 tl.to calls (macos-notification)", () => {
80+
let parsed: string;
81+
let serialized: string;
82+
beforeAll(() => {
83+
({ parsed, serialized } = parseAndSerialize(MINIMAL_SCRIPT));
84+
});
85+
86+
it("parseGsapScript output matches golden", async () => {
87+
await expect(parsed).toMatchFileSnapshot(g("minimal.parsed.json"));
88+
});
89+
90+
it("serializeGsapAnimations output matches golden", async () => {
91+
await expect(serialized).toMatchFileSnapshot(g("minimal.serialized.js"));
92+
});
93+
});
94+
95+
describe("moderate — 6 tl.to calls, multiple selectors (yt-lower-third)", () => {
96+
let parsed: string;
97+
let serialized: string;
98+
beforeAll(() => {
99+
({ parsed, serialized } = parseAndSerialize(MODERATE_SCRIPT));
100+
});
101+
102+
it("parseGsapScript output matches golden", async () => {
103+
await expect(parsed).toMatchFileSnapshot(g("moderate.parsed.json"));
104+
});
105+
106+
it("serializeGsapAnimations output matches golden", async () => {
107+
await expect(serialized).toMatchFileSnapshot(g("moderate.serialized.js"));
108+
});
109+
});
110+
111+
describe("complex — stagger + chained .from() calls (vpn-youtube-spot)", () => {
112+
let parsed: string;
113+
let serialized: string;
114+
beforeAll(() => {
115+
({ parsed, serialized } = parseAndSerialize(COMPLEX_SCRIPT));
116+
});
117+
118+
it("parseGsapScript output matches golden", async () => {
119+
await expect(parsed).toMatchFileSnapshot(g("complex.parsed.json"));
120+
});
121+
122+
it("serializeGsapAnimations output matches golden", async () => {
123+
await expect(serialized).toMatchFileSnapshot(g("complex.serialized.js"));
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)