mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 20:51:58 +00:00
[refactor] Migrate minimap to domain-driven renderer architecture (#5069)
* move ref initialization to the component * remove redundant init * [refactor] Move minimap to domain-driven renderer structure - Create new src/renderer/extensions/minimap/ structure following domain-driven design - Add composables: useMinimapGraph, useMinimapViewport, useMinimapRenderer, useMinimapInteraction, useMinimapSettings - Add minimapCanvasRenderer with efficient batched rendering - Add comprehensive type definitions in types.ts - Remove old src/composables/useMinimap.ts composable - Implement proper separation of concerns with dedicated composables for each domain The new structure provides cleaner APIs, better performance through batched rendering, and improved maintainability through domain separation. * [test] Fix minimap tests for new renderer structure - Update all test imports to use new renderer paths - Fix mock implementations to match new composable APIs - Add proper RAF mocking for throttled functions - Fix type assertions to handle strict TypeScript checks - Update test expectations for new implementation behavior - Fix viewport transform calculations in tests - Handle async/throttled behavior correctly in tests All 28 minimap tests now passing with new architecture. * [fix] Remove unused init import in MiniMap component * [refactor] Move useWorkflowThumbnail to renderer/thumbnail structure - Moved useWorkflowThumbnail from src/composables to src/renderer/thumbnail/composables - Updated all imports in components, stores and services - Moved test file to match new structure - This ensures all rendering-related composables live in the renderer directory * [test] Fix minimap canvas renderer test for connections - Fixed mock setup for graph links to match LiteGraph's hybrid Map/Object structure - LiteGraph expects links to be accessible both as a Map and as an object - Test now properly verifies connection rendering functionality
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { useThrottleFn } from '@vueuse/core'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapGraph } from '@/renderer/extensions/minimap/composables/useMinimapGraph'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
useThrottleFn: vi.fn((fn) => fn)
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('useMinimapGraph', () => {
|
||||
let mockGraph: LGraph
|
||||
let onGraphChangedMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockGraph = {
|
||||
id: 'test-graph-123',
|
||||
_nodes: [
|
||||
{ id: '1', pos: [100, 100], size: [150, 80] },
|
||||
{ id: '2', pos: [300, 200], size: [120, 60] }
|
||||
],
|
||||
links: { link1: { id: 'link1' } },
|
||||
onNodeAdded: vi.fn(),
|
||||
onNodeRemoved: vi.fn(),
|
||||
onConnectionChange: vi.fn()
|
||||
} as any
|
||||
|
||||
onGraphChangedMock = vi.fn()
|
||||
})
|
||||
|
||||
it('should initialize with empty state', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
expect(graphManager.updateFlags.value).toEqual({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should setup event listeners on init', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.init()
|
||||
|
||||
expect(api.addEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should wrap graph callbacks on setup', () => {
|
||||
const originalOnNodeAdded = vi.fn()
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
const originalOnConnectionChange = vi.fn()
|
||||
|
||||
mockGraph.onNodeAdded = originalOnNodeAdded
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
mockGraph.onConnectionChange = originalOnConnectionChange
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
// Should wrap the callbacks
|
||||
expect(mockGraph.onNodeAdded).not.toBe(originalOnNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).not.toBe(originalOnNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).not.toBe(originalOnConnectionChange)
|
||||
|
||||
// Test wrapped callbacks
|
||||
const testNode = { id: '3' } as LGraphNode
|
||||
mockGraph.onNodeAdded!(testNode)
|
||||
|
||||
expect(originalOnNodeAdded).toHaveBeenCalledWith(testNode)
|
||||
expect(onGraphChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should prevent duplicate event listener setup', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Store original callbacks for comparison
|
||||
// const originalCallbacks = {
|
||||
// onNodeAdded: mockGraph.onNodeAdded,
|
||||
// onNodeRemoved: mockGraph.onNodeRemoved,
|
||||
// onConnectionChange: mockGraph.onConnectionChange
|
||||
// }
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
const wrappedCallbacks = {
|
||||
onNodeAdded: mockGraph.onNodeAdded,
|
||||
onNodeRemoved: mockGraph.onNodeRemoved,
|
||||
onConnectionChange: mockGraph.onConnectionChange
|
||||
}
|
||||
|
||||
// Setup again - should not re-wrap
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
expect(mockGraph.onNodeAdded).toBe(wrappedCallbacks.onNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).toBe(wrappedCallbacks.onNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).toBe(
|
||||
wrappedCallbacks.onConnectionChange
|
||||
)
|
||||
})
|
||||
|
||||
it('should cleanup event listeners properly', () => {
|
||||
const originalOnNodeAdded = vi.fn()
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
const originalOnConnectionChange = vi.fn()
|
||||
|
||||
mockGraph.onNodeAdded = originalOnNodeAdded
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
mockGraph.onConnectionChange = originalOnConnectionChange
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
graphManager.cleanupEventListeners()
|
||||
|
||||
// Should restore original callbacks
|
||||
expect(mockGraph.onNodeAdded).toBe(originalOnNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).toBe(originalOnNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).toBe(originalOnConnectionChange)
|
||||
})
|
||||
|
||||
it('should handle cleanup for never-setup graph', () => {
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.cleanupEventListeners()
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should detect node position changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// First check - cache initial state
|
||||
let hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true) // Initial cache population
|
||||
|
||||
// No changes
|
||||
hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(false)
|
||||
|
||||
// Change node position
|
||||
mockGraph._nodes[0].pos = [200, 150]
|
||||
hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
expect(graphManager.updateFlags.value.nodes).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect node count changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Add a node
|
||||
mockGraph._nodes.push({ id: '3', pos: [400, 300], size: [100, 50] } as any)
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
expect(graphManager.updateFlags.value.nodes).toBe(true)
|
||||
})
|
||||
|
||||
it('should detect connection changes', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Change connections
|
||||
mockGraph.links = new Map([
|
||||
[1, { id: 1 }],
|
||||
[2, { id: 2 }]
|
||||
]) as any
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.connections).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle node removal in callbacks', () => {
|
||||
const originalOnNodeRemoved = vi.fn()
|
||||
mockGraph.onNodeRemoved = originalOnNodeRemoved
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
const removedNode = { id: '2' } as LGraphNode
|
||||
mockGraph.onNodeRemoved!(removedNode)
|
||||
|
||||
expect(originalOnNodeRemoved).toHaveBeenCalledWith(removedNode)
|
||||
expect(onGraphChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should destroy properly', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.init()
|
||||
graphManager.setupEventListeners()
|
||||
graphManager.destroy()
|
||||
|
||||
expect(api.removeEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('should clear cache', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Populate cache
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Clear cache
|
||||
graphManager.clearCache()
|
||||
|
||||
// Should detect changes again after clear
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null graph gracefully', () => {
|
||||
const graphRef = ref(null as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
expect(() => graphManager.setupEventListeners()).not.toThrow()
|
||||
expect(() => graphManager.cleanupEventListeners()).not.toThrow()
|
||||
expect(graphManager.checkForChanges()).toBe(false)
|
||||
})
|
||||
|
||||
it('should clean up removed nodes from cache', () => {
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
// Cache initial state
|
||||
graphManager.checkForChanges()
|
||||
|
||||
// Remove a node
|
||||
mockGraph._nodes = mockGraph._nodes.filter((n) => n.id !== '2')
|
||||
|
||||
const hasChanges = graphManager.checkForChanges()
|
||||
expect(hasChanges).toBe(true)
|
||||
expect(graphManager.updateFlags.value.bounds).toBe(true)
|
||||
})
|
||||
|
||||
it('should throttle graph changed callback', () => {
|
||||
const throttledFn = vi.fn()
|
||||
vi.mocked(useThrottleFn).mockReturnValue(throttledFn)
|
||||
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
|
||||
|
||||
graphManager.setupEventListeners()
|
||||
|
||||
// Trigger multiple changes rapidly
|
||||
mockGraph.onNodeAdded!({ id: '3' } as LGraphNode)
|
||||
mockGraph.onNodeAdded!({ id: '4' } as LGraphNode)
|
||||
mockGraph.onNodeAdded!({ id: '5' } as LGraphNode)
|
||||
|
||||
// Should be throttled
|
||||
expect(throttledFn).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,328 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useMinimapInteraction } from '@/renderer/extensions/minimap/composables/useMinimapInteraction'
|
||||
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
describe('useMinimapInteraction', () => {
|
||||
let mockContainer: HTMLDivElement
|
||||
let mockCanvas: MinimapCanvas
|
||||
let centerViewOnMock: ReturnType<typeof vi.fn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContainer = {
|
||||
getBoundingClientRect: vi.fn().mockReturnValue({
|
||||
left: 100,
|
||||
top: 50,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
} as any
|
||||
|
||||
centerViewOnMock = vi.fn()
|
||||
})
|
||||
|
||||
it('should initialize with default values', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(false)
|
||||
expect(interaction.containerRect.value).toEqual({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
})
|
||||
|
||||
it('should update container rect', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
interaction.updateContainerRect()
|
||||
|
||||
expect(mockContainer.getBoundingClientRect).toHaveBeenCalled()
|
||||
|
||||
expect(interaction.containerRect.value).toEqual({
|
||||
left: 100,
|
||||
top: 50,
|
||||
width: 250,
|
||||
height: 200
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle pointer down and start dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
|
||||
interaction.handlePointerDown(event)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(true)
|
||||
expect(mockContainer.getBoundingClientRect).toHaveBeenCalled()
|
||||
expect(centerViewOnMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer move when dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Start dragging
|
||||
interaction.handlePointerDown(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
)
|
||||
|
||||
// Move pointer
|
||||
const moveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
interaction.handlePointerMove(moveEvent)
|
||||
|
||||
// Should calculate world coordinates and center view
|
||||
expect(centerViewOnMock).toHaveBeenCalledTimes(2) // Once on down, once on move
|
||||
|
||||
// Calculate expected world coordinates
|
||||
const x = 200 - 100 // clientX - containerLeft
|
||||
const y = 150 - 50 // clientY - containerTop
|
||||
const offsetX = (250 - 500 * 0.5) / 2 // (width - bounds.width * scale) / 2
|
||||
const offsetY = (200 - 400 * 0.5) / 2 // (height - bounds.height * scale) / 2
|
||||
const worldX = (x - offsetX) / 0.5 + 0 // (x - offsetX) / scale + bounds.minX
|
||||
const worldY = (y - offsetY) / 0.5 + 0 // (y - offsetY) / scale + bounds.minY
|
||||
|
||||
expect(centerViewOnMock).toHaveBeenLastCalledWith(worldX, worldY)
|
||||
})
|
||||
|
||||
it('should not move when not dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const moveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
interaction.handlePointerMove(moveEvent)
|
||||
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer up to stop dragging', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Start dragging
|
||||
interaction.handlePointerDown(
|
||||
new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 100
|
||||
})
|
||||
)
|
||||
|
||||
expect(interaction.isDragging.value).toBe(true)
|
||||
|
||||
interaction.handlePointerUp()
|
||||
|
||||
expect(interaction.isDragging.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle wheel events for zooming', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
wheelEvent.preventDefault = vi.fn()
|
||||
|
||||
interaction.handleWheel(wheelEvent)
|
||||
|
||||
// Should update canvas scale (zoom in)
|
||||
expect(mockCanvas.ds.scale).toBeCloseTo(1.1)
|
||||
expect(centerViewOnMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should respect zoom limits', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Set scale close to minimum
|
||||
mockCanvas.ds.scale = 0.11
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: 100, // Zoom out
|
||||
clientX: 200,
|
||||
clientY: 150
|
||||
})
|
||||
wheelEvent.preventDefault = vi.fn()
|
||||
|
||||
interaction.handleWheel(wheelEvent)
|
||||
|
||||
// Should not go below minimum scale
|
||||
expect(mockCanvas.ds.scale).toBe(0.11)
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle null container gracefully', () => {
|
||||
const containerRef = ref<HTMLDivElement | undefined>(undefined)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() => interaction.updateContainerRect()).not.toThrow()
|
||||
expect(() =>
|
||||
interaction.handlePointerDown(new PointerEvent('pointerdown'))
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const containerRef = ref(mockContainer)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 500, height: 400 })
|
||||
const scaleRef = ref(0.5)
|
||||
const canvasRef = ref(null as any)
|
||||
|
||||
const interaction = useMinimapInteraction(
|
||||
containerRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
250,
|
||||
200,
|
||||
centerViewOnMock,
|
||||
canvasRef
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() =>
|
||||
interaction.handlePointerMove(new PointerEvent('pointermove'))
|
||||
).not.toThrow()
|
||||
expect(() => interaction.handleWheel(new WheelEvent('wheel'))).not.toThrow()
|
||||
expect(centerViewOnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,267 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapRenderer } from '@/renderer/extensions/minimap/composables/useMinimapRenderer'
|
||||
import { renderMinimapToCanvas } from '@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
import type { UpdateFlags } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
vi.mock('@/renderer/extensions/minimap/minimapCanvasRenderer', () => ({
|
||||
renderMinimapToCanvas: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useMinimapRenderer', () => {
|
||||
let mockCanvas: HTMLCanvasElement
|
||||
let mockContext: CanvasRenderingContext2D
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContext = {
|
||||
clearRect: vi.fn()
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
getContext: vi.fn().mockReturnValue(mockContext)
|
||||
} as any
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [{ id: '1', pos: [0, 0], size: [100, 100] }]
|
||||
} as any
|
||||
})
|
||||
|
||||
it('should initialize with full redraw needed', () => {
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
expect(renderer.needsFullRedraw.value).toBe(true)
|
||||
expect(renderer.needsBoundsUpdate.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle empty graph with fast path', () => {
|
||||
const emptyGraph = { _nodes: [] } as any
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(emptyGraph)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.renderMinimap()
|
||||
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
expect(vi.mocked(renderMinimapToCanvas)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should only render when redraw is needed', async () => {
|
||||
const { renderMinimapToCanvas } = await import(
|
||||
'@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
)
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
// First render (needsFullRedraw is true by default)
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Second render without changes (should not render)
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Set update flag and render again
|
||||
updateFlagsRef.value.nodes = true
|
||||
renderer.renderMinimap()
|
||||
expect(vi.mocked(renderMinimapToCanvas)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should update minimap with bounds and viewport callbacks', () => {
|
||||
const updateBounds = vi.fn()
|
||||
const updateViewport = vi.fn()
|
||||
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: true,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.updateMinimap(updateBounds, updateViewport)
|
||||
|
||||
expect(updateBounds).toHaveBeenCalled()
|
||||
expect(updateViewport).toHaveBeenCalled()
|
||||
expect(updateFlagsRef.value.bounds).toBe(false)
|
||||
expect(renderer.needsFullRedraw.value).toBe(false) // After rendering, needsFullRedraw is reset to false
|
||||
expect(updateFlagsRef.value.viewport).toBe(false) // After updating viewport, this is reset to false
|
||||
})
|
||||
|
||||
it('should force full redraw when requested', () => {
|
||||
const canvasRef = ref(mockCanvas)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
renderer.forceFullRedraw()
|
||||
|
||||
expect(renderer.needsFullRedraw.value).toBe(true)
|
||||
expect(updateFlagsRef.value.bounds).toBe(true)
|
||||
expect(updateFlagsRef.value.nodes).toBe(true)
|
||||
expect(updateFlagsRef.value.connections).toBe(true)
|
||||
expect(updateFlagsRef.value.viewport).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const canvasRef = ref<HTMLCanvasElement | undefined>(undefined)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
|
||||
const scaleRef = ref(1)
|
||||
const updateFlagsRef = ref<UpdateFlags>({
|
||||
bounds: false,
|
||||
nodes: false,
|
||||
connections: false,
|
||||
viewport: false
|
||||
})
|
||||
const settings = {
|
||||
nodeColors: ref(true),
|
||||
showLinks: ref(true),
|
||||
showGroups: ref(true),
|
||||
renderBypass: ref(false),
|
||||
renderError: ref(false)
|
||||
}
|
||||
|
||||
const renderer = useMinimapRenderer(
|
||||
canvasRef,
|
||||
graphRef,
|
||||
boundsRef,
|
||||
scaleRef,
|
||||
updateFlagsRef,
|
||||
settings,
|
||||
250,
|
||||
200
|
||||
)
|
||||
|
||||
// Should not throw
|
||||
expect(() => renderer.renderMinimap()).not.toThrow()
|
||||
expect(mockCanvas.getContext).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,122 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useMinimapSettings } from '@/renderer/extensions/minimap/composables/useMinimapSettings'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
vi.mock('@/stores/settingStore')
|
||||
vi.mock('@/stores/workspace/colorPaletteStore')
|
||||
|
||||
describe('useMinimapSettings', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return all minimap settings as computed refs', () => {
|
||||
const mockSettingStore = {
|
||||
get: vi.fn((key: string) => {
|
||||
const settings: Record<string, any> = {
|
||||
'Comfy.Minimap.NodeColors': true,
|
||||
'Comfy.Minimap.ShowLinks': false,
|
||||
'Comfy.Minimap.ShowGroups': true,
|
||||
'Comfy.Minimap.RenderBypassState': false,
|
||||
'Comfy.Minimap.RenderErrorState': true
|
||||
}
|
||||
return settings[key]
|
||||
})
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
|
||||
expect(settings.nodeColors.value).toBe(true)
|
||||
expect(settings.showLinks.value).toBe(false)
|
||||
expect(settings.showGroups.value).toBe(true)
|
||||
expect(settings.renderBypass.value).toBe(false)
|
||||
expect(settings.renderError.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should generate container styles based on theme', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: false }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.containerStyles.value
|
||||
|
||||
expect(styles.width).toBe('250px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.backgroundColor).toBe('#15161C') // dark theme color
|
||||
expect(styles.border).toBe('1px solid #333')
|
||||
})
|
||||
|
||||
it('should generate light theme container styles', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: true }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.containerStyles.value
|
||||
|
||||
expect(styles.backgroundColor).toBe('#FAF9F5') // light theme color
|
||||
expect(styles.border).toBe('1px solid #ccc')
|
||||
})
|
||||
|
||||
it('should generate panel styles based on theme', () => {
|
||||
const mockColorPaletteStore = {
|
||||
completedActivePalette: { light_theme: false }
|
||||
}
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue(
|
||||
mockColorPaletteStore as any
|
||||
)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
const styles = settings.panelStyles.value
|
||||
|
||||
expect(styles.backgroundColor).toBe('#15161C')
|
||||
expect(styles.border).toBe('1px solid #333')
|
||||
expect(styles.borderRadius).toBe('8px')
|
||||
})
|
||||
|
||||
it('should create computed properties that call the store getter', () => {
|
||||
const mockGet = vi.fn((key: string) => {
|
||||
if (key === 'Comfy.Minimap.NodeColors') return true
|
||||
if (key === 'Comfy.Minimap.ShowLinks') return false
|
||||
return true
|
||||
})
|
||||
const mockSettingStore = { get: mockGet }
|
||||
|
||||
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
|
||||
const settings = useMinimapSettings()
|
||||
|
||||
// Access the computed properties
|
||||
expect(settings.nodeColors.value).toBe(true)
|
||||
expect(settings.showLinks.value).toBe(false)
|
||||
|
||||
// Verify the store getter was called with the correct keys
|
||||
expect(mockGet).toHaveBeenCalledWith('Comfy.Minimap.NodeColors')
|
||||
expect(mockGet).toHaveBeenCalledWith('Comfy.Minimap.ShowLinks')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,289 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useMinimapViewport } from '@/renderer/extensions/minimap/composables/useMinimapViewport'
|
||||
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
|
||||
|
||||
vi.mock('@/composables/canvas/useCanvasTransformSync')
|
||||
vi.mock('@/renderer/core/spatial/boundsCalculator', () => ({
|
||||
calculateNodeBounds: vi.fn(),
|
||||
calculateMinimapScale: vi.fn(),
|
||||
enforceMinimumBounds: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useMinimapViewport', () => {
|
||||
let mockCanvas: MinimapCanvas
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockCanvas = {
|
||||
canvas: {
|
||||
clientWidth: 800,
|
||||
clientHeight: 600,
|
||||
width: 1600,
|
||||
height: 1200
|
||||
} as HTMLCanvasElement,
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [
|
||||
{ pos: [100, 100], size: [150, 80] },
|
||||
{ pos: [300, 200], size: [120, 60] }
|
||||
]
|
||||
} as any
|
||||
|
||||
vi.mocked(useCanvasTransformSync).mockReturnValue({
|
||||
startSync: vi.fn(),
|
||||
stopSync: vi.fn()
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should initialize with default bounds', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
expect(viewport.bounds.value).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 0,
|
||||
maxY: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
})
|
||||
|
||||
expect(viewport.scale.value).toBe(1)
|
||||
})
|
||||
|
||||
it('should calculate graph bounds from nodes', async () => {
|
||||
const { calculateNodeBounds, enforceMinimumBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 100,
|
||||
minY: 100,
|
||||
maxX: 420,
|
||||
maxY: 260,
|
||||
width: 320,
|
||||
height: 160
|
||||
})
|
||||
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(calculateNodeBounds).toHaveBeenCalledWith(mockGraph._nodes)
|
||||
expect(enforceMinimumBounds).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty graph', async () => {
|
||||
const { calculateNodeBounds } = await import(
|
||||
'@/renderer/core/spatial/boundsCalculator'
|
||||
)
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue(null)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref({ _nodes: [] } as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(viewport.bounds.value).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 100,
|
||||
maxY: 100,
|
||||
width: 100,
|
||||
height: 100
|
||||
})
|
||||
})
|
||||
|
||||
it('should update canvas dimensions', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
expect(viewport.canvasDimensions.value).toEqual({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
})
|
||||
|
||||
it('should calculate viewport transform', async () => {
|
||||
const { calculateNodeBounds, enforceMinimumBounds, calculateMinimapScale } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
// Mock the bounds calculation
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 500,
|
||||
maxY: 400,
|
||||
width: 500,
|
||||
height: 400
|
||||
})
|
||||
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
vi.mocked(calculateMinimapScale).mockReturnValue(0.5)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
// Set canvas transform
|
||||
mockCanvas.ds.scale = 2
|
||||
mockCanvas.ds.offset = [-100, -50]
|
||||
|
||||
// Update bounds and viewport
|
||||
viewport.updateBounds()
|
||||
viewport.updateCanvasDimensions()
|
||||
viewport.updateViewport()
|
||||
|
||||
const transform = viewport.viewportTransform.value
|
||||
|
||||
// World coordinates
|
||||
const worldX = -(-100) // -offset[0] = 100
|
||||
const worldY = -(-50) // -offset[1] = 50
|
||||
|
||||
// Viewport size in world coordinates
|
||||
const viewportWidth = 800 / 2 // canvasWidth / scale = 400
|
||||
const viewportHeight = 600 / 2 // canvasHeight / scale = 300
|
||||
|
||||
// Center offsets
|
||||
const centerOffsetX = (250 - 500 * 0.5) / 2 // (250 - 250) / 2 = 0
|
||||
const centerOffsetY = (200 - 400 * 0.5) / 2 // (200 - 200) / 2 = 0
|
||||
|
||||
// Expected values based on implementation: (worldX - bounds.minX) * scale + centerOffsetX
|
||||
expect(transform.x).toBeCloseTo((worldX - 0) * 0.5 + centerOffsetX) // (100 - 0) * 0.5 + 0 = 50
|
||||
expect(transform.y).toBeCloseTo((worldY - 0) * 0.5 + centerOffsetY) // (50 - 0) * 0.5 + 0 = 25
|
||||
expect(transform.width).toBeCloseTo(viewportWidth * 0.5) // 400 * 0.5 = 200
|
||||
expect(transform.height).toBeCloseTo(viewportHeight * 0.5) // 300 * 0.5 = 150
|
||||
})
|
||||
|
||||
it('should center view on world coordinates', () => {
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
mockCanvas.ds.scale = 2
|
||||
|
||||
viewport.centerViewOn(300, 200)
|
||||
|
||||
// Should update canvas offset to center on the given world coordinates
|
||||
const expectedOffsetX = -(300 - 800 / 2 / 2) // -(worldX - viewportWidth/2)
|
||||
const expectedOffsetY = -(200 - 600 / 2 / 2) // -(worldY - viewportHeight/2)
|
||||
|
||||
expect(mockCanvas.ds.offset[0]).toBe(expectedOffsetX)
|
||||
expect(mockCanvas.ds.offset[1]).toBe(expectedOffsetY)
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should start and stop viewport sync', () => {
|
||||
const startSyncMock = vi.fn()
|
||||
const stopSyncMock = vi.fn()
|
||||
|
||||
vi.mocked(useCanvasTransformSync).mockReturnValue({
|
||||
startSync: startSyncMock,
|
||||
stopSync: stopSyncMock
|
||||
} as any)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.startViewportSync()
|
||||
expect(startSyncMock).toHaveBeenCalled()
|
||||
|
||||
viewport.stopViewportSync()
|
||||
expect(stopSyncMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle null canvas gracefully', () => {
|
||||
const canvasRef = ref(null as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
// Should not throw
|
||||
expect(() => viewport.updateCanvasDimensions()).not.toThrow()
|
||||
expect(() => viewport.updateViewport()).not.toThrow()
|
||||
expect(() => viewport.centerViewOn(100, 100)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should calculate scale correctly', async () => {
|
||||
const { calculateMinimapScale, calculateNodeBounds, enforceMinimumBounds } =
|
||||
await import('@/renderer/core/spatial/boundsCalculator')
|
||||
|
||||
const testBounds = {
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 500,
|
||||
maxY: 400,
|
||||
width: 500,
|
||||
height: 400
|
||||
}
|
||||
|
||||
vi.mocked(calculateNodeBounds).mockReturnValue(testBounds)
|
||||
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
|
||||
vi.mocked(calculateMinimapScale).mockReturnValue(0.4)
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateBounds()
|
||||
|
||||
expect(calculateMinimapScale).toHaveBeenCalledWith(testBounds, 250, 200)
|
||||
expect(viewport.scale.value).toBe(0.4)
|
||||
})
|
||||
|
||||
it('should handle device pixel ratio', () => {
|
||||
const originalDPR = window.devicePixelRatio
|
||||
Object.defineProperty(window, 'devicePixelRatio', {
|
||||
value: 2,
|
||||
configurable: true
|
||||
})
|
||||
|
||||
const canvasRef = ref(mockCanvas as any)
|
||||
const graphRef = ref(mockGraph as any)
|
||||
|
||||
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
|
||||
|
||||
viewport.updateCanvasDimensions()
|
||||
|
||||
// Should use client dimensions or calculate from canvas dimensions / dpr
|
||||
expect(viewport.canvasDimensions.value.width).toBe(800)
|
||||
expect(viewport.canvasDimensions.value.height).toBe(600)
|
||||
|
||||
Object.defineProperty(window, 'devicePixelRatio', {
|
||||
value: originalDPR,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user