- Move clipboard node import tests into focused files by behavior - Preserve filtering, hierarchy, styling, component, and undo coverage - Remove import-nodes from max-lines exceptions
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { describe, expect, it } from 'bun:test'
|
|
|
|
import { importClipboardNodes } from '@open-pencil/core'
|
|
import type { NodeChange } from '@open-pencil/core'
|
|
|
|
import { getNodeOrThrow } from '#tests/helpers/assert'
|
|
import { createClipboardGraph } from '#tests/helpers/clipboard'
|
|
|
|
describe('importClipboardNodes: components', () => {
|
|
it('maps SYMBOL type to COMPONENT with auto-layout', () => {
|
|
const { graph, pageId } = createClipboardGraph()
|
|
|
|
const nodeChanges = [
|
|
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page'
|
|
},
|
|
{
|
|
guid: { sessionID: 0, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Dialog/Form',
|
|
size: { x: 452, y: 299 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
stackMode: 'VERTICAL',
|
|
stackSpacing: 16,
|
|
stackVerticalPadding: 24,
|
|
stackHorizontalPadding: 24,
|
|
stackPrimarySizing: 'RESIZE_TO_FIT',
|
|
stackCounterSizing: 'RESIZE_TO_FIT'
|
|
},
|
|
{
|
|
guid: { sessionID: 0, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 10 }, position: '!' },
|
|
type: 'TEXT',
|
|
name: 'Title',
|
|
size: { x: 404, y: 32 },
|
|
transform: { m00: 1, m01: 0, m02: 24, m10: 0, m11: 1, m12: 24 },
|
|
textData: { characters: 'Hello' },
|
|
fontSize: 24,
|
|
fontWeight: 700
|
|
},
|
|
{
|
|
guid: { sessionID: 0, localID: 12 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 10 }, position: '"' },
|
|
type: 'RECTANGLE',
|
|
name: 'Divider',
|
|
size: { x: 404, y: 1 },
|
|
transform: { m00: 1, m01: 0, m02: 24, m10: 0, m11: 1, m12: 72 }
|
|
}
|
|
] as NodeChange[]
|
|
|
|
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
|
expect(created).toHaveLength(1)
|
|
|
|
const component = getNodeOrThrow(graph, created[0])
|
|
expect(component.type).toBe('COMPONENT')
|
|
expect(component.layoutMode).toBe('VERTICAL')
|
|
expect(component.itemSpacing).toBe(16)
|
|
expect(component.primaryAxisSizing).toBe('HUG')
|
|
expect(component.counterAxisSizing).toBe('HUG')
|
|
|
|
const children = graph.getChildren(component.id)
|
|
expect(children).toHaveLength(2)
|
|
expect(children[0].name).toBe('Title')
|
|
expect(children[1].name).toBe('Divider')
|
|
})
|
|
|
|
it('populates instance children from pasted component', () => {
|
|
const { graph, pageId } = createClipboardGraph()
|
|
|
|
const nodeChanges = [
|
|
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page'
|
|
},
|
|
// Component with a child
|
|
{
|
|
guid: { sessionID: 1, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Icon/Warning',
|
|
size: { x: 48, y: 48 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 10 }, position: '!' },
|
|
type: 'VECTOR',
|
|
name: 'Triangle',
|
|
size: { x: 48, y: 42 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 3 }
|
|
},
|
|
// Instance referencing the component
|
|
{
|
|
guid: { sessionID: 2, localID: 20 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '"' },
|
|
type: 'INSTANCE',
|
|
name: 'Icon/Warning',
|
|
size: { x: 48, y: 48 },
|
|
transform: { m00: 1, m01: 0, m02: 100, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: { symbolID: { sessionID: 1, localID: 10 } }
|
|
}
|
|
] as NodeChange[]
|
|
|
|
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
|
expect(created).toHaveLength(2)
|
|
|
|
const component = getNodeOrThrow(graph, created[0])
|
|
expect(component.type).toBe('COMPONENT')
|
|
expect(graph.getChildren(component.id)).toHaveLength(1)
|
|
|
|
const instance = getNodeOrThrow(graph, created[1])
|
|
expect(instance.type).toBe('INSTANCE')
|
|
expect(instance.componentId).toBe(component.id)
|
|
|
|
const instanceChildren = graph.getChildren(instance.id)
|
|
expect(instanceChildren).toHaveLength(1)
|
|
expect(instanceChildren[0].name).toBe('Triangle')
|
|
expect(instanceChildren[0].type).toBe('VECTOR')
|
|
})
|
|
|
|
it('internal canvas components populate instances but are not pasted', () => {
|
|
const { graph, pageId } = createClipboardGraph()
|
|
|
|
const nodeChanges = [
|
|
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page 1'
|
|
},
|
|
// Internal Only Canvas with component
|
|
{
|
|
guid: { sessionID: 99, localID: 2 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '"' },
|
|
type: 'CANVAS',
|
|
name: 'Internal Only Canvas',
|
|
internalOnly: true
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 99, localID: 2 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Icon',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 10 }, position: '!' },
|
|
type: 'VECTOR',
|
|
name: 'Path',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
// Visible page with instance
|
|
{
|
|
guid: { sessionID: 2, localID: 20 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'Icon',
|
|
size: { x: 24, y: 24 },
|
|
transform: { m00: 1, m01: 0, m02: 50, m10: 0, m11: 1, m12: 50 },
|
|
symbolData: { symbolID: { sessionID: 1, localID: 10 } }
|
|
}
|
|
] as NodeChange[]
|
|
|
|
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
|
expect(created).toHaveLength(1)
|
|
|
|
const instance = getNodeOrThrow(graph, created[0])
|
|
expect(instance.type).toBe('INSTANCE')
|
|
expect(instance.name).toBe('Icon')
|
|
|
|
const children = graph.getChildren(instance.id)
|
|
expect(children).toHaveLength(1)
|
|
expect(children[0].name).toBe('Path')
|
|
expect(children[0].type).toBe('VECTOR')
|
|
|
|
// Component should NOT exist as a visible node
|
|
for (const node of graph.getAllNodes()) {
|
|
if (node.type === 'COMPONENT' && node.name === 'Icon') {
|
|
throw new Error('Internal component should not be pasted as visible node')
|
|
}
|
|
}
|
|
})
|
|
|
|
it('detaches orphaned instances to FRAME when component is missing', () => {
|
|
const { graph, pageId } = createClipboardGraph()
|
|
|
|
const nodeChanges = [
|
|
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page'
|
|
},
|
|
// Frame containing an instance whose component is NOT in the clipboard
|
|
{
|
|
guid: { sessionID: 0, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'FRAME',
|
|
name: 'Card',
|
|
size: { x: 400, y: 200 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 0, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 10 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'Button',
|
|
size: { x: 120, y: 40 },
|
|
transform: { m00: 1, m01: 0, m02: 20, m10: 0, m11: 1, m12: 20 },
|
|
fillPaints: [
|
|
{ type: 'SOLID', color: { r: 0.2, g: 0.4, b: 1, a: 1 }, opacity: 1, visible: true }
|
|
],
|
|
cornerRadius: 8,
|
|
symbolData: { symbolID: { sessionID: 99, localID: 999 } }
|
|
}
|
|
] as NodeChange[]
|
|
|
|
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
|
expect(created).toHaveLength(1)
|
|
|
|
const card = getNodeOrThrow(graph, created[0])
|
|
const children = graph.getChildren(card.id)
|
|
expect(children).toHaveLength(1)
|
|
|
|
const button = children[0]
|
|
expect(button.type).toBe('FRAME')
|
|
expect(button.name).toBe('Button')
|
|
expect(button.componentId).toBe('')
|
|
expect(button.fills).toHaveLength(1)
|
|
expect(button.fills[0].color.b).toBe(1)
|
|
expect(button.cornerRadius).toBe(8)
|
|
expect(button.width).toBe(120)
|
|
expect(button.height).toBe(40)
|
|
})
|
|
|
|
it('applies symbolOverrides text to instance children via overrideKey', () => {
|
|
const { graph, pageId } = createClipboardGraph()
|
|
|
|
const nodeChanges = [
|
|
{ guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Doc' },
|
|
{
|
|
guid: { sessionID: 0, localID: 1 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' },
|
|
type: 'CANVAS',
|
|
name: 'Page 1'
|
|
},
|
|
{
|
|
guid: { sessionID: 99, localID: 2 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '"' },
|
|
type: 'CANVAS',
|
|
name: 'Internal Only Canvas',
|
|
internalOnly: true
|
|
},
|
|
// Component on internal canvas
|
|
{
|
|
guid: { sessionID: 1, localID: 10 },
|
|
parentIndex: { guid: { sessionID: 99, localID: 2 }, position: '!' },
|
|
type: 'SYMBOL',
|
|
name: 'Day',
|
|
size: { x: 46, y: 46 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
|
|
},
|
|
{
|
|
guid: { sessionID: 1, localID: 11 },
|
|
parentIndex: { guid: { sessionID: 1, localID: 10 }, position: '!' },
|
|
type: 'TEXT',
|
|
name: 'Number',
|
|
size: { x: 14, y: 17 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
textData: { characters: '1' },
|
|
overrideKey: { sessionID: 50, localID: 100 }
|
|
},
|
|
// Instance on visible page with text override
|
|
{
|
|
guid: { sessionID: 2, localID: 20 },
|
|
parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' },
|
|
type: 'INSTANCE',
|
|
name: 'Day',
|
|
size: { x: 46, y: 46 },
|
|
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 },
|
|
symbolData: {
|
|
symbolID: { sessionID: 1, localID: 10 },
|
|
symbolOverrides: [
|
|
{
|
|
guidPath: { guids: [{ sessionID: 50, localID: 100 }] },
|
|
textData: { characters: '25' }
|
|
}
|
|
]
|
|
}
|
|
}
|
|
] as NodeChange[]
|
|
|
|
const created = importClipboardNodes(nodeChanges, graph, pageId)
|
|
expect(created).toHaveLength(1)
|
|
|
|
const instance = getNodeOrThrow(graph, created[0])
|
|
expect(instance.type).toBe('INSTANCE')
|
|
const children = graph.getChildren(instance.id)
|
|
expect(children).toHaveLength(1)
|
|
expect(children[0].text).toBe('25')
|
|
})
|
|
})
|