import { describe, expect, test } from 'bun:test' import { ALL_TOOLS, FigmaAPI, SceneGraph, computeAllLayouts, parseFigFile } from '@open-pencil/core' import { expectDefined } from '#tests/helpers/assert' interface PageTreeResult { page: string children: PageTreeNode[] } interface PageTreeNode { id: string type: string name: string children?: PageTreeNode[] } 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 PageTreeResult expect(result.page).toBe('Page 1') expect(result.children).toBeArray() }) test('get_page_tree supports depth, root_id, and node_types filters', () => { const { api } = setup() const parent = findTool('create_shape').execute(api, { type: 'FRAME', x: 0, y: 0, width: 400, height: 400, name: 'Parent' }) as { id: string } const text = findTool('create_shape').execute(api, { type: 'TEXT', x: 0, y: 0, width: 100, height: 20, parent_id: parent.id, name: 'Nested text' }) as { id: string } findTool('create_shape').execute(api, { type: 'RECTANGLE', x: 0, y: 0, width: 50, height: 50, name: 'Other rect' }) const depthTree = findTool('get_page_tree').execute(api, { depth: 1 }) as PageTreeResult const depthParent = expectDefined( depthTree.children.find((node) => node.id === parent.id), 'depth-limited parent' ) expect(depthParent.children).toBeUndefined() const subtree = findTool('get_page_tree').execute(api, { root_id: parent.id }) as { root: string tree: PageTreeNode } expect(subtree.root).toBe(parent.id) expect(subtree.tree.children?.[0]?.id).toBe(text.id) const filtered = findTool('get_page_tree').execute(api, { node_types: ['TEXT'] }) as PageTreeResult expect(filtered.children.some((node) => node.type === 'RECTANGLE')).toBe(false) const filteredParent = expectDefined( filtered.children.find((node) => node.id === parent.id), 'filtered parent' ) expect(filteredParent.children?.[0]?.type).toBe('TEXT') }) 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 = expectDefined(api.getNodeById(result.id), 'created frame') 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 = expectDefined(api.getNodeById(frame.id), 'filled rectangle') 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 = expectDefined(api.getNodeById(frame.id), 'layout frame') 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 } findTool('create_shape').execute(api, { type: 'TEXT', x: 0, y: 0, width: 200, height: 30, name: 'Child', parent_id: parent.id }) const tree = findTool('get_page_tree').execute(api, {}) as PageTreeResult const parentNode = expectDefined( tree.children.find((c: { id: string }) => c.id === parent.id), 'parent tree node' ) 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 = expectDefined(api.getNodeById(clone.id), 'cloned node') 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 = expectDefined(api.getNodeById(frame.id), 'updated node') 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) }) })