- Move top-level engine and e2e prefixed test files under domain folders - Update fixture path helpers after moving render and pen tests - Add lint coverage to prevent new top-level prefixed test files - Refresh testing docs for the new fig and layout paths
189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { SceneGraph } from '@open-pencil/core'
|
|
|
|
function pageId(graph: SceneGraph) {
|
|
return graph.getPages()[0].id
|
|
}
|
|
|
|
describe('absolute position cache', () => {
|
|
test('cached result matches uncached computation', () => {
|
|
const graph = new SceneGraph()
|
|
const frame = graph.createNode('FRAME', pageId(graph), { name: 'F', x: 100, y: 200 })
|
|
const child = graph.createNode('RECTANGLE', frame.id, { name: 'R', x: 10, y: 20 })
|
|
|
|
const first = graph.getAbsolutePosition(child.id)
|
|
const second = graph.getAbsolutePosition(child.id)
|
|
|
|
expect(first).toEqual({ x: 110, y: 220 })
|
|
expect(second).toEqual({ x: 110, y: 220 })
|
|
expect(first).toBe(second)
|
|
})
|
|
|
|
test('cache invalidated after node position change', () => {
|
|
const graph = new SceneGraph()
|
|
const frame = graph.createNode('FRAME', pageId(graph), { name: 'F', x: 50, y: 50 })
|
|
const child = graph.createNode('RECTANGLE', frame.id, { name: 'R', x: 10, y: 10 })
|
|
|
|
expect(graph.getAbsolutePosition(child.id)).toEqual({ x: 60, y: 60 })
|
|
|
|
graph.updateNode(frame.id, { x: 100, y: 100 })
|
|
|
|
expect(graph.getAbsolutePosition(child.id)).toEqual({ x: 110, y: 110 })
|
|
})
|
|
|
|
test('cache invalidated after reparenting', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const frameA = graph.createNode('FRAME', page, { name: 'A', x: 100, y: 100 })
|
|
const frameB = graph.createNode('FRAME', page, { name: 'B', x: 300, y: 300 })
|
|
const child = graph.createNode('RECTANGLE', frameA.id, { name: 'R', x: 10, y: 10 })
|
|
|
|
expect(graph.getAbsolutePosition(child.id)).toEqual({ x: 110, y: 110 })
|
|
|
|
graph.reparentNode(child.id, frameB.id)
|
|
|
|
const pos = graph.getAbsolutePosition(child.id)
|
|
expect(pos).toEqual({ x: 110, y: 110 })
|
|
})
|
|
|
|
test('nested nodes (3+ levels) get correct absolute positions', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const level1 = graph.createNode('FRAME', page, { name: 'L1', x: 10, y: 20 })
|
|
const level2 = graph.createNode('FRAME', level1.id, { name: 'L2', x: 30, y: 40 })
|
|
const level3 = graph.createNode('FRAME', level2.id, { name: 'L3', x: 50, y: 60 })
|
|
const leaf = graph.createNode('RECTANGLE', level3.id, { name: 'Leaf', x: 1, y: 2 })
|
|
|
|
expect(graph.getAbsolutePosition(level1.id)).toEqual({ x: 10, y: 20 })
|
|
expect(graph.getAbsolutePosition(level2.id)).toEqual({ x: 40, y: 60 })
|
|
expect(graph.getAbsolutePosition(level3.id)).toEqual({ x: 90, y: 120 })
|
|
expect(graph.getAbsolutePosition(leaf.id)).toEqual({ x: 91, y: 122 })
|
|
})
|
|
|
|
test('clearAbsPosCache forces recomputation', () => {
|
|
const graph = new SceneGraph()
|
|
const rect = graph.createNode('RECTANGLE', pageId(graph), { name: 'R', x: 10, y: 20 })
|
|
|
|
const first = graph.getAbsolutePosition(rect.id)
|
|
graph.clearAbsPosCache()
|
|
const second = graph.getAbsolutePosition(rect.id)
|
|
|
|
expect(first).toEqual(second)
|
|
expect(first).not.toBe(second)
|
|
})
|
|
|
|
test('sibling cache entries survive unrelated sibling update', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const a = graph.createNode('RECTANGLE', page, { name: 'A', x: 10, y: 10 })
|
|
const b = graph.createNode('RECTANGLE', page, { name: 'B', x: 20, y: 20 })
|
|
|
|
graph.getAbsolutePosition(a.id)
|
|
graph.updateNode(b.id, { x: 30 })
|
|
|
|
const posA = graph.getAbsolutePosition(a.id)
|
|
expect(posA).toEqual({ x: 10, y: 10 })
|
|
})
|
|
|
|
test('non-layout changes do NOT clear absPosCache', () => {
|
|
const graph = new SceneGraph()
|
|
const rect = graph.createNode('RECTANGLE', pageId(graph), { name: 'R', x: 10, y: 20 })
|
|
|
|
// Populate cache
|
|
const cached = graph.getAbsolutePosition(rect.id)
|
|
expect(cached).toEqual({ x: 10, y: 20 })
|
|
|
|
// Update a non-layout property (fills)
|
|
graph.updateNode(rect.id, {
|
|
fills: [
|
|
{
|
|
type: 'SOLID',
|
|
color: { r: 1, g: 0, b: 0, a: 1 },
|
|
opacity: 1,
|
|
visible: true,
|
|
blendMode: 'NORMAL'
|
|
}
|
|
]
|
|
})
|
|
|
|
// Cache should be preserved (same reference)
|
|
const afterFill = graph.getAbsolutePosition(rect.id)
|
|
expect(afterFill).toBe(cached) // strict reference equality
|
|
|
|
// Update plugin data (non-layout)
|
|
graph.updateNode(rect.id, {
|
|
pluginData: [{ pluginId: 'test', key: 'k', value: 'v' }]
|
|
})
|
|
|
|
const afterPlugin = graph.getAbsolutePosition(rect.id)
|
|
expect(afterPlugin).toBe(cached) // still same reference
|
|
})
|
|
|
|
test('layout changes DO clear absPosCache', () => {
|
|
const graph = new SceneGraph()
|
|
const rect = graph.createNode('RECTANGLE', pageId(graph), { name: 'R', x: 10, y: 20 })
|
|
|
|
const cached = graph.getAbsolutePosition(rect.id)
|
|
|
|
// Update a layout property (x)
|
|
graph.updateNode(rect.id, { x: 50 })
|
|
|
|
// Cache should be cleared (new computation)
|
|
const afterX = graph.getAbsolutePosition(rect.id)
|
|
expect(afterX).toEqual({ x: 50, y: 20 })
|
|
expect(afterX).not.toBe(cached)
|
|
|
|
// Rotation also clears cache
|
|
const cached2 = graph.getAbsolutePosition(rect.id)
|
|
graph.updateNode(rect.id, { rotation: 45 })
|
|
const afterRotation = graph.getAbsolutePosition(rect.id)
|
|
expect(afterRotation).not.toBe(cached2)
|
|
})
|
|
|
|
test('auto-layout property changes clear absPosCache', () => {
|
|
const graph = new SceneGraph()
|
|
const frame = graph.createNode('FRAME', pageId(graph), {
|
|
name: 'F',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
})
|
|
const child = graph.createNode('RECTANGLE', frame.id, { name: 'R', x: 10, y: 10 })
|
|
|
|
const cached = graph.getAbsolutePosition(child.id)
|
|
|
|
// Changing auto-layout property on parent clears cache
|
|
graph.updateNode(frame.id, { primaryAxisAlign: 'CENTER' })
|
|
const afterAlign = graph.getAbsolutePosition(child.id)
|
|
expect(afterAlign).not.toBe(cached)
|
|
|
|
// Changing itemSpacing also clears cache
|
|
const cached2 = graph.getAbsolutePosition(child.id)
|
|
graph.updateNode(frame.id, { itemSpacing: 10 })
|
|
const afterSpacing = graph.getAbsolutePosition(child.id)
|
|
expect(afterSpacing).not.toBe(cached2)
|
|
})
|
|
|
|
test('effects, fills, strokes, opacity changes do NOT clear absPosCache', () => {
|
|
const graph = new SceneGraph()
|
|
const rect = graph.createNode('RECTANGLE', pageId(graph), { name: 'R', x: 10, y: 20 })
|
|
const cached = graph.getAbsolutePosition(rect.id)
|
|
|
|
graph.updateNode(rect.id, {
|
|
opacity: 0.5,
|
|
fills: [],
|
|
strokes: [],
|
|
effects: []
|
|
})
|
|
expect(graph.getAbsolutePosition(rect.id)).toBe(cached)
|
|
|
|
graph.updateNode(rect.id, { name: 'Renamed' })
|
|
expect(graph.getAbsolutePosition(rect.id)).toBe(cached)
|
|
|
|
graph.updateNode(rect.id, { visible: false })
|
|
expect(graph.getAbsolutePosition(rect.id)).toBe(cached)
|
|
})
|
|
})
|