220 lines
6.6 KiB
TypeScript
220 lines
6.6 KiB
TypeScript
import { describe, it, expect } from 'bun:test'
|
|
|
|
import { SceneGraph, LabelCache } from '@open-pencil/core'
|
|
|
|
function buildGraph() {
|
|
const g = new SceneGraph()
|
|
const pageId = g.getPages()[0].id
|
|
|
|
const sectionId = g.createNode('SECTION', pageId, {
|
|
name: 'Section 1',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
}).id
|
|
|
|
const nestedSectionId = g.createNode('SECTION', sectionId, {
|
|
name: 'Nested Section',
|
|
x: 10,
|
|
y: 10,
|
|
width: 100,
|
|
height: 100
|
|
}).id
|
|
|
|
const compSetId = g.createNode('COMPONENT_SET', pageId, {
|
|
name: 'Button Set',
|
|
x: 300,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
}).id
|
|
|
|
const compId = g.createNode('COMPONENT', compSetId, {
|
|
name: 'Button',
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 40
|
|
}).id
|
|
|
|
const standaloneCompId = g.createNode('COMPONENT', pageId, {
|
|
name: 'Standalone',
|
|
x: 600,
|
|
y: 0,
|
|
width: 100,
|
|
height: 50
|
|
}).id
|
|
|
|
return { g, pageId, sectionId, nestedSectionId, compSetId, compId, standaloneCompId }
|
|
}
|
|
|
|
describe('LabelCache', () => {
|
|
it('collects sections on first call', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const sections = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
expect(sections.length).toBe(2)
|
|
expect(sections.map((s) => s.node.name).sort()).toEqual(['Nested Section', 'Section 1'])
|
|
})
|
|
|
|
it('returns cached data when sceneVersion unchanged', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const first = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
cache.update(g, pageId, 1)
|
|
const second = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
expect(first.length).toBe(second.length)
|
|
expect(first.map((s) => s.node.id)).toEqual(second.map((s) => s.node.id))
|
|
})
|
|
|
|
it('invalidates when sceneVersion changes', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const before = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
g.createNode('SECTION', pageId, {
|
|
name: 'New Section',
|
|
x: 500,
|
|
y: 500,
|
|
width: 100,
|
|
height: 100
|
|
})
|
|
cache.update(g, pageId, 2)
|
|
const after = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
expect(after.length).toBe(before.length + 1)
|
|
})
|
|
|
|
it('invalidates when pageId changes', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
expect(cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 }).length).toBe(2)
|
|
|
|
const page2 = g.addPage('Page 2')
|
|
cache.update(g, page2.id, 1)
|
|
expect(cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 }).length).toBe(0)
|
|
})
|
|
|
|
it('filters sections by viewport', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
|
|
const all = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
expect(all.length).toBe(2)
|
|
|
|
const visible = cache.getSections(g, { x: 0, y: 0, w: 50, h: 50 })
|
|
expect(visible.length).toBeGreaterThan(0)
|
|
|
|
const offscreen = cache.getSections(g, { x: 5000, y: 5000, w: 100, h: 100 })
|
|
expect(offscreen.length).toBe(0)
|
|
})
|
|
|
|
it('tracks nested sections', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const sections = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
const topLevel = sections.find((s) => s.node.name === 'Section 1')
|
|
const nested = sections.find((s) => s.node.name === 'Nested Section')
|
|
|
|
expect(topLevel?.nested).toBe(false)
|
|
expect(nested?.nested).toBe(true)
|
|
})
|
|
|
|
it('collects top-level components and component sets', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const components = cache.getComponents(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
expect(components.length).toBe(2)
|
|
const names = components.map((c) => c.node.name).sort()
|
|
expect(names).toEqual(['Button Set', 'Standalone'])
|
|
})
|
|
|
|
it('skips components nested inside other components', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const components = cache.getComponents(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
expect(components.find((c) => c.node.name === 'Button Set')).toBeDefined()
|
|
expect(components.find((c) => c.node.name === 'Button')).toBeUndefined()
|
|
expect(components.find((c) => c.node.name === 'Standalone')).toBeDefined()
|
|
})
|
|
|
|
it('skips invisible nodes', () => {
|
|
const { g, pageId, sectionId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
g.updateNode(sectionId, { visible: false })
|
|
cache.update(g, pageId, 1)
|
|
|
|
const sections = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
const names = sections.map((s) => s.node.name)
|
|
expect(names).not.toContain('Section 1')
|
|
expect(names).not.toContain('Nested Section')
|
|
})
|
|
|
|
it('handles empty graph', () => {
|
|
const g = new SceneGraph()
|
|
const pageId = g.getPages()[0].id
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 0)
|
|
expect(cache.getSections(g, { x: 0, y: 0, w: 1000, h: 1000 })).toEqual([])
|
|
expect(cache.getComponents(g, { x: 0, y: 0, w: 1000, h: 1000 })).toEqual([])
|
|
})
|
|
|
|
it('explicit invalidate() forces recollection', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
expect(cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 }).length).toBe(2)
|
|
|
|
g.createNode('SECTION', pageId, { name: 'Sneaky', x: 800, y: 0, width: 50, height: 50 })
|
|
|
|
cache.update(g, pageId, 1)
|
|
expect(cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 }).length).toBe(2)
|
|
|
|
cache.invalidate()
|
|
cache.update(g, pageId, 1)
|
|
expect(cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 }).length).toBe(3)
|
|
})
|
|
|
|
it('computes correct absolute positions for nested nodes', () => {
|
|
const { g, pageId } = buildGraph()
|
|
const cache = new LabelCache()
|
|
|
|
cache.update(g, pageId, 1)
|
|
const sections = cache.getSections(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
|
|
const nested = sections.find((s) => s.node.name === 'Nested Section')
|
|
expect(nested?.absX).toBe(10)
|
|
expect(nested?.absY).toBe(10)
|
|
|
|
const components = cache.getComponents(g, { x: -1000, y: -1000, w: 3000, h: 3000 })
|
|
const buttonSet = components.find((c) => c.node.name === 'Button Set')
|
|
expect(buttonSet?.absX).toBe(300)
|
|
expect(buttonSet?.absY).toBe(0)
|
|
})
|
|
})
|