openpencil/tests/engine/scene-graph/basic/update-node.test.ts
Danila Poyarkov 5e7c042352 test(scene-graph): split basic coverage
- Move scene graph behavior tests into focused files
- Share basic scene graph helper builders from basic/helpers.ts
- Remove scene graph basic from max-lines exceptions
2026-05-06 14:05:58 +03:00

228 lines
7.6 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/core'
import { expectDefined } from '#tests/helpers/assert'
import { pageId, rect } from './helpers'
describe('updateNode', () => {
test('non-layout change does not clear absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 100, 200)
// Populate absPosCache by calling getAbsolutePosition
const absPos = graph.getAbsolutePosition(rId)
expect(absPos).toEqual({ x: 100, y: 200 })
// Change fills (non-layout property)
graph.updateNode(rId, { fills: [] })
// Cache should still be valid
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).toBe(absPos) // same reference = cache hit
})
test('layout change clears absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 100, 200)
const absPos = graph.getAbsolutePosition(rId)
expect(absPos).toEqual({ x: 100, y: 200 })
// Change x (layout-affecting property)
graph.updateNode(rId, { x: 150 })
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).toEqual({ x: 150, y: 200 })
expect(absPos2).not.toBe(absPos) // different reference = cache miss
})
test('instance index updated on componentId change', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const comp1 = graph.createNode('COMPONENT', page, {
name: 'C1',
x: 0,
y: 0,
width: 50,
height: 50
})
const comp2 = graph.createNode('COMPONENT', page, {
name: 'C2',
x: 100,
y: 0,
width: 50,
height: 50
})
const inst = graph.createInstance(comp1.id, page, { x: 200, y: 0 })
// Instance should be indexed under comp1
expect(graph.instanceIndex.get(comp1.id)?.has(inst.id)).toBe(true)
// Re-point instance to comp2
graph.updateNode(inst.id, { componentId: comp2.id })
// Old index entry removed, new one added
expect(graph.instanceIndex.get(comp1.id)?.has(inst.id)).toBeFalsy()
expect(graph.instanceIndex.get(comp2.id)?.has(inst.id)).toBe(true)
})
test('updateNode on nonexistent node is a no-op', () => {
const graph = new SceneGraph()
// Should not throw
graph.updateNode('nonexistent-id', { x: 100 })
})
test('y change clears absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20)
const absPos = graph.getAbsolutePosition(rId)
expect(absPos).toEqual({ x: 10, y: 20 })
graph.updateNode(rId, { y: 99 })
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).toEqual({ x: 10, y: 99 })
expect(absPos2).not.toBe(absPos)
})
test('width change clears absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20, 50, 50)
const absPos = graph.getAbsolutePosition(rId)
graph.updateNode(rId, { width: 200 })
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).not.toBe(absPos)
})
test('rotation change clears absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20)
const absPos = graph.getAbsolutePosition(rId)
graph.updateNode(rId, { rotation: 45 })
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).not.toBe(absPos)
})
test('paddingLeft change clears absPosCache', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const frameId = graph.createNode('FRAME', page, {
name: 'F',
x: 0,
y: 0,
width: 200,
height: 200
}).id
const absPos = graph.getAbsolutePosition(frameId)
graph.updateNode(frameId, { paddingLeft: 20 })
const absPos2 = graph.getAbsolutePosition(frameId)
expect(absPos2).not.toBe(absPos)
})
test('layoutMode change clears absPosCache', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const frameId = graph.createNode('FRAME', page, {
name: 'F',
x: 0,
y: 0,
width: 200,
height: 200
}).id
const absPos = graph.getAbsolutePosition(frameId)
graph.updateNode(frameId, { layoutMode: 'HORIZONTAL' as const })
const absPos2 = graph.getAbsolutePosition(frameId)
expect(absPos2).not.toBe(absPos)
})
test('mixed layout + non-layout change clears absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20)
const absPos = graph.getAbsolutePosition(rId)
// Change both x (layout) and fills (non-layout) simultaneously
graph.updateNode(rId, { x: 50, fills: [] })
const absPos2 = graph.getAbsolutePosition(rId)
expect(absPos2).toEqual({ x: 50, y: 20 })
expect(absPos2).not.toBe(absPos)
})
test('non-layout properties: strokes, opacity, name, visible do not clear absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20)
const absPos = graph.getAbsolutePosition(rId)
graph.updateNode(rId, { strokes: [] })
expect(graph.getAbsolutePosition(rId)).toBe(absPos)
graph.updateNode(rId, { opacity: 0.5 })
expect(graph.getAbsolutePosition(rId)).toBe(absPos)
graph.updateNode(rId, { name: 'NewName' })
expect(graph.getAbsolutePosition(rId)).toBe(absPos)
graph.updateNode(rId, { visible: false })
expect(graph.getAbsolutePosition(rId)).toBe(absPos)
})
test('empty changes object does not clear absPosCache', () => {
const graph = new SceneGraph()
const rId = rect(graph, 'R', 10, 20)
const absPos = graph.getAbsolutePosition(rId)
graph.updateNode(rId, {})
expect(graph.getAbsolutePosition(rId)).toBe(absPos)
})
test('textPicture is nulled when text properties change on TEXT node', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const textId = graph.createNode('TEXT', page, {
name: 'T',
x: 0,
y: 0,
width: 100,
height: 20
}).id
const textNode = graph.getNode(textId)
expect(textNode).toBeDefined()
// Simulate a cached textPicture
const fakePicture = new Uint8Array([1, 2, 3])
expectDefined(textNode, 'text node').textPicture = fakePicture
// Changing fontSize (a TEXT_PICTURE_KEY) should null the cache
graph.updateNode(textId, { fontSize: 24 })
const afterUpdate = graph.getNode(textId)
expect(afterUpdate).toBeDefined()
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull()
})
test('textPicture survives non-text property change on TEXT node', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const textId = graph.createNode('TEXT', page, {
name: 'T',
x: 0,
y: 0,
width: 100,
height: 20
}).id
const fakePicture = new Uint8Array([4, 5, 6])
const textNode = graph.getNode(textId)
expect(textNode).toBeDefined()
expectDefined(textNode, 'text node').textPicture = fakePicture
// Changing opacity (NOT a TEXT_PICTURE_KEY) should preserve textPicture
graph.updateNode(textId, { opacity: 0.5 })
const afterUpdate = graph.getNode(textId)
expect(afterUpdate).toBeDefined()
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBe(fakePicture)
})
test('textPicture is nulled when textPicture is already null', () => {
const graph = new SceneGraph()
const page = pageId(graph)
const textId = graph.createNode('TEXT', page, {
name: 'T',
x: 0,
y: 0,
width: 100,
height: 20
}).id
// textPicture starts as null — updateNode should not crash
graph.updateNode(textId, { fontSize: 16 })
const afterUpdate = graph.getNode(textId)
expect(afterUpdate).toBeDefined()
expect(expectDefined(afterUpdate, 'updated node').textPicture).toBeNull()
})
})