openpencil/tests/engine/multi-selection.test.ts
Danila Poyarkov 9784544f1f Clean up tools/schema, add geometry.ts, isMixed helper, fix imports, add tests
- Strip tools/schema.ts from 1831 to 65 lines (remove duplicate tool defs)
- Fix tools/index.ts to export ALL_TOOLS from registry instead of schema
- Fix renderJsx → renderJSX typo in tools/create.ts and structure.ts
- Add geometry.ts with shared rotation/coordinate math utilities
- Add isArrayMixed() helper to use-multi-props.ts, deduplicate FillSection/StrokeSection/EffectsSection
- Fix broken test imports: use @open-pencil/core instead of relative paths into packages/core/src/
- Merge duplicate import statements across all test files
- Export profiler internals and style-run helpers from core index.ts
- Add 6 new test files: color, undo, snap, vector, style-runs, text-editor (98 new tests)
- Update renderer-effects test for renderer/ directory split
2026-03-06 22:33:10 +03:00

317 lines
10 KiB
TypeScript

import { describe, test, expect } from 'bun:test'
import { SceneGraph } from '@open-pencil/core'
function pageId(graph: SceneGraph) {
return graph.getPages()[0].id
}
function rect(
graph: SceneGraph,
parentId: string,
props: { name?: string; x?: number; y?: number; width?: number; height?: number } = {}
) {
return graph.createNode('RECTANGLE', parentId, {
name: props.name ?? 'Rect',
x: props.x ?? 0,
y: props.y ?? 0,
width: props.width ?? 50,
height: props.height ?? 50
})
}
describe('flip', () => {
test('flipX defaults to false', () => {
const graph = new SceneGraph()
const node = rect(graph, pageId(graph))
expect(node.flipX).toBe(false)
expect(node.flipY).toBe(false)
})
test('flipX can be set via updateNode', () => {
const graph = new SceneGraph()
const node = rect(graph, pageId(graph))
graph.updateNode(node.id, { flipX: true })
expect(graph.getNode(node.id)!.flipX).toBe(true)
expect(graph.getNode(node.id)!.flipY).toBe(false)
})
test('flipY can be set via updateNode', () => {
const graph = new SceneGraph()
const node = rect(graph, pageId(graph))
graph.updateNode(node.id, { flipY: true })
expect(graph.getNode(node.id)!.flipX).toBe(false)
expect(graph.getNode(node.id)!.flipY).toBe(true)
})
test('flip toggles', () => {
const graph = new SceneGraph()
const node = rect(graph, pageId(graph))
graph.updateNode(node.id, { flipX: true })
expect(graph.getNode(node.id)!.flipX).toBe(true)
graph.updateNode(node.id, { flipX: false })
expect(graph.getNode(node.id)!.flipX).toBe(false)
})
})
describe('single-node alignment to parent', () => {
function setup() {
const graph = new SceneGraph()
const frame = graph.createNode('FRAME', pageId(graph), {
name: 'Container',
x: 0,
y: 0,
width: 400,
height: 300
})
const child = rect(graph, frame.id, { name: 'Child', x: 50, y: 50, width: 100, height: 80 })
return { graph, frame, child }
}
test('align left within parent', () => {
const { graph, frame, child } = setup()
graph.updateNode(child.id, { x: 0 })
expect(graph.getNode(child.id)!.x).toBe(0)
})
test('align right within parent', () => {
const { graph, frame, child } = setup()
const targetX = frame.width - child.width
graph.updateNode(child.id, { x: targetX })
expect(graph.getNode(child.id)!.x).toBe(300)
})
test('align center horizontal within parent', () => {
const { graph, frame, child } = setup()
const targetX = (frame.width - child.width) / 2
graph.updateNode(child.id, { x: targetX })
expect(graph.getNode(child.id)!.x).toBe(150)
})
test('align top within parent', () => {
const { graph, child } = setup()
graph.updateNode(child.id, { y: 0 })
expect(graph.getNode(child.id)!.y).toBe(0)
})
test('align bottom within parent', () => {
const { graph, frame, child } = setup()
const targetY = frame.height - child.height
graph.updateNode(child.id, { y: targetY })
expect(graph.getNode(child.id)!.y).toBe(220)
})
test('align center vertical within parent', () => {
const { graph, frame, child } = setup()
const targetY = (frame.height - child.height) / 2
graph.updateNode(child.id, { y: targetY })
expect(graph.getNode(child.id)!.y).toBe(110)
})
})
describe('multi-node alignment', () => {
function setup() {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page, { name: 'A', x: 10, y: 20, width: 50, height: 30 })
const b = rect(graph, page, { name: 'B', x: 100, y: 80, width: 60, height: 40 })
const c = rect(graph, page, { name: 'C', x: 200, y: 50, width: 40, height: 50 })
return { graph, a, b, c }
}
test('align left — all nodes move to min x', () => {
const { graph, a, b, c } = setup()
const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
const minX = Math.min(...abs.map((p, i) => p.x))
for (const n of nodes) {
const nodeAbs = graph.getAbsolutePosition(n.id)
graph.updateNode(n.id, { x: n.x + (minX - nodeAbs.x) })
}
expect(graph.getNode(a.id)!.x).toBe(10)
expect(graph.getNode(b.id)!.x).toBe(10)
expect(graph.getNode(c.id)!.x).toBe(10)
})
test('align right — all nodes align to max right edge', () => {
const { graph, a, b, c } = setup()
const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
const maxX = Math.max(...abs.map((p, i) => p.x + nodes[i].width))
for (const n of nodes) {
const nodeAbs = graph.getAbsolutePosition(n.id)
const targetX = maxX - n.width
graph.updateNode(n.id, { x: n.x + (targetX - nodeAbs.x) })
}
expect(graph.getNode(a.id)!.x).toBe(190)
expect(graph.getNode(b.id)!.x).toBe(180)
expect(graph.getNode(c.id)!.x).toBe(200)
})
test('align top — all nodes move to min y', () => {
const { graph, a, b, c } = setup()
const nodes = [a, b, c].map((n) => graph.getNode(n.id)!)
const abs = nodes.map((n) => graph.getAbsolutePosition(n.id))
const minY = Math.min(...abs.map((p) => p.y))
for (const n of nodes) {
const nodeAbs = graph.getAbsolutePosition(n.id)
graph.updateNode(n.id, { y: n.y + (minY - nodeAbs.y) })
}
expect(graph.getNode(a.id)!.y).toBe(20)
expect(graph.getNode(b.id)!.y).toBe(20)
expect(graph.getNode(c.id)!.y).toBe(20)
})
})
describe('multi-node property merging', () => {
test('same values merge to single value', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page, { width: 100, height: 50 })
const b = rect(graph, page, { width: 100, height: 50 })
const nodes = [graph.getNode(a.id)!, graph.getNode(b.id)!]
const widths = new Set(nodes.map((n) => n.width))
expect(widths.size).toBe(1)
expect([...widths][0]).toBe(100)
})
test('different values are detected', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page, { width: 100 })
const b = rect(graph, page, { width: 200 })
const nodes = [graph.getNode(a.id)!, graph.getNode(b.id)!]
const widths = new Set(nodes.map((n) => n.width))
expect(widths.size).toBe(2)
})
test('update applies to all selected nodes', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page, { width: 100 })
const b = rect(graph, page, { width: 200 })
const c = rect(graph, page, { width: 150 })
for (const id of [a.id, b.id, c.id]) {
graph.updateNode(id, { width: 300 })
}
expect(graph.getNode(a.id)!.width).toBe(300)
expect(graph.getNode(b.id)!.width).toBe(300)
expect(graph.getNode(c.id)!.width).toBe(300)
})
test('opacity update applies uniformly', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page)
const b = rect(graph, page)
graph.updateNode(a.id, { opacity: 0.5 })
graph.updateNode(b.id, { opacity: 0.8 })
for (const id of [a.id, b.id]) {
graph.updateNode(id, { opacity: 0.75 })
}
expect(graph.getNode(a.id)!.opacity).toBe(0.75)
expect(graph.getNode(b.id)!.opacity).toBe(0.75)
})
test('fills comparison detects mixed', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const a = rect(graph, page)
const b = rect(graph, page)
graph.updateNode(a.id, {
fills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
})
graph.updateNode(b.id, {
fills: [{ type: 'SOLID', color: { r: 0, g: 1, b: 0, a: 1 }, opacity: 1, visible: true }]
})
const nodeA = graph.getNode(a.id)!
const nodeB = graph.getNode(b.id)!
expect(JSON.stringify(nodeA.fills)).not.toBe(JSON.stringify(nodeB.fills))
})
test('fills comparison detects same', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const fill = { type: 'SOLID' as const, color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }
const a = rect(graph, page)
const b = rect(graph, page)
graph.updateNode(a.id, { fills: [{ ...fill }] })
graph.updateNode(b.id, { fills: [{ ...fill }] })
const nodeA = graph.getNode(a.id)!
const nodeB = graph.getNode(b.id)!
expect(JSON.stringify(nodeA.fills)).toBe(JSON.stringify(nodeB.fills))
})
})
describe('flip roundtrip via kiwi', () => {
async function roundtrip(graph: SceneGraph) {
const { initCodec } = await import('../../packages/core/src/kiwi/codec')
const { parseFigFile } = await import('../../packages/core/src/kiwi/fig-file')
const { exportFigFile } = await import('../../packages/core/src/fig-export')
await initCodec()
const buf = await exportFigFile(graph)
return parseFigFile(buf)
}
function findChild(graph: SceneGraph, name: string) {
const pages = graph.getPages()
const children = graph.getChildren(pages[0].id)
return children.find((c) => c.name === name)
}
test('flipX preserved through export/import', async () => {
const graph = new SceneGraph()
const page = pageId(graph)
rect(graph, page, { name: 'Flipped', x: 50, y: 50, width: 100, height: 80 })
graph.updateNode(graph.getChildren(page).find((n) => n.name === 'Flipped')!.id, { flipX: true })
const imported = await roundtrip(graph)
const found = findChild(imported, 'Flipped')
expect(found).toBeTruthy()
expect(found!.flipX).toBe(true)
})
test('non-flipped node stays non-flipped', async () => {
const graph = new SceneGraph()
rect(graph, pageId(graph), { name: 'Normal', x: 100, y: 50, width: 200, height: 100 })
const imported = await roundtrip(graph)
const found = findChild(imported, 'Normal')
expect(found).toBeTruthy()
expect(found!.flipX).toBe(false)
})
test('flipX with rotation preserved', async () => {
const graph = new SceneGraph()
const page = pageId(graph)
rect(graph, page, { name: 'RotFlip', x: 0, y: 0, width: 100, height: 100 })
graph.updateNode(graph.getChildren(page).find((n) => n.name === 'RotFlip')!.id, {
flipX: true,
rotation: 45
})
const imported = await roundtrip(graph)
const found = findChild(imported, 'RotFlip')
expect(found).toBeTruthy()
expect(found!.flipX).toBe(true)
expect(Math.round(found!.rotation)).toBe(45)
})
})