openpencil/packages/scene-graph/tests/basic/count-descendants.test.ts
Danila Poyarkov 7b9f506f4a
test: migrate Scene Graph tests into the package and gate new tests/engine files (#790)
Scene Graph unit tests move to packages/scene-graph/tests with a package-local assert helper and test type-checking; five Core- and fig-owned tests move to their owners' engine homes. check:test-homes rejects any new test under tests/engine against a reviewed baseline so the migration debt only shrinks.
2026-09-30 04:53:28 +04:00

87 lines
3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, test } from 'bun:test'
import { SceneGraph } from '@open-pencil/scene-graph'
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)
})
})