Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
// Keyframe UI components — wired dynamically via EaseCurveSection/MotionPanel.
"packages/studio/src/components/editor/KeyframeDiamond.tsx",
"packages/studio/src/components/editor/SpringEaseEditor.tsx",
// NLE notice — rendered conditionally via NLELayout when timeline is first shown.
"packages/studio/src/components/nle/TimelineEditorNotice.tsx",
// Zoom hook extracted for downstream razor-blade PRs (#1330, #1331).
"packages/studio/src/player/components/useTimelineZoom.ts",
],
"ignorePatterns": [
"docs/**",
Expand Down Expand Up @@ -113,6 +117,27 @@
"createFailedCaptureCalibrationEstimate",
],
},
// Shared test helpers consumed by gsapParser.test.ts (same file,
// fallow doesn't trace intra-file test consumption).
{
"file": "packages/core/src/parsers/gsapParser.test-helpers.ts",
"exports": [
"expectKeyframe",
"expectKeyframesFormat",
"convertAndReparse",
"parseSplitAndAssert",
],
},
// Shared timeline components extracted for downstream PRs in the
// razor-blade stack (#1330, #1331). Consumers live on those branches.
{
"file": "packages/studio/src/player/components/timelineCallbacks.ts",
"exports": ["*"],
},
{
"file": "packages/studio/src/utils/timelineElementSplit.ts",
"exports": ["buildPatchTarget", "readFileContent"],
},
],
"ignoreDependencies": [
// Runtime/dynamic deps not visible to static analysis: tsup `external`,
Expand Down
135 changes: 58 additions & 77 deletions packages/core/src/parsers/gsapParser.stress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import {
removeAnimationFromScript,
} from "./gsapParser.js";
import type { ParsedGsap } from "./gsapParser.js";
import {
parseAndSerialize,
parseSingleAnimation,
expectStaggerRaw,
expectRawWithResolvable,
expectSingleAnimPosition,
} from "./gsapParser.test-helpers.js";

// ── Helpers ────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -232,16 +239,15 @@ describe("3. Extreme values", () => {
});

it("Infinity literal", () => {
const script = `
expectRawWithResolvable(
`
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: Infinity, y: 50, duration: 1 }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// Infinity is an Identifier, not a NumericLiteral — should be __raw
const xVal = result.animations[0].properties.x;
expect(typeof xVal === "string" && xVal.startsWith("__raw:")).toBe(true);
expect(result.animations[0].properties.y).toBe(50);
`,
"x",
"y",
50,
);
});
});

Expand Down Expand Up @@ -298,28 +304,16 @@ describe("5. Deeply nested objects", () => {
const tl = gsap.timeline({ paused: true });
tl.to(".items", { opacity: 1, duration: 0.5, stagger: { amount: 1, grid: [3, 3], from: "center", axis: "x" } }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
expect(result.animations[0].extras).toBeDefined();
expect(result.animations[0].extras!.stagger).toBeDefined();
// stagger should be __raw: containing the nested object source
const stagger = String(result.animations[0].extras!.stagger);
expect(stagger.startsWith("__raw:")).toBe(true);
expect(stagger).toContain("amount");
expect(stagger).toContain("grid");
expect(stagger).toContain("center");
const anim = parseSingleAnimation(script);
expectStaggerRaw(anim, "amount", "grid", "center");
});

it("complex stagger survives round-trip serialization", () => {
const script = `
const tl = gsap.timeline({ paused: true });
tl.to(".items", { opacity: 1, duration: 0.5, stagger: { amount: 1, grid: [3, 3], from: "center", axis: "x" } }, 0);
`;
const parsed = parseGsapScript(script);
const serialized = serializeGsapAnimations(parsed.animations, parsed.timelineVar, {
preamble: parsed.preamble,
postamble: parsed.postamble,
});
const { serialized } = parseAndSerialize(script);
expect(serialized).toContain("stagger:");
expect(serialized).toContain("amount");
expect(serialized).toContain("grid");
Expand Down Expand Up @@ -380,17 +374,16 @@ describe("7. Template literals in values", () => {
});

it("template literal with expression becomes __raw", () => {
const script = `
expectRawWithResolvable(
`
const val = 100;
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: \`\${val}px\`, y: 50, duration: 1 }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// Template literal with expressions is not resolvable
const xVal = result.animations[0].properties.x;
expect(typeof xVal === "string" && xVal.startsWith("__raw:")).toBe(true);
expect(result.animations[0].properties.y).toBe(50);
`,
"x",
"y",
50,
);
});
});

