[test] Add comprehensive tests for transform and spatial composables

- Add useTransformState tests covering coordinate conversion, viewport culling
- Add useSpatialIndex tests for QuadTree operations and spatial queries
- Test camera state management and transform synchronization
- Verify spatial indexing performance and correctness
- Cover edge cases and error conditions for robust testing
This commit is contained in:
bymyself
2025-07-05 00:05:25 -07:00
parent 95ab7022e5
commit a58a35459f
3 changed files with 953 additions and 0 deletions

View File

@@ -0,0 +1,329 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { useTransformState } from '@/composables/element/useTransformState'
import { createMockCanvasContext } from '../../helpers/nodeTestHelpers'
describe('useTransformState', () => {
let transformState: ReturnType<typeof useTransformState>
beforeEach(() => {
transformState = useTransformState()
})
describe('initial state', () => {
it('should initialize with default camera values', () => {
const { camera } = transformState
expect(camera.x).toBe(0)
expect(camera.y).toBe(0)
expect(camera.z).toBe(1)
})
it('should generate correct initial transform style', () => {
const { transformStyle } = transformState
expect(transformStyle.value).toEqual({
transform: 'scale(1) translate(0px, 0px)',
transformOrigin: '0 0'
})
})
})
describe('syncWithCanvas', () => {
it('should sync camera state with canvas transform', () => {
const { syncWithCanvas, camera } = transformState
const mockCanvas = createMockCanvasContext()
// Set mock canvas transform
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
syncWithCanvas(mockCanvas as any)
expect(camera.x).toBe(100)
expect(camera.y).toBe(50)
expect(camera.z).toBe(2)
})
it('should handle null canvas gracefully', () => {
const { syncWithCanvas, camera } = transformState
syncWithCanvas(null as any)
// Should remain at initial values
expect(camera.x).toBe(0)
expect(camera.y).toBe(0)
expect(camera.z).toBe(1)
})
it('should handle canvas without ds property', () => {
const { syncWithCanvas, camera } = transformState
const canvasWithoutDs = { canvas: {} }
syncWithCanvas(canvasWithoutDs as any)
// Should remain at initial values
expect(camera.x).toBe(0)
expect(camera.y).toBe(0)
expect(camera.z).toBe(1)
})
it('should update transform style after sync', () => {
const { syncWithCanvas, transformStyle } = transformState
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [150, 75]
mockCanvas.ds.scale = 0.5
syncWithCanvas(mockCanvas as any)
expect(transformStyle.value).toEqual({
transform: 'scale(0.5) translate(150px, 75px)',
transformOrigin: '0 0'
})
})
})
describe('coordinate conversions', () => {
beforeEach(() => {
// Set up a known transform state
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
})
describe('canvasToScreen', () => {
it('should convert canvas coordinates to screen coordinates', () => {
const { canvasToScreen } = transformState
const canvasPoint = { x: 10, y: 20 }
const screenPoint = canvasToScreen(canvasPoint)
// screen = canvas * scale + offset
// x: 10 * 2 + 100 = 120
// y: 20 * 2 + 50 = 90
expect(screenPoint).toEqual({ x: 120, y: 90 })
})
it('should handle zero coordinates', () => {
const { canvasToScreen } = transformState
const screenPoint = canvasToScreen({ x: 0, y: 0 })
expect(screenPoint).toEqual({ x: 100, y: 50 })
})
it('should handle negative coordinates', () => {
const { canvasToScreen } = transformState
const screenPoint = canvasToScreen({ x: -10, y: -20 })
expect(screenPoint).toEqual({ x: 80, y: 10 })
})
})
describe('screenToCanvas', () => {
it('should convert screen coordinates to canvas coordinates', () => {
const { screenToCanvas } = transformState
const screenPoint = { x: 120, y: 90 }
const canvasPoint = screenToCanvas(screenPoint)
// canvas = (screen - offset) / scale
// x: (120 - 100) / 2 = 10
// y: (90 - 50) / 2 = 20
expect(canvasPoint).toEqual({ x: 10, y: 20 })
})
it('should be inverse of canvasToScreen', () => {
const { canvasToScreen, screenToCanvas } = transformState
const originalPoint = { x: 25, y: 35 }
const screenPoint = canvasToScreen(originalPoint)
const backToCanvas = screenToCanvas(screenPoint)
expect(backToCanvas.x).toBeCloseTo(originalPoint.x)
expect(backToCanvas.y).toBeCloseTo(originalPoint.y)
})
})
})
describe('getNodeScreenBounds', () => {
beforeEach(() => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
})
it('should calculate correct screen bounds for a node', () => {
const { getNodeScreenBounds } = transformState
const nodePos = [10, 20]
const nodeSize = [200, 100]
const bounds = getNodeScreenBounds(nodePos, nodeSize)
// Top-left: canvasToScreen(10, 20) = (120, 90)
// Width: 200 * 2 = 400
// Height: 100 * 2 = 200
expect(bounds.x).toBe(120)
expect(bounds.y).toBe(90)
expect(bounds.width).toBe(400)
expect(bounds.height).toBe(200)
})
})
describe('isNodeInViewport', () => {
beforeEach(() => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [0, 0]
mockCanvas.ds.scale = 1
transformState.syncWithCanvas(mockCanvas as any)
})
const viewport = { width: 1000, height: 600 }
it('should return true for nodes inside viewport', () => {
const { isNodeInViewport } = transformState
const nodePos = [100, 100]
const nodeSize = [200, 100]
expect(isNodeInViewport(nodePos, nodeSize, viewport)).toBe(true)
})
it('should return false for nodes completely outside viewport', () => {
const { isNodeInViewport } = transformState
// Node far to the right
expect(isNodeInViewport([2000, 100], [200, 100], viewport)).toBe(false)
// Node far to the left
expect(isNodeInViewport([-500, 100], [200, 100], viewport)).toBe(false)
// Node far below
expect(isNodeInViewport([100, 1000], [200, 100], viewport)).toBe(false)
// Node far above
expect(isNodeInViewport([100, -500], [200, 100], viewport)).toBe(false)
})
it('should return true for nodes partially in viewport with margin', () => {
const { isNodeInViewport } = transformState
// Node slightly outside but within margin
const nodePos = [-50, -50]
const nodeSize = [100, 100]
expect(isNodeInViewport(nodePos, nodeSize, viewport, 0.2)).toBe(true)
})
it('should return false for tiny nodes (size culling)', () => {
const { isNodeInViewport } = transformState
// Node is in viewport but too small
const nodePos = [100, 100]
const nodeSize = [3, 3] // Less than 4 pixels
expect(isNodeInViewport(nodePos, nodeSize, viewport)).toBe(false)
})
it('should adjust margin based on zoom level', () => {
const { isNodeInViewport, syncWithCanvas } = transformState
const mockCanvas = createMockCanvasContext()
// Test with very low zoom
mockCanvas.ds.scale = 0.05
syncWithCanvas(mockCanvas as any)
// Node at edge should still be visible due to increased margin
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(true)
// Test with high zoom
mockCanvas.ds.scale = 4
syncWithCanvas(mockCanvas as any)
// Margin should be tighter
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(false)
})
})
describe('getViewportBounds', () => {
beforeEach(() => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
})
it('should calculate viewport bounds in canvas coordinates', () => {
const { getViewportBounds } = transformState
const viewport = { width: 1000, height: 600 }
const bounds = getViewportBounds(viewport, 0.2)
// With 20% margin:
// marginX = 1000 * 0.2 = 200
// marginY = 600 * 0.2 = 120
// topLeft in screen: (-200, -120)
// bottomRight in screen: (1200, 720)
// Convert to canvas coordinates:
// topLeft: ((-200 - 100) / 2, (-120 - 50) / 2) = (-150, -85)
// bottomRight: ((1200 - 100) / 2, (720 - 50) / 2) = (550, 335)
expect(bounds.x).toBe(-150)
expect(bounds.y).toBe(-85)
expect(bounds.width).toBe(700) // 550 - (-150)
expect(bounds.height).toBe(420) // 335 - (-85)
})
it('should handle zero margin', () => {
const { getViewportBounds } = transformState
const viewport = { width: 1000, height: 600 }
const bounds = getViewportBounds(viewport, 0)
// No margin, so viewport bounds are exact
expect(bounds.x).toBe(-50) // (0 - 100) / 2
expect(bounds.y).toBe(-25) // (0 - 50) / 2
expect(bounds.width).toBe(500) // 1000 / 2
expect(bounds.height).toBe(300) // 600 / 2
})
})
describe('edge cases', () => {
it('should handle extreme zoom levels', () => {
const { syncWithCanvas, canvasToScreen } = transformState
const mockCanvas = createMockCanvasContext()
// Very small zoom
mockCanvas.ds.scale = 0.001
syncWithCanvas(mockCanvas as any)
const point1 = canvasToScreen({ x: 1000, y: 1000 })
expect(point1.x).toBeCloseTo(1)
expect(point1.y).toBeCloseTo(1)
// Very large zoom
mockCanvas.ds.scale = 100
syncWithCanvas(mockCanvas as any)
const point2 = canvasToScreen({ x: 1, y: 1 })
expect(point2.x).toBe(100)
expect(point2.y).toBe(100)
})
it('should handle zero scale in screenToCanvas', () => {
const { syncWithCanvas, screenToCanvas } = transformState
const mockCanvas = createMockCanvasContext()
// Scale of 0 gets converted to 1 by || operator
mockCanvas.ds.scale = 0
syncWithCanvas(mockCanvas as any)
// Should use scale of 1 due to camera.z || 1 in implementation
const result = screenToCanvas({ x: 100, y: 100 })
expect(result.x).toBe(100) // (100 - 0) / 1
expect(result.y).toBe(100) // (100 - 0) / 1
})
})
})

