Files
hyperframes/packages/cli/src/commands/figma/tokens.test.ts
T
Vance IngallsandClaude Fable 5 4d60792adc feat(core,cli): figma tokens import with alias-aware binding records (M2) (#1871)
tokensToVariables: variables -> composition brand-variable entries
(COLOR->hex/rgba, FLOAT/STRING/BOOLEAN), alias chains walked cycle-safe
to the leaf value while the binding keeps the semantic id. Sidecar
figma-tokens.json + .media/figma-bindings.jsonl records per spec 7.1.

hyperframes figma tokens: variables path, REQUIRES_ENTERPRISE degrades
to published-styles metadata (values resolve at component time).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:16:48 -07:00

75 lines
2.8 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, afterEach, beforeEach } from "vitest";
import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runTokensImport } from "./tokens.js";
import { FigmaClientError, type FigmaClient } from "@hyperframes/core/figma";
let dir = "";
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "hf-figma-tokens-"));
});
afterEach(() => rmSync(dir, { recursive: true, force: true }));
function client(overrides: Partial<FigmaClient>): FigmaClient {
return {
renderNode: () => Promise.reject(new Error("unused")),
imageFills: () => Promise.resolve(new Map()),
variables: () =>
Promise.resolve({
variables: {
"VariableID:1:1": {
name: "Blue/500",
key: "kblue",
resolvedType: "COLOR",
valuesByMode: { m1: { r: 0, g: 0.4, b: 1, a: 1 } },
},
},
variableCollections: {},
}),
styles: () => Promise.resolve([{ key: "s1", name: "Primary", style_type: "FILL" }]),
nodeTree: () => Promise.reject(new Error("unused")),
fileVersion: () => Promise.resolve({ version: "7", lastModified: "2026-07-01" }),
...overrides,
};
}
describe("runTokensImport", () => {
it("imports variables: entries + sidecar + binding index", async () => {
const out = await runTokensImport("FILE", { projectDir: dir, client: client({}) });
expect(out.entries).toHaveLength(1);
expect(out.mode).toBe("variables");
const sidecar = JSON.parse(readFileSync(join(dir, "figma-tokens.json"), "utf8")) as {
tokens: unknown[];
};
expect(sidecar.tokens).toHaveLength(1);
const bindings = readFileSync(join(dir, ".media", "figma-bindings.jsonl"), "utf8");
expect(bindings).toContain('"figmaId":"VariableID:1:1"');
});
it("falls back to styles metadata when variables are enterprise-gated", async () => {
const gated = client({
variables: () =>
Promise.reject(new FigmaClientError("REQUIRES_ENTERPRISE", "enterprise only", 403)),
});
const out = await runTokensImport("FILE", { projectDir: dir, client: gated });
expect(out.mode).toBe("styles");
expect(out.entries).toEqual([]);
const sidecar = JSON.parse(readFileSync(join(dir, "figma-tokens.json"), "utf8")) as {
tokens: Array<{ name: string; type: string }>;
};
expect(sidecar.tokens[0]).toMatchObject({ name: "Primary", type: "style:FILL" });
});
it("propagates non-enterprise failures", async () => {
const broken = client({
variables: () => Promise.reject(new FigmaClientError("RATE_LIMITED", "429", 429)),
});
await expect(runTokensImport("FILE", { projectDir: dir, client: broken })).rejects.toThrow(
/429/,
);
expect(existsSync(join(dir, "figma-tokens.json"))).toBe(false);
});
});