import { describe, expect, setDefaultTimeout, test } from 'bun:test' import { importNodeChanges, parseFigFile } from '@open-pencil/core' import { expectDefined } from '#tests/helpers/assert' import { parseFixture } from '#tests/helpers/fig-fixtures' setDefaultTimeout(60_000) describe('edge cases', () => { test('non-fig-kiwi bytes throw meaningful error', async () => { const garbage = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) await expect(parseFigFile(garbage.buffer as ArrayBuffer)).rejects.toThrow() }) test('invalid zip data throws', async () => { const garbage = new Uint8Array(50).fill(0xff) await expect(parseFigFile(garbage.buffer as ArrayBuffer)).rejects.toThrow() }) test('REMOVED nodes are skipped', async () => { const { importNodeChanges } = await import('#core/kiwi/fig/import') const graph = importNodeChanges([ { guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Document', visible: true, opacity: 1, phase: 'CREATED', transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } }, { guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page', visible: true, opacity: 1, phase: 'CREATED', transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } }, { guid: { sessionID: 1, localID: 10 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'RECTANGLE', name: 'Visible', visible: true, opacity: 1, phase: 'CREATED', size: { x: 100, y: 100 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } }, { guid: { sessionID: 1, localID: 11 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' }, type: 'RECTANGLE', name: 'Deleted', visible: true, opacity: 1, phase: 'REMOVED', size: { x: 100, y: 100 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } ]) const children = graph.getChildren(graph.getPages()[0].id) expect(children).toHaveLength(1) expect(children[0].name).toBe('Visible') }) test('symbolOverrides propagate through nested instances', () => { // Structure: // DOCUMENT (0:0) // └─ CANVAS "Page" (0:1) // ├─ COMPONENT "Inner" (1:1) ← inner component // │ └─ TEXT "Label" (1:2) ← default text "Default" // ├─ COMPONENT "Outer" (1:3) ← outer component // │ └─ INSTANCE "InnerUse" (1:4) symId=1:1, override: Label→"Changed" // └─ INSTANCE "OuterUse" (1:5) ← instance of Outer // // After import, OuterUse should contain a clone of InnerUse, // which should contain a Label text with "Changed" (not "Default") const graph = importNodeChanges([ { guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Document', phase: 'CREATED' } as NodeChange, { guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page', phase: 'CREATED' } as NodeChange, // Inner component { guid: { sessionID: 1, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'SYMBOL', name: 'Inner', phase: 'CREATED', size: { x: 100, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, // TEXT child of Inner { guid: { sessionID: 1, localID: 2 }, overrideKey: { sessionID: 99, localID: 2 }, parentIndex: { guid: { sessionID: 1, localID: 1 }, position: '!' }, type: 'TEXT', name: 'Label', textData: { characters: 'Default' }, phase: 'CREATED', size: { x: 80, y: 20 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, // Outer component { guid: { sessionID: 1, localID: 3 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' }, type: 'SYMBOL', name: 'Outer', phase: 'CREATED', size: { x: 200, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, // INSTANCE of Inner inside Outer, with symbolOverride on Label { guid: { sessionID: 1, localID: 4 }, parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '!' }, type: 'INSTANCE', name: 'InnerUse', phase: 'CREATED', size: { x: 100, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 1 }, symbolOverrides: [ { guidPath: { guids: [{ sessionID: 99, localID: 2 }] }, textData: { characters: 'Changed' } } ] } } as NodeChange, // Top-level INSTANCE of Outer on the page { guid: { sessionID: 1, localID: 5 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '#' }, type: 'INSTANCE', name: 'OuterUse', phase: 'CREATED', size: { x: 200, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 3 } } } as NodeChange ]) const page = graph.getPages()[0] const outerUse = graph.getChildren(page.id).find((n) => n.name === 'OuterUse') expect(outerUse).toBeDefined() expect(expectDefined(outerUse, 'outerUse').type).toBe('INSTANCE') // Walk down: OuterUse > InnerUse clone > Label clone const innerClone = graph.getChildren(outerUse?.id ?? '')[0] expect(innerClone).toBeDefined() expect(innerClone.name).toBe('InnerUse') const labelClone = graph.getChildren(innerClone.id)[0] expect(labelClone).toBeDefined() expect(labelClone.name).toBe('Label') expect(labelClone.text).toBe('Changed') }) test('overriddenSymbolID swaps instance component through nested levels', () => { // Structure: // COMPONENT "IconA" (1:1) → VECTOR "PathA" // COMPONENT "IconB" (1:3) → VECTOR "PathB1", VECTOR "PathB2" // COMPONENT "Button" (1:5) → INSTANCE "icon" of IconA // COMPONENT "Toolbar" (1:7) → INSTANCE "btn" of Button, with override swapping icon to IconB // INSTANCE "ToolbarUse" (1:9) of Toolbar on the page // // After import, ToolbarUse > btn clone > icon clone should have IconB's children const graph = importNodeChanges([ { guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Document', phase: 'CREATED' } as NodeChange, { guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page', phase: 'CREATED' } as NodeChange, // IconA component with 1 child { guid: { sessionID: 1, localID: 1 }, overrideKey: { sessionID: 90, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'SYMBOL', name: 'IconA', phase: 'CREATED', size: { x: 24, y: 24 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, { guid: { sessionID: 1, localID: 2 }, overrideKey: { sessionID: 90, localID: 2 }, parentIndex: { guid: { sessionID: 1, localID: 1 }, position: '!' }, type: 'VECTOR', name: 'PathA', phase: 'CREATED', size: { x: 24, y: 24 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, // IconB component with 2 children { guid: { sessionID: 1, localID: 3 }, overrideKey: { sessionID: 90, localID: 3 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' }, type: 'SYMBOL', name: 'IconB', phase: 'CREATED', size: { x: 24, y: 24 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, { guid: { sessionID: 1, localID: 31 }, overrideKey: { sessionID: 90, localID: 31 }, parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '!' }, type: 'VECTOR', name: 'PathB1', phase: 'CREATED', size: { x: 24, y: 12 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, { guid: { sessionID: 1, localID: 32 }, overrideKey: { sessionID: 90, localID: 32 }, parentIndex: { guid: { sessionID: 1, localID: 3 }, position: '"' }, type: 'VECTOR', name: 'PathB2', phase: 'CREATED', size: { x: 24, y: 12 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, // Button component with instance of IconA { guid: { sessionID: 1, localID: 5 }, overrideKey: { sessionID: 90, localID: 5 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '#' }, type: 'SYMBOL', name: 'Button', phase: 'CREATED', size: { x: 40, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, { guid: { sessionID: 1, localID: 6 }, overrideKey: { sessionID: 90, localID: 6 }, parentIndex: { guid: { sessionID: 1, localID: 5 }, position: '!' }, type: 'INSTANCE', name: 'icon', phase: 'CREATED', size: { x: 24, y: 24 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 1 } } } as NodeChange, // Toolbar component with instance of Button, swapping icon to IconB { guid: { sessionID: 1, localID: 7 }, overrideKey: { sessionID: 90, localID: 7 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '$' }, type: 'SYMBOL', name: 'Toolbar', phase: 'CREATED', size: { x: 200, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 } } as NodeChange, { guid: { sessionID: 1, localID: 8 }, overrideKey: { sessionID: 90, localID: 8 }, parentIndex: { guid: { sessionID: 1, localID: 7 }, position: '!' }, type: 'INSTANCE', name: 'btn', phase: 'CREATED', size: { x: 40, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 5 }, symbolOverrides: [ { guidPath: { guids: [{ sessionID: 90, localID: 6 }] }, overriddenSymbolID: { sessionID: 1, localID: 3 } } ] } } as NodeChange, // Page-level instance of Toolbar { guid: { sessionID: 1, localID: 9 }, parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '%' }, type: 'INSTANCE', name: 'ToolbarUse', phase: 'CREATED', size: { x: 200, y: 40 }, transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }, symbolData: { symbolID: { sessionID: 1, localID: 7 } } } as NodeChange ]) const page = graph.getPages()[0] const toolbarUse = graph.getChildren(page.id).find((n) => n.name === 'ToolbarUse') expect(toolbarUse).toBeDefined() // ToolbarUse > btn clone > icon clone const btnClone = graph.getChildren(toolbarUse?.id ?? '')[0] expect(btnClone).toBeDefined() expect(btnClone.name).toBe('btn') const iconClone = graph.getChildren(btnClone.id)[0] expect(iconClone).toBeDefined() expect(iconClone.name).toBe('icon') // Icon should have IconB's 2 children, not IconA's 1 child const iconChildren = graph.getChildren(iconClone.id) expect(iconChildren).toHaveLength(2) expect(iconChildren.map((c) => c.name).sort()).toEqual(['PathB1', 'PathB2']) }) test('DSD propagates through intermediate clones that are also DSD-targeted', async () => { const graph = await parseFixture('gold-preview.fig') const thumb = [...graph.getAllNodes()].find((n) => n.name === 'Preview Thumbnail') expect(thumb).toBeDefined() let overflows = 0 function walk(id: string) { const node = graph.getNode(id) if (!node) return if (node.type === 'VECTOR') { const parent = graph.getNode(node.parentId) if (parent?.type === 'INSTANCE' && parent.width > 0 && parent.height > 0) { // Check visibility let vis = true let cur: typeof node | null = node while (cur) { if (!cur.visible) { vis = false break } cur = cur.parentId ? (graph.getNode(cur.parentId) ?? null) : null } // Check clipping let clipped = false cur = graph.getNode(parent.parentId) while (cur) { if (cur.clipsContent) { clipped = true break } cur = cur.parentId ? (graph.getNode(cur.parentId) ?? null) : null } if (vis && !clipped && node.width > parent.width * 1.2) { overflows++ } } } for (const cid of node.childIds) walk(cid) } walk(thumb?.id ?? '') expect(overflows).toBe(0) }) })