View File

@@ -0,0 +1,483 @@
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')
})
})
})

View File

@@ -0,0 +1,141 @@
import { describe, expect, it } from 'vitest'
import { WidgetType } from '@/components/graph/vueWidgets/widgetRegistry'
import { useWidgetRenderer } from '@/composables/graph/useWidgetRenderer'
describe('useWidgetRenderer', () => {
const { getWidgetComponent, shouldRenderAsVue } = useWidgetRenderer()
describe('getWidgetComponent', () => {
// Test number type mappings
describe('number types', () => {
it('should map number type to NUMBER widget', () => {
expect(getWidgetComponent('number')).toBe(WidgetType.NUMBER)
})
it('should map slider type to SLIDER widget', () => {
expect(getWidgetComponent('slider')).toBe(WidgetType.SLIDER)
})
it('should map INT type to INT widget', () => {
expect(getWidgetComponent('INT')).toBe(WidgetType.INT)
})
it('should map FLOAT type to FLOAT widget', () => {
expect(getWidgetComponent('FLOAT')).toBe(WidgetType.FLOAT)
})
})
// Test text type mappings
describe('text types', () => {
it('should map text variations to STRING widget', () => {
expect(getWidgetComponent('text')).toBe(WidgetType.STRING)
expect(getWidgetComponent('string')).toBe(WidgetType.STRING)
expect(getWidgetComponent('STRING')).toBe(WidgetType.STRING)
})
it('should map multiline text types to TEXTAREA widget', () => {
expect(getWidgetComponent('multiline')).toBe(WidgetType.TEXTAREA)
expect(getWidgetComponent('textarea')).toBe(WidgetType.TEXTAREA)
expect(getWidgetComponent('MARKDOWN')).toBe(WidgetType.TEXTAREA)
expect(getWidgetComponent('customtext')).toBe(WidgetType.TEXTAREA)
})
})
// Test selection type mappings
describe('selection types', () => {
it('should map combo types to COMBO widget', () => {
expect(getWidgetComponent('combo')).toBe(WidgetType.COMBO)
expect(getWidgetComponent('COMBO')).toBe(WidgetType.COMBO)
})
})
// Test boolean type mappings
describe('boolean types', () => {
it('should map boolean types to appropriate widgets', () => {
expect(getWidgetComponent('toggle')).toBe(WidgetType.TOGGLESWITCH)
expect(getWidgetComponent('boolean')).toBe(WidgetType.BOOLEAN)
expect(getWidgetComponent('BOOLEAN')).toBe(WidgetType.BOOLEAN)
})
})
// Test advanced widget mappings
describe('advanced widgets', () => {
it('should map color types to COLOR widget', () => {
expect(getWidgetComponent('color')).toBe(WidgetType.COLOR)
expect(getWidgetComponent('COLOR')).toBe(WidgetType.COLOR)
})
it('should map image types to IMAGE widget', () => {
expect(getWidgetComponent('image')).toBe(WidgetType.IMAGE)
expect(getWidgetComponent('IMAGE')).toBe(WidgetType.IMAGE)
})
it('should map file types to FILEUPLOAD widget', () => {
expect(getWidgetComponent('file')).toBe(WidgetType.FILEUPLOAD)
expect(getWidgetComponent('FILEUPLOAD')).toBe(WidgetType.FILEUPLOAD)
})
it('should map button types to BUTTON widget', () => {
expect(getWidgetComponent('button')).toBe(WidgetType.BUTTON)
expect(getWidgetComponent('BUTTON')).toBe(WidgetType.BUTTON)
})
})
// Test fallback behavior
describe('fallback behavior', () => {
it('should return STRING widget for unknown types', () => {
expect(getWidgetComponent('unknown')).toBe(WidgetType.STRING)
expect(getWidgetComponent('custom_widget')).toBe(WidgetType.STRING)
expect(getWidgetComponent('')).toBe(WidgetType.STRING)
})
it('should return STRING widget for unmapped but valid types', () => {
expect(getWidgetComponent('datetime')).toBe(WidgetType.STRING)
expect(getWidgetComponent('json')).toBe(WidgetType.STRING)
})
})
})
describe('shouldRenderAsVue', () => {
it('should return false for widgets marked as canvas-only', () => {
const widget = { type: 'text', options: { canvasOnly: true } }
expect(shouldRenderAsVue(widget)).toBe(false)
})
it('should return false for widgets without a type', () => {
const widget = { options: {} }
expect(shouldRenderAsVue(widget)).toBe(false)
})
it('should return true for widgets with mapped types', () => {
expect(shouldRenderAsVue({ type: 'text' })).toBe(true)
expect(shouldRenderAsVue({ type: 'number' })).toBe(true)
expect(shouldRenderAsVue({ type: 'combo' })).toBe(true)
})
it('should return true even for unknown types (fallback to STRING)', () => {
expect(shouldRenderAsVue({ type: 'unknown_type' })).toBe(true)
})
it('should respect options while checking type', () => {
const widget = { type: 'text', options: { someOption: 'value' } }
expect(shouldRenderAsVue(widget)).toBe(true)
})
})
describe('edge cases', () => {
it('should handle widgets with empty options', () => {
const widget = { type: 'text', options: {} }
expect(shouldRenderAsVue(widget)).toBe(true)
})
it('should handle case sensitivity correctly', () => {
// Test that both lowercase and uppercase work
expect(getWidgetComponent('string')).toBe(WidgetType.STRING)
expect(getWidgetComponent('STRING')).toBe(WidgetType.STRING)
expect(getWidgetComponent('combo')).toBe(WidgetType.COMBO)
expect(getWidgetComponent('COMBO')).toBe(WidgetType.COMBO)
})
})
})