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)
|
|||
|
|
})
|
|||
|
|
})
|