Expand Down Expand Up @@ -472,40 +465,32 @@ describe("9. Comments everywhere", () => {

describe("10. Arrow functions as values", () => {
it("arrow function property becomes __raw", () => {
const script = `
expectRawWithResolvable(
`
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: (i) => i * 50, opacity: 1, duration: 1 }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// Arrow function is not resolvable
const xVal = result.animations[0].properties.x;
expect(typeof xVal === "string" && xVal.startsWith("__raw:")).toBe(true);
// Resolvable values still work
expect(result.animations[0].properties.opacity).toBe(1);
`,
"x",
"opacity",
1,
);
});

it("arrow function in stagger becomes __raw extra", () => {
const script = `
const tl = gsap.timeline({ paused: true });
tl.to(".items", { opacity: 1, duration: 0.5, stagger: (i) => i * 0.1 }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations[0].extras).toBeDefined();
const stagger = String(result.animations[0].extras!.stagger);
expect(stagger.startsWith("__raw:")).toBe(true);
const anim = parseSingleAnimation(script);
expectStaggerRaw(anim);
});

it("arrow function round-trips via serialization", () => {
const script = `
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: (i) => i * 50, opacity: 1, duration: 1 }, 0);
`;
const parsed = parseGsapScript(script);
const serialized = serializeGsapAnimations(parsed.animations, parsed.timelineVar, {
preamble: parsed.preamble,
postamble: parsed.postamble,
});
const { serialized } = parseAndSerialize(script);
// The raw arrow function should be emitted without quotes
expect(serialized).toContain("(i) => i * 50");
expect(serialized).not.toContain('"(i) => i * 50"');
Expand Down Expand Up @@ -534,28 +519,26 @@ describe("11. Spread operator", () => {

describe("12. Conditional expressions", () => {
it("ternary expression becomes __raw", () => {
const script = `
expectRawWithResolvable(
`
const condition = true;
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: condition ? 100 : 200, y: 50, duration: 1 }, 0);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// ConditionalExpression is not handled by resolveNode
const xVal = result.animations[0].properties.x;
expect(typeof xVal === "string" && xVal.startsWith("__raw:")).toBe(true);
expect(result.animations[0].properties.y).toBe(50);
`,
"x",
"y",
50,
);
});

it("conditional in position argument defaults to 0", () => {
const script = `
expectSingleAnimPosition(
`
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: 100, duration: 1 }, someCondition ? 0 : 2);
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// Position can't be resolved — falls back to 0
expect(result.animations[0].position).toBe(0);
`,
0,
);
});
});

Expand Down Expand Up @@ -930,17 +913,16 @@ describe("Additional edge cases", () => {
});

it("scope resolution: binary expression with one unresolvable side", () => {
const script = `
expectRawWithResolvable(
`
const BASE = 100;
const tl = gsap.timeline({ paused: true });
tl.to("#el", { x: BASE + unknownVar, y: BASE * 2, duration: 1 }, 0);
`;
const result = parseGsapScript(script);
// BASE + unknownVar: left is 100, right is undefined => result is undefined => __raw
const xVal = result.animations[0].properties.x;
expect(typeof xVal === "string" && xVal.startsWith("__raw:")).toBe(true);
// BASE * 2: both resolved => 200
expect(result.animations[0].properties.y).toBe(200);
`,
"x",
"y",
200,
);
});

it("negative position in ID generation", () => {
Expand All @@ -954,13 +936,12 @@ describe("Additional edge cases", () => {
});

it("fromTo with no position arg defaults to 0", () => {
const script = `
expectSingleAnimPosition(
`
const tl = gsap.timeline({ paused: true });
tl.fromTo("#el", { opacity: 0 }, { opacity: 1, duration: 1 });
`;
const result = parseGsapScript(script);
expect(result.animations).toHaveLength(1);
// For fromTo, position is args[3] which is undefined => defaults to 0
expect(result.animations[0].position).toBe(0);
`,
0,
);
});
});
Loading
Loading