- 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
87 lines
3 KiB
TypeScript
87 lines
3 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
||
|
||
import { SceneGraph } from '@open-pencil/core'
|
||
|
||
import { pageId, rect } from './helpers'
|
||
|
||
describe('countDescendants', () => {
|
||
test('nonexistent node returns 0', () => {
|
||
const graph = new SceneGraph()
|
||
expect(graph.countDescendants('nonexistent-id')).toBe(0)
|
||
})
|
||
|
||
test('page with 0 children returns 0', () => {
|
||
const graph = new SceneGraph()
|
||
const page = graph.getPages()[0]
|
||
// Default new graph has an empty page
|
||
expect(graph.countDescendants(page.id)).toBe(0)
|
||
})
|
||
|
||
test('counts all descendants recursively', () => {
|
||
const graph = new SceneGraph()
|
||
const page = pageId(graph)
|
||
// Create a 3-level hierarchy: page → frame → rect, ellipse
|
||
const frame = graph.createNode('FRAME', page, {
|
||
name: 'F',
|
||
x: 0,
|
||
y: 0,
|
||
width: 200,
|
||
height: 200
|
||
}).id
|
||
graph.createNode('RECTANGLE', frame, { name: 'R1', x: 0, y: 0, width: 10, height: 10 })
|
||
graph.createNode('ELLIPSE', frame, { name: 'E1', x: 20, y: 20, width: 10, height: 10 })
|
||
// Page descendants = frame + R1 + E1 = 3
|
||
expect(graph.countDescendants(page)).toBe(3)
|
||
// Frame has 2 descendants
|
||
expect(graph.countDescendants(frame)).toBe(2)
|
||
// Leaf has 0
|
||
const rectId = graph.getChildren(frame)[0].id
|
||
expect(graph.countDescendants(rectId)).toBe(0)
|
||
})
|
||
|
||
test('root node count includes all pages and their children', () => {
|
||
const graph = new SceneGraph()
|
||
rect(graph, 'R1')
|
||
rect(graph, 'R2')
|
||
graph.addPage('Page 2')
|
||
const page2 = graph.getPages()[1].id
|
||
graph.createNode('RECTANGLE', page2, { name: 'R3', x: 0, y: 0, width: 10, height: 10 })
|
||
// Root descendants = Page1 + R1 + R2 + Page2 + R3 = 5
|
||
expect(graph.countDescendants(graph.rootId)).toBe(5)
|
||
})
|
||
|
||
test('does not crash on a node with 200k direct children (spread regression)', () => {
|
||
// V8/JSC spread-into-push caps function arguments at ~125k–500k depending on engine
|
||
// and version. Using stack.push(...childIds) inside a loop crashes with
|
||
// RangeError: Maximum call stack size exceeded on real Figma documents.
|
||
// This test guards against the regression introduced in a6122c13.
|
||
const graph = new SceneGraph()
|
||
const page = pageId(graph)
|
||
const frame = graph.createNode('FRAME', page, {
|
||
name: 'BigFrame',
|
||
x: 0,
|
||
y: 0,
|
||
width: 200,
|
||
height: 200
|
||
}).id
|
||
const CHILD_COUNT = 200_000
|
||
// Directly inject children into the node map to avoid O(N^2) createNode overhead
|
||
const frameNode = graph.nodes.get(frame)
|
||
if (!frameNode) throw new Error('frame not found')
|
||
for (let i = 0; i < CHILD_COUNT; i++) {
|
||
const childId = `bulk:${i}`
|
||
graph.nodes.set(childId, {
|
||
...frameNode,
|
||
id: childId,
|
||
name: `bulk_${i}`,
|
||
type: 'RECTANGLE',
|
||
childIds: [],
|
||
parentId: frame
|
||
})
|
||
frameNode.childIds.push(childId)
|
||
}
|
||
expect(() => graph.countDescendants(frame)).not.toThrow()
|
||
expect(graph.countDescendants(frame)).toBe(CHILD_COUNT)
|
||
})
|
||
})
|