Files
glips--figma-context-mcp/src/tests/tree-walker.test.ts
T
wehub-resource-sync 588cfc5bb3
Release / release-please (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:31 +08:00

752 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import { extractFromDesign } from "~/extractors/node-walker.js";
import { allExtractors, collapseSvgContainers } from "~/extractors/built-in.js";
import { simplifyRawFigmaObject } from "~/extractors/design-extractor.js";
import type { GetFileResponse, Style } from "@figma/rest-api-spec";
import type { Node as FigmaNode } from "@figma/rest-api-spec";
// Minimal Figma node factory — only the fields the walker actually reads.
// The Figma types are deeply discriminated unions; we cast through unknown
// because tests only need the subset of fields the walker touches.
function makeNode(overrides: Record<string, unknown>): FigmaNode {
return { visible: true, ...overrides } as unknown as FigmaNode;
}
// A small but representative node tree:
// Page
// ├── Frame "Header" (visible)
// │ ├── Text "Title"
// │ └── Rectangle "Bg" (invisible)
// ├── Frame "Body"
// │ └── Frame "Card"
// │ └── Text "Label"
// └── Vector "Icon" (becomes IMAGE-SVG)
const fixtureNodes: FigmaNode[] = [
makeNode({
id: "1:1",
name: "Header",
type: "FRAME",
children: [
makeNode({ id: "1:2", name: "Title", type: "TEXT", characters: "Hello" }),
makeNode({ id: "1:3", name: "Bg", type: "RECTANGLE", visible: false }),
],
}),
makeNode({
id: "2:1",
name: "Body",
type: "FRAME",
children: [
makeNode({
id: "2:2",
name: "Card",
type: "FRAME",
children: [makeNode({ id: "2:3", name: "Label", type: "TEXT", characters: "World" })],
}),
],
}),
makeNode({ id: "3:1", name: "Icon", type: "VECTOR" }),
];
describe("extractFromDesign", () => {
it("produces correct node structure from a nested tree", async () => {
const { nodes } = await extractFromDesign(fixtureNodes, allExtractors);
// Top-level: Header, Body, Icon (3 nodes — Bg is invisible, filtered out)
expect(nodes).toHaveLength(3);
expect(nodes.map((n) => n.name)).toEqual(["Header", "Body", "Icon"]);
// Header has 1 child (Title only — Bg is invisible)
const header = nodes[0];
expect(header.children).toHaveLength(1);
expect(header.children![0].name).toBe("Title");
expect(header.children![0].text).toBe("Hello");
// Body > Card > Label
const body = nodes[1];
expect(body.children).toHaveLength(1);
expect(body.children![0].name).toBe("Card");
expect(body.children![0].children).toHaveLength(1);
expect(body.children![0].children![0].name).toBe("Label");
expect(body.children![0].children![0].text).toBe("World");
// Vector becomes IMAGE-SVG
const icon = nodes[2];
expect(icon.type).toBe("IMAGE-SVG");
expect(icon.children).toBeUndefined();
});
it("respects maxDepth option", async () => {
const { nodes } = await extractFromDesign(fixtureNodes, allExtractors, { maxDepth: 1 });
// At depth 0 we get top-level nodes, depth 1 gets their direct children, no deeper
const header = nodes.find((n) => n.name === "Header")!;
expect(header.children).toHaveLength(1);
expect(header.children![0].name).toBe("Title");
// Body's child "Card" is at depth 1 — it should exist but have no children
const body = nodes.find((n) => n.name === "Body")!;
expect(body.children).toHaveLength(1);
expect(body.children![0].name).toBe("Card");
expect(body.children![0].children).toBeUndefined();
});
it("accumulates global style variables across nodes", async () => {
const styledNode = makeNode({
id: "4:1",
name: "Styled",
type: "FRAME",
fills: [{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: true }],
});
const { globalVars } = await extractFromDesign([styledNode], allExtractors);
// The fill should be extracted into a global variable
expect(Object.keys(globalVars.styles).length).toBeGreaterThan(0);
});
it("deduplicates identical styles across nodes into a single global variable", async () => {
const sharedFill = [{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: true }];
const nodeA = makeNode({ id: "5:1", name: "A", type: "FRAME", fills: sharedFill });
const nodeB = makeNode({ id: "5:2", name: "B", type: "FRAME", fills: sharedFill });
const { nodes, globalVars } = await extractFromDesign([nodeA, nodeB], allExtractors);
// Both nodes should reference the same fill variable
expect(nodes[0].fills).toBeDefined();
expect(nodes[0].fills).toBe(nodes[1].fills);
// Only one fill entry should exist in globalVars
const fillEntries = Object.entries(globalVars.styles).filter(([key]) => key.startsWith("fill"));
expect(fillEntries).toHaveLength(1);
});
it("deduplicates identical colors used as both fill and stroke", async () => {
const sharedColor = [{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: true }];
// Stroke node first — if strokes used a different prefix, the var would
// be named stroke_* and the fill would reuse it under the wrong prefix.
const strokeNode = makeNode({
id: "8:1",
name: "A",
type: "FRAME",
strokes: sharedColor,
strokeWeight: 1,
});
const fillNode = makeNode({ id: "8:2", name: "B", type: "FRAME", fills: sharedColor });
const { nodes, globalVars } = await extractFromDesign([strokeNode, fillNode], allExtractors);
expect(nodes[0].strokes).toBeDefined();
expect(nodes[1].fills).toBeDefined();
expect(nodes[0].strokes).toBe(nodes[1].fills);
// The shared var should use the fill prefix since stroke colors are
// structurally identical to fill colors in Figma (both are FILL-type styles).
const colorEntries = Object.entries(globalVars.styles).filter(
([, value]) => JSON.stringify(value) === JSON.stringify(["#FF0000"]),
);
expect(colorEntries).toHaveLength(1);
expect(colorEntries[0][0]).toMatch(/^fill_/);
});
it("preserves non-default stroke alignment on the simplified node", async () => {
const node = makeNode({
id: "9:1",
name: "Card",
type: "FRAME",
strokes: [{ type: "SOLID", color: { r: 0.89, g: 0.9, b: 0.9, a: 1 }, visible: true }],
strokeWeight: 2,
strokeAlign: "OUTSIDE",
});
const { nodes } = await extractFromDesign([node], allExtractors);
expect(nodes[0].strokeAlign).toBe("OUTSIDE");
expect(nodes[0].strokeWeight).toBe("2px");
});
it("omits INSIDE stroke alignment, the CSS-border default consumers assume", async () => {
const node = makeNode({
id: "9:2",
name: "Card",
type: "FRAME",
strokes: [{ type: "SOLID", color: { r: 0.89, g: 0.9, b: 0.9, a: 1 }, visible: true }],
strokeWeight: 2,
strokeAlign: "INSIDE",
});
const { nodes } = await extractFromDesign([node], allExtractors);
expect(nodes[0].strokeAlign).toBeUndefined();
expect(nodes[0].strokeWeight).toBe("2px");
});
it("disambiguates named styles when style names collide", async () => {
const nodeA = makeNode({
id: "7:1",
name: "Text A",
type: "TEXT",
characters: "Hello",
style: { fontFamily: "Inter", fontWeight: 400, fontSize: 12 },
styles: { text: "13:77" },
});
const nodeB = makeNode({
id: "7:2",
name: "Text B",
type: "TEXT",
characters: "World",
style: { fontFamily: "Inter", fontWeight: 600, fontSize: 14 },
styles: { text: "161:300" },
});
const extraStyles: Record<string, Style> = {
"13:77": { name: "Heading / Large" } as Style,
"161:300": { name: "Heading / Large" } as Style,
};
const { nodes, globalVars: resultVars } = await extractFromDesign(
[nodeA, nodeB],
allExtractors,
{},
{ styles: {} },
extraStyles,
);
expect(nodes[0].textStyle).toBe("Heading / Large");
expect(nodes[1].textStyle).toBe("Heading / Large (161:300)");
const styleKeys = Object.keys(resultVars.styles).filter((key) =>
key.startsWith("Heading / Large"),
);
expect(styleKeys).toHaveLength(2);
});
});
describe("fill flattening", () => {
// Resolve a node's registered fills var back to its concrete value.
type Extracted = Awaited<ReturnType<typeof extractFromDesign>>;
function fillsValue(nodes: Extracted["nodes"], globalVars: Extracted["globalVars"]) {
return globalVars.styles[nodes[0].fills as string];
}
// Figma orders the fills array bottom-first, so index 0 is the backdrop and
// the last entry is the topmost layer.
it("composites an all-solid stack into a single resolved color", async () => {
const node = makeNode({
id: "f:1",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 1, g: 1, b: 1, a: 1 }, visible: true }, // white backdrop
{ type: "SOLID", color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 0.2, visible: true }, // black @ 20%
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
expect(fillsValue(nodes, globalVars)).toEqual(["#CCCCCC"]);
});
it("culls layers fully occluded by an opaque paint above them", async () => {
const node = makeNode({
id: "f:2",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 0, g: 0, b: 1, a: 1 }, visible: true }, // blue backdrop
{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: true }, // opaque red on top
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
// Only the opaque top color survives; the blue beneath contributes nothing.
expect(fillsValue(nodes, globalVars)).toEqual(["#FF0000"]);
});
it("folds both color.a and paint.opacity into the effective alpha", async () => {
const node = makeNode({
id: "f:6",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 1, g: 1, b: 1, a: 1 }, visible: true }, // white backdrop
// black at color.a 0.5 × opacity 0.5 = 0.25 effective → 0.75 of white shows through
{ type: "SOLID", color: { r: 0, g: 0, b: 0, a: 0.5 }, opacity: 0.5, visible: true },
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
expect(fillsValue(nodes, globalVars)).toEqual(["#BFBFBF"]);
});
it("culls everything below a fully-opaque mid-stack paint, compositing only what's above", async () => {
const node = makeNode({
id: "f:7",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 1, g: 0, b: 0, a: 1 }, visible: true }, // red (culled)
{ type: "SOLID", color: { r: 0, g: 1, b: 0, a: 1 }, visible: true }, // opaque green
{ type: "SOLID", color: { r: 0, g: 0, b: 1, a: 1 }, opacity: 0.5, visible: true }, // blue @ 50% on top
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
// Red contributes nothing (opaque green above it); blue@50% blends over green → teal.
expect(fillsValue(nodes, globalVars)).toEqual(["#008080"]);
});
it("treats PASS_THROUGH blend as flattenable", async () => {
const node = makeNode({
id: "f:8",
name: "Swatch",
type: "FRAME",
fills: [
{
type: "SOLID",
color: { r: 1, g: 1, b: 1, a: 1 },
blendMode: "PASS_THROUGH",
visible: true,
},
{
type: "SOLID",
color: { r: 0, g: 0, b: 0, a: 1 },
opacity: 0.2,
blendMode: "PASS_THROUGH",
visible: true,
},
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
expect(fillsValue(nodes, globalVars)).toEqual(["#CCCCCC"]);
});
it("emits rgba() when the composited stack is still translucent", async () => {
const node = makeNode({
id: "f:3",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 1, g: 1, b: 1, a: 0.5 }, visible: true },
{ type: "SOLID", color: { r: 0, g: 0, b: 0, a: 0.5 }, visible: true },
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
expect(fillsValue(nodes, globalVars)).toEqual(["rgba(85, 85, 85, 0.75)"]);
});
it("leaves a stack untouched when it contains a gradient", async () => {
const node = makeNode({
id: "f:4",
name: "Swatch",
type: "FRAME",
fills: [
{
type: "GRADIENT_LINEAR",
visible: true,
gradientHandlePositions: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
{ x: 0, y: 1 },
],
gradientStops: [
{ position: 0, color: { r: 1, g: 0, b: 0, a: 1 } },
{ position: 1, color: { r: 0, g: 0, b: 1, a: 1 } },
],
},
{ type: "SOLID", color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 0.2, visible: true },
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
// Both layers survive, reversed into CSS top-first order: solid first, gradient last.
const value = fillsValue(nodes, globalVars) as unknown[];
expect(value).toHaveLength(2);
expect(value[0]).toBe("rgba(0, 0, 0, 0.2)");
expect((value[1] as { type: string }).type).toBe("GRADIENT_LINEAR");
});
it("leaves a stack untouched when any solid uses a non-normal blend mode", async () => {
const node = makeNode({
id: "f:5",
name: "Swatch",
type: "FRAME",
fills: [
{ type: "SOLID", color: { r: 1, g: 1, b: 1, a: 1 }, visible: true },
{
type: "SOLID",
color: { r: 0, g: 0, b: 0, a: 0.2 },
blendMode: "MULTIPLY",
visible: true,
},
],
});
const { nodes, globalVars } = await extractFromDesign([node], allExtractors);
expect(fillsValue(nodes, globalVars)).toHaveLength(2);
});
});
describe("collapseSvgContainers", () => {
it("collapses BOOLEAN_OPERATION nodes to IMAGE-SVG", async () => {
const booleanOpNode = makeNode({
id: "5:1",
name: "Combined Shape",
type: "BOOLEAN_OPERATION",
booleanOperation: "UNION",
children: [
makeNode({ id: "5:2", name: "Circle", type: "ELLIPSE" }),
makeNode({ id: "5:3", name: "Square", type: "RECTANGLE" }),
],
});
const { nodes } = await extractFromDesign([booleanOpNode], allExtractors, {
afterChildren: collapseSvgContainers,
});
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});
it("collapses a frame containing a BOOLEAN_OPERATION to IMAGE-SVG", async () => {
const frameWithBoolOp = makeNode({
id: "6:1",
name: "Icon Frame",
type: "FRAME",
children: [
makeNode({
id: "6:2",
name: "Union",
type: "BOOLEAN_OPERATION",
booleanOperation: "UNION",
children: [
makeNode({ id: "6:3", name: "A", type: "RECTANGLE" }),
makeNode({ id: "6:4", name: "B", type: "ELLIPSE" }),
],
}),
],
});
const { nodes } = await extractFromDesign([frameWithBoolOp], allExtractors, {
afterChildren: collapseSvgContainers,
});
// The BOOLEAN_OPERATION collapses to IMAGE-SVG first (bottom-up),
// then the FRAME sees all children are SVG-eligible and collapses too.
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});
// Auto-layout signals authored structure — the spacing between children is
// intentional, so we should preserve the container even when its children
// are all SVG-eligible (e.g., bar charts, button rows, layout test frames).
it("does not collapse an auto-layout frame whose children are all SVG-eligible", async () => {
const autoLayoutRow = makeNode({
id: "7:1",
name: "Bar Chart",
type: "FRAME",
clipsContent: false,
layoutMode: "HORIZONTAL",
itemSpacing: 8,
children: [
makeNode({ id: "7:2", name: "Bar 1", type: "RECTANGLE" }),
makeNode({ id: "7:3", name: "Bar 2", type: "RECTANGLE" }),
makeNode({ id: "7:4", name: "Bar 3", type: "RECTANGLE" }),
],
});
const { nodes } = await extractFromDesign([autoLayoutRow], allExtractors, {
afterChildren: collapseSvgContainers,
});
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("FRAME");
expect(nodes[0].children).toHaveLength(3);
});
// Escape hatch for decorative patterns: enough leaf primitives that the
// payload cost outweighs the structural value (e.g., dotted backgrounds
// built from grids of ellipses).
it("collapses an auto-layout frame with many SVG-eligible children", async () => {
const dotRow = makeNode({
id: "8:1",
name: "Dot Row",
type: "FRAME",
clipsContent: false,
layoutMode: "HORIZONTAL",
itemSpacing: 4,
children: Array.from({ length: 20 }, (_, i) =>
makeNode({ id: `8:${i + 2}`, name: `Dot ${i}`, type: "ELLIPSE" }),
),
});
const { nodes } = await extractFromDesign([dotRow], allExtractors, {
afterChildren: collapseSvgContainers,
});
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});
// Non-auto-layout container with shape children is the original target case:
// hand-drawn icons made of vector primitives. Must keep collapsing.
it("still collapses a non-auto-layout frame whose children are all SVG-eligible", async () => {
const iconFrame = makeNode({
id: "9:1",
name: "Icon",
type: "FRAME",
clipsContent: false,
children: [
makeNode({ id: "9:2", name: "Circle", type: "ELLIPSE" }),
makeNode({ id: "9:3", name: "Rect", type: "RECTANGLE" }),
],
});
const { nodes } = await extractFromDesign([iconFrame], allExtractors, {
afterChildren: collapseSvgContainers,
});
expect(nodes).toHaveLength(1);
expect(nodes[0].type).toBe("IMAGE-SVG");
expect(nodes[0].children).toBeUndefined();
});
});
describe("component property support", () => {
it("rescues hidden nodes with componentPropertyReferences.visible inside components", async () => {
const componentNode = makeNode({
id: "10:1",
name: "Card",
type: "COMPONENT",
children: [
makeNode({ id: "10:2", name: "Title", type: "TEXT", characters: "Card Title" }),
makeNode({
id: "10:3",
name: "Badge",
type: "FRAME",
visible: false,
componentPropertyReferences: { visible: "Show Badge#341:0" },
children: [makeNode({ id: "10:4", name: "Badge Text", type: "TEXT", characters: "NEW" })],
}),
],
});
const { nodes } = await extractFromDesign([componentNode], allExtractors);
const card = nodes[0];
expect(card.children).toHaveLength(2);
const badge = card.children!.find((c) => c.name === "Badge")!;
expect(badge).toBeDefined();
expect(badge.componentPropertyReferences).toEqual({ visible: "Show Badge" });
});
it("strips hidden nodes normally inside instances", async () => {
const instanceNode = makeNode({
id: "11:1",
name: "Card Instance",
type: "INSTANCE",
componentId: "10:1",
componentProperties: {
"Show Badge": { type: "BOOLEAN", value: false },
},
children: [
makeNode({ id: "11:2", name: "Title", type: "TEXT", characters: "My Card" }),
makeNode({ id: "11:3", name: "Badge", type: "FRAME", visible: false }),
],
});
const { nodes } = await extractFromDesign([instanceNode], allExtractors);
const instance = nodes[0];
expect(instance.children).toHaveLength(1);
expect(instance.children![0].name).toBe("Title");
});
it("collects componentPropertyDefinitions during traversal", async () => {
const componentNode = makeNode({
id: "12:1",
name: "Product Card",
type: "COMPONENT",
componentPropertyDefinitions: {
"On Sale#341:0": { type: "BOOLEAN", defaultValue: true },
"Title#341:1": { type: "TEXT", defaultValue: "Product Name" },
"Icon#341:2": { type: "INSTANCE_SWAP", defaultValue: "999:1" },
},
children: [makeNode({ id: "12:2", name: "Title", type: "TEXT", characters: "Product Name" })],
});
const { traversalState } = await extractFromDesign([componentNode], allExtractors);
expect(traversalState.componentPropertyDefinitions["12:1"]).toEqual({
"On Sale": { type: "boolean", defaultValue: true },
Title: { type: "text", defaultValue: "Product Name" },
});
expect(traversalState.componentPropertyDefinitions["12:1"]).not.toHaveProperty("Icon");
});
it("annotates componentPropertyReferences with characters→text rename", async () => {
const componentNode = makeNode({
id: "13:1",
name: "Button",
type: "COMPONENT",
children: [
makeNode({
id: "13:2",
name: "Label",
type: "TEXT",
characters: "Click me",
componentPropertyReferences: { characters: "Button Label#100:0" },
}),
],
});
const { nodes } = await extractFromDesign([componentNode], allExtractors);
const label = nodes[0].children![0];
expect(label.componentPropertyReferences).toEqual({ text: "Button Label" });
});
it("simplifies instance componentProperties to Record format", async () => {
const instanceNode = makeNode({
id: "14:1",
name: "Card Instance",
type: "INSTANCE",
componentId: "10:1",
componentProperties: {
"On Sale": { type: "BOOLEAN", value: true },
Title: { type: "TEXT", value: "My Product" },
},
children: [makeNode({ id: "14:2", name: "Content", type: "FRAME" })],
});
const { nodes } = await extractFromDesign([instanceNode], allExtractors);
expect(nodes[0].componentProperties).toEqual({
"On Sale": true,
Title: "My Product",
});
});
it("strips hidden children inside nested instances within components", async () => {
const componentNode = makeNode({
id: "15:1",
name: "Wrapper",
type: "COMPONENT",
children: [
makeNode({
id: "15:2",
name: "Nested Instance",
type: "INSTANCE",
componentId: "99:1",
children: [
makeNode({ id: "15:3", name: "Visible Child", type: "FRAME" }),
makeNode({ id: "15:4", name: "Hidden Child", type: "FRAME", visible: false }),
],
}),
],
});
const { nodes } = await extractFromDesign([componentNode], allExtractors);
const nestedInstance = nodes[0].children![0];
expect(nestedInstance).toBeDefined();
expect(nestedInstance.name).toBe("Nested Instance");
expect(nestedInstance.children).toHaveLength(1);
expect(nestedInstance.children![0].name).toBe("Visible Child");
});
});
describe("simplifyRawFigmaObject", () => {
it("produces a complete SimplifiedDesign from a mock API response", async () => {
const mockResponse = {
name: "Test File",
document: {
id: "0:0",
name: "Document",
type: "DOCUMENT",
children: fixtureNodes,
visible: true,
},
components: {},
componentSets: {},
styles: {},
schemaVersion: 0,
version: "1",
role: "owner",
lastModified: "2024-01-01",
thumbnailUrl: "",
editorType: "figma",
} as unknown as GetFileResponse;
const result = await simplifyRawFigmaObject(mockResponse, allExtractors, {
afterChildren: collapseSvgContainers,
});
expect(result.name).toBe("Test File");
expect(result.nodes).toHaveLength(3);
expect(result.nodes.map((n) => n.name)).toEqual(["Header", "Body", "Icon"]);
// Verify full depth traversal happened
const label = result.nodes[1].children![0].children![0];
expect(label.name).toBe("Label");
expect(label.text).toBe("World");
});
it("flows property definitions from tree traversal into component metadata", async () => {
const componentNode = makeNode({
id: "20:1",
name: "Product Card",
type: "COMPONENT",
componentPropertyDefinitions: {
"On Sale#341:0": { type: "BOOLEAN", defaultValue: true },
"Title#341:1": { type: "TEXT", defaultValue: "Product Name" },
},
children: [makeNode({ id: "20:2", name: "Content", type: "FRAME" })],
});
const mockResponse = {
name: "Test File",
document: {
id: "0:0",
name: "Document",
type: "DOCUMENT",
children: [componentNode],
visible: true,
},
components: {
"20:1": { key: "abc123", name: "Product Card", componentSetId: undefined },
},
componentSets: {},
styles: {},
schemaVersion: 0,
version: "1",
role: "owner",
lastModified: "2024-01-01",
thumbnailUrl: "",
editorType: "figma",
} as unknown as GetFileResponse;
const result = await simplifyRawFigmaObject(mockResponse, allExtractors);
expect(result.components["20:1"].propertyDefinitions).toEqual({
"On Sale": { type: "boolean", defaultValue: true },
Title: { type: "text", defaultValue: "Product Name" },
});
});
});