mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-01 13:59:54 +00:00
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: github-actions <github-actions@github.com>
484 lines
13 KiB
TypeScript
484 lines
13 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { useSpatialIndex } from '@/composables/graph/useSpatialIndex'
|
|
|
|
import { createBounds } from '../../helpers/nodeTestHelpers'
|
|
|
|
// Mock @vueuse/core
|
|
vi.mock('@vueuse/core', () => ({
|
|
useDebounceFn: (fn: (...args: any[]) => any) => fn // Return function directly for testing
|
|
}))
|
|
|
|
describe('useSpatialIndex', () => {
|
|
let spatialIndex: ReturnType<typeof useSpatialIndex>
|
|
|
|
beforeEach(() => {
|
|
spatialIndex = useSpatialIndex()
|
|
})
|
|
|
|
describe('initialization', () => {
|
|
it('should start with null quadTree', () => {
|
|
expect(spatialIndex.quadTree.value).toBeNull()
|
|
})
|
|
|
|
it('should initialize with default bounds when first node is added', () => {
|
|
const { updateNode, quadTree, metrics } = spatialIndex
|
|
|
|
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
|
|
|
|
expect(quadTree.value).not.toBeNull()
|
|
expect(metrics.value.totalNodes).toBe(1)
|
|
})
|
|
|
|
it('should initialize with custom bounds', () => {
|
|
const { initialize, quadTree } = spatialIndex
|
|
const customBounds = createBounds(0, 0, 5000, 3000)
|
|
|
|
initialize(customBounds)
|
|
|
|
expect(quadTree.value).not.toBeNull()
|
|
})
|
|
|
|
it('should increment rebuild count on initialization', () => {
|
|
const { initialize, metrics } = spatialIndex
|
|
|
|
expect(metrics.value.rebuildCount).toBe(0)
|
|
initialize()
|
|
expect(metrics.value.rebuildCount).toBe(1)
|
|
})
|
|
|
|
it('should accept custom options', () => {
|
|
const customIndex = useSpatialIndex({
|
|
maxDepth: 8,
|
|
maxItemsPerNode: 6,
|
|
updateDebounceMs: 32
|
|
})
|
|
|
|
customIndex.initialize()
|
|
|
|
expect(customIndex.quadTree.value).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('updateNode', () => {
|
|
it('should add a new node to the index', () => {
|
|
const { updateNode, metrics } = spatialIndex
|
|
|
|
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
|
|
|
|
expect(metrics.value.totalNodes).toBe(1)
|
|
})
|
|
|
|
it('should update existing node position', () => {
|
|
const { updateNode, queryViewport } = spatialIndex
|
|
|
|
// Add node
|
|
updateNode('node1', { x: 100, y: 100 }, { width: 200, height: 100 })
|
|
|
|
// Move node
|
|
updateNode('node1', { x: 500, y: 500 }, { width: 200, height: 100 })
|
|
|
|
// Query old position - should not find node
|
|
const oldResults = queryViewport(createBounds(50, 50, 300, 200))
|
|
expect(oldResults).not.toContain('node1')
|
|
|
|
// Query new position - should find node
|
|
const newResults = queryViewport(createBounds(450, 450, 300, 200))
|
|
expect(newResults).toContain('node1')
|
|
})
|
|
|
|
it('should auto-initialize if quadTree is null', () => {
|
|
const { updateNode, quadTree } = spatialIndex
|
|
|
|
expect(quadTree.value).toBeNull()
|
|
updateNode('node1', { x: 0, y: 0 }, { width: 100, height: 100 })
|
|
expect(quadTree.value).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('batchUpdate', () => {
|
|
it('should update multiple nodes at once', () => {
|
|
const { batchUpdate, metrics } = spatialIndex
|
|
|
|
const updates = [
|
|
{
|
|
id: 'node1',
|
|
position: { x: 100, y: 100 },
|
|
size: { width: 200, height: 100 }
|
|
},
|
|
{
|
|
id: 'node2',
|
|
position: { x: 300, y: 300 },
|
|
size: { width: 150, height: 150 }
|
|
},
|
|
{
|
|
id: 'node3',
|
|
position: { x: 500, y: 200 },
|
|
size: { width: 100, height: 200 }
|
|
}
|
|
]
|
|
|
|
batchUpdate(updates)
|
|
|
|
expect(metrics.value.totalNodes).toBe(3)
|
|
})
|
|
|
|
it('should handle empty batch', () => {
|
|
const { batchUpdate, metrics } = spatialIndex
|
|
|
|
batchUpdate([])
|
|
|
|
expect(metrics.value.totalNodes).toBe(0)
|
|
})
|
|
|
|
it('should auto-initialize if needed', () => {
|
|
const { batchUpdate, quadTree } = spatialIndex
|
|
|
|
expect(quadTree.value).toBeNull()
|
|
batchUpdate([
|
|
{
|
|
id: 'node1',
|
|
position: { x: 0, y: 0 },
|
|
size: { width: 100, height: 100 }
|
|
}
|
|
])
|
|
expect(quadTree.value).not.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('removeNode', () => {
|
|
beforeEach(() => {
|
|
spatialIndex.updateNode(
|
|
'node1',
|
|
{ x: 100, y: 100 },
|
|
{ width: 200, height: 100 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'node2',
|
|
{ x: 300, y: 300 },
|
|
{ width: 200, height: 100 }
|
|
)
|
|
})
|
|
|
|
it('should remove node from index', () => {
|
|
const { removeNode, metrics } = spatialIndex
|
|
|
|
expect(metrics.value.totalNodes).toBe(2)
|
|
removeNode('node1')
|
|
expect(metrics.value.totalNodes).toBe(1)
|
|
})
|
|
|
|
it('should handle removing non-existent node', () => {
|
|
const { removeNode, metrics } = spatialIndex
|
|
|
|
expect(metrics.value.totalNodes).toBe(2)
|
|
removeNode('node999')
|
|
expect(metrics.value.totalNodes).toBe(2)
|
|
})
|
|
|
|
it('should handle removeNode when quadTree is null', () => {
|
|
const freshIndex = useSpatialIndex()
|
|
|
|
// Should not throw
|
|
expect(() => freshIndex.removeNode('node1')).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('queryViewport', () => {
|
|
beforeEach(() => {
|
|
// Set up a grid of nodes
|
|
spatialIndex.updateNode(
|
|
'node1',
|
|
{ x: 0, y: 0 },
|
|
{ width: 100, height: 100 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'node2',
|
|
{ x: 200, y: 0 },
|
|
{ width: 100, height: 100 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'node3',
|
|
{ x: 0, y: 200 },
|
|
{ width: 100, height: 100 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'node4',
|
|
{ x: 200, y: 200 },
|
|
{ width: 100, height: 100 }
|
|
)
|
|
})
|
|
|
|
it('should find nodes within viewport bounds', () => {
|
|
const { queryViewport } = spatialIndex
|
|
|
|
// Query top-left quadrant
|
|
const results = queryViewport(createBounds(-50, -50, 200, 200))
|
|
expect(results).toContain('node1')
|
|
expect(results).not.toContain('node2')
|
|
expect(results).not.toContain('node3')
|
|
expect(results).not.toContain('node4')
|
|
})
|
|
|
|
it('should find multiple nodes in larger viewport', () => {
|
|
const { queryViewport } = spatialIndex
|
|
|
|
// Query entire area
|
|
const results = queryViewport(createBounds(-50, -50, 400, 400))
|
|
expect(results).toHaveLength(4)
|
|
expect(results).toContain('node1')
|
|
expect(results).toContain('node2')
|
|
expect(results).toContain('node3')
|
|
expect(results).toContain('node4')
|
|
})
|
|
|
|
it('should return empty array for empty region', () => {
|
|
const { queryViewport } = spatialIndex
|
|
|
|
const results = queryViewport(createBounds(1000, 1000, 100, 100))
|
|
expect(results).toEqual([])
|
|
})
|
|
|
|
it('should update metrics after query', () => {
|
|
const { queryViewport, metrics } = spatialIndex
|
|
|
|
queryViewport(createBounds(0, 0, 300, 300))
|
|
|
|
expect(metrics.value.queryTime).toBeGreaterThan(0)
|
|
expect(metrics.value.visibleNodes).toBe(4)
|
|
})
|
|
|
|
it('should handle query when quadTree is null', () => {
|
|
const freshIndex = useSpatialIndex()
|
|
|
|
const results = freshIndex.queryViewport(createBounds(0, 0, 100, 100))
|
|
expect(results).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('queryRadius', () => {
|
|
beforeEach(() => {
|
|
// Set up nodes at different distances
|
|
spatialIndex.updateNode(
|
|
'center',
|
|
{ x: 475, y: 475 },
|
|
{ width: 50, height: 50 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'near1',
|
|
{ x: 525, y: 475 },
|
|
{ width: 50, height: 50 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'near2',
|
|
{ x: 425, y: 475 },
|
|
{ width: 50, height: 50 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'far',
|
|
{ x: 775, y: 775 },
|
|
{ width: 50, height: 50 }
|
|
)
|
|
})
|
|
|
|
it('should find nodes within radius', () => {
|
|
const { queryRadius } = spatialIndex
|
|
|
|
const results = queryRadius({ x: 500, y: 500 }, 100)
|
|
|
|
expect(results).toContain('center')
|
|
expect(results).toContain('near1')
|
|
expect(results).toContain('near2')
|
|
expect(results).not.toContain('far')
|
|
})
|
|
|
|
it('should handle zero radius', () => {
|
|
const { queryRadius } = spatialIndex
|
|
|
|
const results = queryRadius({ x: 500, y: 500 }, 0)
|
|
|
|
// Zero radius creates a point query at (500,500)
|
|
// The 'center' node spans 475-525 on both axes, so it contains this point
|
|
expect(results).toContain('center')
|
|
})
|
|
|
|
it('should handle large radius', () => {
|
|
const { queryRadius } = spatialIndex
|
|
|
|
const results = queryRadius({ x: 500, y: 500 }, 1000)
|
|
|
|
expect(results).toHaveLength(4) // Should find all nodes
|
|
})
|
|
})
|
|
|
|
describe('clear', () => {
|
|
beforeEach(() => {
|
|
spatialIndex.updateNode(
|
|
'node1',
|
|
{ x: 100, y: 100 },
|
|
{ width: 200, height: 100 }
|
|
)
|
|
spatialIndex.updateNode(
|
|
'node2',
|
|
{ x: 300, y: 300 },
|
|
{ width: 200, height: 100 }
|
|
)
|
|
})
|
|
|
|
it('should remove all nodes', () => {
|
|
const { clear, metrics } = spatialIndex
|
|
|
|
expect(metrics.value.totalNodes).toBe(2)
|
|
clear()
|
|
expect(metrics.value.totalNodes).toBe(0)
|
|
})
|
|
|
|
it('should reset metrics', () => {
|
|
const { clear, queryViewport, metrics } = spatialIndex
|
|
|
|
// Do a query to set visible nodes
|
|
queryViewport(createBounds(0, 0, 500, 500))
|
|
expect(metrics.value.visibleNodes).toBe(2)
|
|
|
|
clear()
|
|
expect(metrics.value.visibleNodes).toBe(0)
|
|
})
|
|
|
|
it('should handle clear when quadTree is null', () => {
|
|
const freshIndex = useSpatialIndex()
|
|
|
|
expect(() => freshIndex.clear()).not.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('rebuild', () => {
|
|
it('should rebuild index with new nodes', () => {
|
|
const { rebuild, metrics, queryViewport } = spatialIndex
|
|
|
|
// Add initial nodes
|
|
spatialIndex.updateNode(
|
|
'old1',
|
|
{ x: 0, y: 0 },
|
|
{ width: 100, height: 100 }
|
|
)
|
|
expect(metrics.value.rebuildCount).toBe(1)
|
|
|
|
// Rebuild with new set
|
|
const newNodes = new Map([
|
|
[
|
|
'new1',
|
|
{ position: { x: 100, y: 100 }, size: { width: 50, height: 50 } }
|
|
],
|
|
[
|
|
'new2',
|
|
{ position: { x: 200, y: 200 }, size: { width: 50, height: 50 } }
|
|
]
|
|
])
|
|
|
|
rebuild(newNodes)
|
|
|
|
expect(metrics.value.totalNodes).toBe(2)
|
|
expect(metrics.value.rebuildCount).toBe(2)
|
|
|
|
// Old nodes should be gone
|
|
const oldResults = queryViewport(createBounds(-50, -50, 100, 100))
|
|
expect(oldResults).not.toContain('old1')
|
|
|
|
// New nodes should be findable
|
|
const newResults = queryViewport(createBounds(50, 50, 200, 200))
|
|
expect(newResults).toContain('new1')
|
|
expect(newResults).toContain('new2')
|
|
})
|
|
|
|
it('should handle empty rebuild', () => {
|
|
const { rebuild, metrics } = spatialIndex
|
|
|
|
rebuild(new Map())
|
|
|
|
expect(metrics.value.totalNodes).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('getDebugVisualization', () => {
|
|
it('should return null when debug is disabled', () => {
|
|
const { getDebugVisualization } = spatialIndex
|
|
|
|
expect(getDebugVisualization()).toBeNull()
|
|
})
|
|
|
|
it('should return debug info when enabled', () => {
|
|
const debugIndex = useSpatialIndex({ enableDebugVisualization: true })
|
|
debugIndex.initialize()
|
|
|
|
const debug = debugIndex.getDebugVisualization()
|
|
expect(debug).not.toBeNull()
|
|
expect(debug).toHaveProperty('size')
|
|
expect(debug).toHaveProperty('tree')
|
|
})
|
|
})
|
|
|
|
describe('metrics', () => {
|
|
it('should track performance metrics', () => {
|
|
const { metrics, updateNode, queryViewport } = spatialIndex
|
|
|
|
// Initial state
|
|
expect(metrics.value).toEqual({
|
|
queryTime: 0,
|
|
totalNodes: 0,
|
|
visibleNodes: 0,
|
|
treeDepth: 0,
|
|
rebuildCount: 0
|
|
})
|
|
|
|
// Add nodes
|
|
updateNode('node1', { x: 0, y: 0 }, { width: 100, height: 100 })
|
|
expect(metrics.value.totalNodes).toBe(1)
|
|
|
|
// Query
|
|
queryViewport(createBounds(-50, -50, 200, 200))
|
|
expect(metrics.value.queryTime).toBeGreaterThan(0)
|
|
expect(metrics.value.visibleNodes).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle nodes with zero size', () => {
|
|
const { updateNode, queryViewport } = spatialIndex
|
|
|
|
updateNode('point', { x: 100, y: 100 }, { width: 0, height: 0 })
|
|
|
|
// Should still be findable
|
|
const results = queryViewport(createBounds(50, 50, 100, 100))
|
|
expect(results).toContain('point')
|
|
})
|
|
|
|
it('should handle negative positions', () => {
|
|
const { updateNode, queryViewport } = spatialIndex
|
|
|
|
updateNode('negative', { x: -500, y: -500 }, { width: 100, height: 100 })
|
|
|
|
const results = queryViewport(createBounds(-600, -600, 200, 200))
|
|
expect(results).toContain('negative')
|
|
})
|
|
|
|
it('should handle very large nodes', () => {
|
|
const { updateNode, queryViewport } = spatialIndex
|
|
|
|
updateNode('huge', { x: 0, y: 0 }, { width: 5000, height: 5000 })
|
|
|
|
// Should be found even when querying small area within it
|
|
const results = queryViewport(createBounds(100, 100, 10, 10))
|
|
expect(results).toContain('huge')
|
|
})
|
|
})
|
|
|
|
describe('debouncedUpdateNode', () => {
|
|
it('should be available', () => {
|
|
const { debouncedUpdateNode } = spatialIndex
|
|
|
|
expect(debouncedUpdateNode).toBeDefined()
|
|
expect(typeof debouncedUpdateNode).toBe('function')
|
|
})
|
|
})
|
|
})
|