Files
hyperframes/packages/core/src/figma/motionToGsap.test.ts
T
Vance IngallsandClaude Fable 5 e92700acde feat(core): figma motion → GSAP translator + /figma skill v1 (#1869)
* feat(core): add figma motion easing mapping

* feat(core): translate figma motion doc to gsap timeline spec

* feat(core): emit paused GSAP timeline script from figma motion spec

* fix(core): restore type exports dropped from figma barrel in Task 8

* feat(skills): add /figma import skill + catalog wiring

Add the agent-facing /figma skill (asset + Figma Motion import via the
Figma MCP connector, built on @hyperframes/core/figma) and wire it into
the skill catalog across CLAUDE.md, README.md, docs/guides/skills.mdx,
and the hyperframes router's capability map. Bumps the skill count from
19 to 20 in CLAUDE.md and README.md.

* fix(core): use replaceAll for figma node-id dash-to-colon conversion

* style: format skills catalog tables

oxfmt-align the README and router SKILL.md tables after the /figma +
/hyperframes-keyframes merge left uneven column padding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): add missing cache fields to telemetry test fixture

ExtractionPhaseBreakdown gained cachePublishFailures/cacheGcEvictions/
cacheGcBytesFreed/cacheAgedPartialsCleared; the studioRenderTelemetry
test fixture was never updated, breaking Typecheck on main and every PR
based on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:45:07 -07:00

62 lines
2.1 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from "vitest";
import { motionToGsap } from "./motionToGsap";
import type { MotionDoc } from "./types";
const headline: MotionDoc = {
selector: "#hero-headline",
tracks: [
{
property: "opacity",
values: [0, 0, 1, 1, 0],
times: [0, 0.0686, 0.2273, 0.9999, 1],
ease: ["linear", [0.539, 0, 0.312, 0.995], "linear", [0.539, 0, 0.312, 0.995]],
duration: 2,
repeat: Infinity,
},
],
};
describe("motionToGsap", () => {
it("derives a finite, paused-timeline spec from the captured Headline payload", () => {
const spec = motionToGsap(headline);
expect(spec.timelineId).toBe("figma-hero-headline");
expect(spec.tweens).toHaveLength(1);
const t = spec.tweens[0];
expect(t?.selector).toBe("#hero-headline");
expect(t?.property).toBe("opacity");
expect(t?.initial).toBe(0);
// 4 segments for 5 keyframes
expect(t?.steps).toHaveLength(4);
// clamps Infinity -> 0 (single play) for determinism
expect(t?.repeat).toBe(0);
});
it("computes per-segment durations from times * duration", () => {
const t = motionToGsap(headline).tweens[0];
// segment 0: (0.0686 - 0) * 2 = 0.1372
expect(t?.steps[0]?.duration).toBeCloseTo(0.1372, 4);
// segment 1: (0.2273 - 0.0686) * 2 = 0.3174
expect(t?.steps[1]?.duration).toBeCloseTo(0.3174, 4);
});
it("registers a CustomEase per bezier segment and names it in the step", () => {
const spec = motionToGsap(headline);
expect(spec.customEases).toHaveLength(2);
expect(spec.customEases[0]?.bezier).toEqual([0.539, 0, 0.312, 0.995]);
// step 0 ease is linear -> none; step 1 ease is the first bezier
expect(spec.tweens[0]?.steps[0]?.ease).toBe("none");
expect(spec.tweens[0]?.steps[1]?.ease).toBe(spec.customEases[0]?.name);
});
it("throws when times and values lengths disagree", () => {
expect(() =>
motionToGsap({
selector: "#x",
tracks: [{ property: "x", values: [0, 1], times: [0], ease: ["linear"], duration: 1 }],
}),
).toThrow();
});
});