import { describe, expect, test } from 'bun:test' import { ALL_TOOLS, FigmaAPI, SceneGraph, computeAllLayouts, parseFigFile } from '@open-pencil/core' describe('MCP tool execution', () => { function setup() { const graph = new SceneGraph() const api = new FigmaAPI(graph) return { graph, api } } function setupWithFile(data: ArrayBuffer) { return parseFigFile(data).then((graph) => { computeAllLayouts(graph) const api = new FigmaAPI(graph) return { graph, api } }) } function findTool(name: string) { const tool = ALL_TOOLS.find((t) => t.name === name) if (!tool) throw new Error(`Tool "${name}" not found`) return tool } test('all tools have name, description, and execute', () => { for (const tool of ALL_TOOLS) { expect(tool.name).toBeString() expect(tool.description).toBeString() expect(tool.execute).toBeFunction() } }) test('tool names are unique', () => { const names = ALL_TOOLS.map((t) => t.name) expect(new Set(names).size).toBe(names.length) }) test('get_page_tree returns page hierarchy', () => { const { api } = setup() const result = findTool('get_page_tree').execute(api, {}) as { page: string; children: unknown[] } expect(result.page).toBe('Page 1') expect(result.children).toBeArray() }) test('create_shape creates a frame', () => { const { api } = setup() const result = findTool('create_shape').execute(api, { type: 'FRAME', x: 10, y: 20, width: 300, height: 200, name: 'TestFrame' }) as { id: string; name: string; type: string } expect(result.type).toBe('FRAME') expect(result.name).toBe('TestFrame') const node = api.getNodeById(result.id) expect(node).not.toBeNull() expect(node!.x).toBe(10) expect(node!.y).toBe(20) expect(node!.width).toBe(300) expect(node!.height).toBe(200) }) test('set_fill applies color', () => { const { api } = setup() const frame = findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 0, y: 0, width: 100, height: 100 }) as { id: string } findTool('set_fill').execute(api, { id: frame.id, color: '#ff0000' }) const node = api.getNodeById(frame.id)! expect(node.fills).toHaveLength(1) expect(node.fills[0].color.r).toBeCloseTo(1) expect(node.fills[0].color.g).toBeCloseTo(0) expect(node.fills[0].color.b).toBeCloseTo(0) }) test('set_layout configures auto-layout', () => { const { api } = setup() const frame = findTool('create_shape').execute(api, { type: 'FRAME', x: 0, y: 0, width: 400, height: 300 }) as { id: string } findTool('set_layout').execute(api, { id: frame.id, direction: 'VERTICAL', spacing: 16, padding: 24 }) const node = api.getNodeById(frame.id)! expect(node.layoutMode).toBe('VERTICAL') expect(node.itemSpacing).toBe(16) expect(node.paddingTop).toBe(24) expect(node.paddingLeft).toBe(24) }) test('create_shape with parent_id nests correctly', () => { const { api } = setup() const parent = findTool('create_shape').execute(api, { type: 'FRAME', x: 0, y: 0, width: 400, height: 300, name: 'Parent' }) as { id: string } const child = findTool('create_shape').execute(api, { type: 'TEXT', x: 0, y: 0, width: 200, height: 30, name: 'Child', parent_id: parent.id }) as { id: string } const tree = findTool('get_page_tree').execute(api, {}) as { children: { id: string; children?: unknown[] }[] } const parentNode = tree.children.find((c: { id: string }) => c.id === parent.id) expect(parentNode).toBeDefined() expect(parentNode!.children).toBeArray() }) test('delete_node removes node', () => { const { api } = setup() const frame = findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 0, y: 0, width: 100, height: 100 }) as { id: string } findTool('delete_node').execute(api, { id: frame.id }) expect(api.getNodeById(frame.id)).toBeNull() }) test('find_nodes filters by type', () => { const { api } = setup() findTool('create_shape').execute(api, { type: 'FRAME', x: 0, y: 0, width: 100, height: 100, name: 'F1' }) findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 0, y: 0, width: 100, height: 100, name: 'R1' }) findTool('create_shape').execute(api, { type: 'FRAME', x: 0, y: 0, width: 100, height: 100, name: 'F2' }) const result = findTool('find_nodes').execute(api, { type: 'FRAME' }) as { count: number } expect(result.count).toBe(2) }) test('clone_node duplicates a node', () => { const { api } = setup() const frame = findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 10, y: 20, width: 100, height: 50 }) as { id: string } const clone = findTool('clone_node').execute(api, { id: frame.id }) as { id: string } expect(clone.id).not.toBe(frame.id) const clonedNode = api.getNodeById(clone.id)! expect(clonedNode.width).toBe(100) expect(clonedNode.height).toBe(50) }) test('update_node modifies properties', () => { const { api } = setup() const frame = findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 0, y: 0, width: 100, height: 100, name: 'Before' }) as { id: string } const result = findTool('update_node').execute(api, { id: frame.id, x: 50, opacity: 0.5, corner_radius: 8, name: 'After' }) as { updated: string[] } expect(result.updated).toContain('x') expect(result.updated).toContain('opacity') expect(result.updated).toContain('cornerRadius') expect(result.updated).toContain('name') const node = api.getNodeById(frame.id)! expect(node.x).toBe(50) expect(node.opacity).toBe(0.5) expect(node.cornerRadius).toBe(8) expect(node.name).toBe('After') }) test('list_pages returns all pages', () => { const { api } = setup() const result = findTool('list_pages').execute(api, {}) as { pages: { name: string }[] } expect(result.pages.length).toBeGreaterThanOrEqual(1) expect(result.pages[0].name).toBe('Page 1') }) test('error on missing node', () => { const { api } = setup() const result = findTool('get_node').execute(api, { id: 'nonexistent' }) as { error: string } expect(result.error).toContain('not found') }) test('open and query .fig file', async () => { const data = await Bun.file('tests/fixtures/gold-preview.fig').arrayBuffer() const { api } = await setupWithFile(data) const pages = findTool('list_pages').execute(api, {}) as { pages: { name: string }[] } expect(pages.pages.length).toBeGreaterThanOrEqual(1) const found = findTool('find_nodes').execute(api, { type: 'INSTANCE' }) as { count: number } expect(found.count).toBeGreaterThan(0) }) })