mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 02:02:08 +00:00
chore: migrate tests from tests-ui/ to colocate with source files (#7811)
## Summary Migrates all unit tests from `tests-ui/` to colocate with their source files in `src/`, improving discoverability and maintainability. ## Changes - **What**: Relocated all unit tests to be adjacent to the code they test, following the `<source>.test.ts` naming convention - **Config**: Updated `vitest.config.ts` to remove `tests-ui` include pattern and `@tests-ui` alias - **Docs**: Moved testing documentation to `docs/testing/` with updated paths and patterns ## Review Focus - Migration patterns documented in `temp/plans/migrate-tests-ui-to-src.md` - Tests use `@/` path aliases instead of relative imports - Shared fixtures placed in `__fixtures__/` directories ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7811-chore-migrate-tests-from-tests-ui-to-colocate-with-source-files-2da6d73d36508147a4cce85365dee614) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
927
src/renderer/extensions/minimap/composables/useMinimap.test.ts
Normal file
927
src/renderer/extensions/minimap/composables/useMinimap.test.ts
Normal file
@@ -0,0 +1,927 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, shallowRef } from 'vue'
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
const triggerRAF = async () => {
|
||||
// Trigger all RAF callbacks
|
||||
Object.values(rafCallbacks).forEach((cb) => cb?.())
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
const mockPause = vi.fn()
|
||||
const mockResume = vi.fn()
|
||||
|
||||
const rafCallbacks: Record<string, () => void> = {}
|
||||
let rafCallbackId = 0
|
||||
|
||||
vi.mock('@vueuse/core', () => {
|
||||
return {
|
||||
useRafFn: vi.fn((callback, options) => {
|
||||
const id = rafCallbackId++
|
||||
rafCallbacks[id] = callback
|
||||
|
||||
if (options?.immediate !== false) {
|
||||
void Promise.resolve().then(() => callback())
|
||||
}
|
||||
|
||||
const resumeFn = vi.fn(() => {
|
||||
mockResume()
|
||||
// Execute the RAF callback immediately when resumed
|
||||
if (rafCallbacks[id]) {
|
||||
rafCallbacks[id]()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pause: mockPause,
|
||||
resume: resumeFn
|
||||
}
|
||||
}),
|
||||
useThrottleFn: vi.fn((callback) => {
|
||||
return (...args: any[]) => {
|
||||
return callback(...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let mockCanvas: any
|
||||
let mockGraph: any
|
||||
|
||||
const setupMocks = () => {
|
||||
const mockNodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
color: '#ff0000',
|
||||
constructor: { color: '#666' },
|
||||
outputs: [
|
||||
{
|
||||
links: ['link1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
pos: [200, 100],
|
||||
size: [150, 75],
|
||||
constructor: { color: '#666' },
|
||||
outputs: []
|
||||
}
|
||||
]
|
||||
|
||||
mockGraph = {
|
||||
_nodes: mockNodes,
|
||||
links: {
|
||||
link1: {
|
||||
id: 'link1',
|
||||
target_id: 'node2'
|
||||
}
|
||||
},
|
||||
getNodeById: vi.fn((id) => mockNodes.find((n) => n.id === id)),
|
||||
setDirtyCanvas: vi.fn(),
|
||||
onNodeAdded: null,
|
||||
onNodeRemoved: null,
|
||||
onConnectionChange: null
|
||||
}
|
||||
|
||||
mockCanvas = {
|
||||
graph: mockGraph,
|
||||
canvas: {
|
||||
width: 1000,
|
||||
height: 800,
|
||||
clientWidth: 1000,
|
||||
clientHeight: 800
|
||||
},
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
setupMocks()
|
||||
|
||||
const defaultCanvasStore = {
|
||||
canvas: mockCanvas,
|
||||
getCanvas: () => defaultCanvasStore.canvas
|
||||
}
|
||||
|
||||
const defaultSettingStore = {
|
||||
get: vi.fn().mockReturnValue(true),
|
||||
set: vi.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: vi.fn(() => defaultCanvasStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => defaultSettingStore)
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
||||
useColorPaletteStore: vi.fn(() => ({
|
||||
completedActivePalette: {
|
||||
light_theme: false
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
apiURL: vi.fn().mockReturnValue('http://localhost:8188')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: {
|
||||
graph: mockGraph
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: vi.fn(() => ({
|
||||
activeSubgraph: null
|
||||
}))
|
||||
}))
|
||||
|
||||
const { useMinimap } =
|
||||
await import('@/renderer/extensions/minimap/composables/useMinimap')
|
||||
const { api } = await import('@/scripts/api')
|
||||
|
||||
describe('useMinimap', () => {
|
||||
let mockCanvas: any
|
||||
let mockGraph: any
|
||||
let mockCanvasElement: any
|
||||
let mockContainerElement: any
|
||||
let mockContext2D: any
|
||||
|
||||
async function createAndInitializeMinimap() {
|
||||
const minimap = useMinimap({
|
||||
containerRefMaybe: shallowRef(mockContainerElement),
|
||||
canvasRefMaybe: shallowRef(mockCanvasElement)
|
||||
})
|
||||
await minimap.init()
|
||||
await nextTick()
|
||||
await flushPromises()
|
||||
return minimap
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockPause.mockClear()
|
||||
mockResume.mockClear()
|
||||
|
||||
mockContext2D = {
|
||||
clearRect: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
strokeRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 0
|
||||
}
|
||||
|
||||
mockCanvasElement = {
|
||||
getContext: vi.fn().mockReturnValue(mockContext2D),
|
||||
width: 250,
|
||||
height: 200,
|
||||
clientWidth: 250,
|
||||
clientHeight: 200
|
||||
}
|
||||
|
||||
const mockRect = {
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 250,
|
||||
height: 200,
|
||||
right: 350,
|
||||
bottom: 300,
|
||||
x: 100,
|
||||
y: 100
|
||||
}
|
||||
|
||||
mockContainerElement = {
|
||||
getBoundingClientRect: vi.fn(() => ({ ...mockRect }))
|
||||
}
|
||||
|
||||
const mockNodes = [
|
||||
{
|
||||
id: 'node1',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
color: '#ff0000',
|
||||
constructor: { color: '#666' },
|
||||
outputs: [
|
||||
{
|
||||
links: ['link1']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'node2',
|
||||
pos: [200, 100],
|
||||
size: [150, 75],
|
||||
constructor: { color: '#666' },
|
||||
outputs: []
|
||||
}
|
||||
]
|
||||
|
||||
mockGraph = {
|
||||
_nodes: mockNodes,
|
||||
links: {
|
||||
link1: {
|
||||
id: 'link1',
|
||||
target_id: 'node2'
|
||||
}
|
||||
},
|
||||
getNodeById: vi.fn((id) => mockNodes.find((n) => n.id === id)),
|
||||
setDirtyCanvas: vi.fn(),
|
||||
onNodeAdded: null,
|
||||
onNodeRemoved: null,
|
||||
onConnectionChange: null
|
||||
}
|
||||
|
||||
mockCanvas = {
|
||||
graph: mockGraph,
|
||||
canvas: {
|
||||
width: 1000,
|
||||
height: 800,
|
||||
clientWidth: 1000,
|
||||
clientHeight: 800
|
||||
},
|
||||
ds: {
|
||||
scale: 1,
|
||||
offset: [0, 0]
|
||||
},
|
||||
setDirty: vi.fn()
|
||||
}
|
||||
|
||||
defaultCanvasStore.canvas = mockCanvas
|
||||
|
||||
defaultSettingStore.get = vi.fn().mockReturnValue(true)
|
||||
defaultSettingStore.set = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
Object.defineProperty(window, 'devicePixelRatio', {
|
||||
writable: true,
|
||||
configurable: true,
|
||||
value: 1
|
||||
})
|
||||
|
||||
window.addEventListener = vi.fn()
|
||||
window.removeEventListener = vi.fn()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const originalCanvas = defaultCanvasStore.canvas
|
||||
defaultCanvasStore.canvas = null
|
||||
|
||||
const minimap = useMinimap()
|
||||
|
||||
expect(minimap.width).toBe(250)
|
||||
expect(minimap.height).toBe(200)
|
||||
expect(minimap.visible.value).toBe(true)
|
||||
expect(minimap.initialized.value).toBe(false)
|
||||
|
||||
defaultCanvasStore.canvas = originalCanvas
|
||||
})
|
||||
|
||||
it('should initialize minimap when canvas is available', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
expect(defaultSettingStore.get).toHaveBeenCalledWith(
|
||||
'Comfy.Minimap.Visible'
|
||||
)
|
||||
expect(api.addEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
|
||||
if (minimap.visible.value) {
|
||||
expect(mockResume).toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
|
||||
it('should not initialize without canvas and graph', async () => {
|
||||
const originalCanvas = defaultCanvasStore.canvas
|
||||
defaultCanvasStore.canvas = null
|
||||
|
||||
const minimap = useMinimap()
|
||||
await minimap.init()
|
||||
|
||||
expect(minimap.initialized.value).toBe(false)
|
||||
expect(api.addEventListener).not.toHaveBeenCalled()
|
||||
|
||||
defaultCanvasStore.canvas = originalCanvas
|
||||
})
|
||||
|
||||
it('should setup event listeners on graph', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
expect(mockGraph.onNodeAdded).toBeDefined()
|
||||
expect(mockGraph.onNodeRemoved).toBeDefined()
|
||||
expect(mockGraph.onConnectionChange).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle visibility from settings', async () => {
|
||||
defaultSettingStore.get.mockReturnValue(false)
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
expect(minimap.visible.value).toBe(false)
|
||||
expect(mockResume).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should cleanup all resources', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
minimap.destroy()
|
||||
|
||||
expect(mockPause).toHaveBeenCalled()
|
||||
expect(api.removeEventListener).toHaveBeenCalledWith(
|
||||
'graphChanged',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(window.removeEventListener).toHaveBeenCalled()
|
||||
expect(minimap.initialized.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should restore original graph callbacks', async () => {
|
||||
const originalCallbacks = {
|
||||
onNodeAdded: vi.fn(),
|
||||
onNodeRemoved: vi.fn(),
|
||||
onConnectionChange: vi.fn()
|
||||
}
|
||||
|
||||
mockGraph.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
mockGraph.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
mockGraph.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
minimap.destroy()
|
||||
|
||||
expect(mockGraph.onNodeAdded).toBe(originalCallbacks.onNodeAdded)
|
||||
expect(mockGraph.onNodeRemoved).toBe(originalCallbacks.onNodeRemoved)
|
||||
expect(mockGraph.onConnectionChange).toBe(
|
||||
originalCallbacks.onConnectionChange
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toggle', () => {
|
||||
it('should toggle visibility and save to settings', async () => {
|
||||
const minimap = useMinimap()
|
||||
const initialVisibility = minimap.visible.value
|
||||
|
||||
await minimap.toggle()
|
||||
|
||||
expect(minimap.visible.value).toBe(!initialVisibility)
|
||||
expect(defaultSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Minimap.Visible',
|
||||
!initialVisibility
|
||||
)
|
||||
|
||||
await minimap.toggle()
|
||||
|
||||
expect(minimap.visible.value).toBe(initialVisibility)
|
||||
expect(defaultSettingStore.set).toHaveBeenCalledWith(
|
||||
'Comfy.Minimap.Visible',
|
||||
initialVisibility
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should verify context is obtained during render', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const getContextSpy = vi.spyOn(mockCanvasElement, 'getContext')
|
||||
|
||||
await minimap.init()
|
||||
|
||||
// Force initial render
|
||||
minimap.renderMinimap()
|
||||
|
||||
// Force a render by triggering a graph change
|
||||
mockGraph._nodes.push({
|
||||
id: 'new-node',
|
||||
pos: [150, 150],
|
||||
size: [100, 50]
|
||||
})
|
||||
|
||||
// Trigger RAF to process changes
|
||||
await triggerRAF()
|
||||
await nextTick()
|
||||
|
||||
expect(getContextSpy).toHaveBeenCalled()
|
||||
expect(getContextSpy).toHaveBeenCalledWith('2d')
|
||||
})
|
||||
|
||||
it('should render at least once after initialization', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
// Force initial render
|
||||
minimap.renderMinimap()
|
||||
|
||||
// Force a render by modifying a node position
|
||||
mockGraph._nodes[0].pos = [50, 50]
|
||||
|
||||
// Trigger RAF to process changes
|
||||
await triggerRAF()
|
||||
await nextTick()
|
||||
|
||||
const renderingOccurred =
|
||||
mockContext2D.clearRect.mock.calls.length > 0 ||
|
||||
mockContext2D.fillRect.mock.calls.length > 0
|
||||
|
||||
if (!renderingOccurred) {
|
||||
console.log('Minimap visible:', minimap.visible.value)
|
||||
console.log('Minimap initialized:', minimap.initialized.value)
|
||||
console.log('Canvas exists:', !!defaultCanvasStore.canvas)
|
||||
console.log('Graph exists:', !!defaultCanvasStore.canvas?.graph)
|
||||
console.log(
|
||||
'clearRect calls:',
|
||||
mockContext2D.clearRect.mock.calls.length
|
||||
)
|
||||
console.log('fillRect calls:', mockContext2D.fillRect.mock.calls.length)
|
||||
console.log(
|
||||
'getContext calls:',
|
||||
mockCanvasElement.getContext.mock.calls.length
|
||||
)
|
||||
}
|
||||
|
||||
expect(renderingOccurred).toBe(true)
|
||||
})
|
||||
|
||||
it('should not render when context is null', async () => {
|
||||
mockCanvasElement.getContext = vi.fn().mockReturnValue(null)
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(mockContext2D.clearRect).not.toHaveBeenCalled()
|
||||
|
||||
mockCanvasElement.getContext = vi.fn().mockReturnValue(mockContext2D)
|
||||
})
|
||||
|
||||
it('should handle empty graph', async () => {
|
||||
const originalNodes = [...mockGraph._nodes]
|
||||
mockGraph._nodes = []
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
// The renderer has a fast path for empty graphs, force it to execute
|
||||
minimap.renderMinimap()
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
|
||||
// With the new reactive system, the minimap may still render some elements
|
||||
// The key test is that it doesn't crash and properly initializes
|
||||
expect(mockContext2D.clearRect).toHaveBeenCalled()
|
||||
|
||||
mockGraph._nodes = originalNodes
|
||||
})
|
||||
})
|
||||
|
||||
describe('pointer interactions', () => {
|
||||
it('should handle pointer down and start dragging (mouse)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
|
||||
minimap.handlePointerDown(pointerEvent)
|
||||
|
||||
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should handle pointer move while dragging (mouse)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
minimap.handlePointerDown(pointerDownEvent)
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
expect(mockCanvas.ds.offset).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle pointer up to stop dragging (mouse)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
minimap.handlePointerDown(pointerDownEvent)
|
||||
|
||||
minimap.handlePointerUp()
|
||||
|
||||
mockCanvas.setDirty.mockClear()
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer down and start dragging (touch)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
|
||||
minimap.handlePointerDown(pointerEvent)
|
||||
|
||||
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should handle pointer move while dragging (touch)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
minimap.handlePointerDown(pointerDownEvent)
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
expect(mockCanvas.ds.offset).toBeDefined()
|
||||
})
|
||||
|
||||
it('should handle pointer move while dragging (pen)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'pen'
|
||||
})
|
||||
minimap.handlePointerDown(pointerDownEvent)
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'pen'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
expect(mockCanvas.ds.offset).toBeDefined()
|
||||
})
|
||||
|
||||
it('should not move when not dragging with pointer', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
mockCanvas.setDirty.mockClear()
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle pointer up to stop dragging (touch)', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', {
|
||||
clientX: 150,
|
||||
clientY: 150,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
minimap.handlePointerDown(pointerDownEvent)
|
||||
|
||||
minimap.handlePointerUp()
|
||||
|
||||
mockCanvas.setDirty.mockClear()
|
||||
|
||||
const pointerMoveEvent = new PointerEvent('pointermove', {
|
||||
clientX: 200,
|
||||
clientY: 200,
|
||||
pointerType: 'touch'
|
||||
})
|
||||
minimap.handlePointerMove(pointerMoveEvent)
|
||||
|
||||
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('wheel interactions', () => {
|
||||
it('should handle wheel zoom in', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
clientX: 150,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||
value: preventDefault
|
||||
})
|
||||
|
||||
minimap.handleWheel(wheelEvent)
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled()
|
||||
expect(mockCanvas.ds.scale).toBeCloseTo(1.1)
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should handle wheel zoom out', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: 100,
|
||||
clientX: 150,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||
value: preventDefault
|
||||
})
|
||||
|
||||
minimap.handleWheel(wheelEvent)
|
||||
|
||||
expect(preventDefault).toHaveBeenCalled()
|
||||
expect(mockCanvas.ds.scale).toBeCloseTo(0.9)
|
||||
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||
})
|
||||
|
||||
it('should respect zoom limits', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
mockCanvas.ds.scale = 0.1
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: 100,
|
||||
clientX: 150,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||
value: preventDefault
|
||||
})
|
||||
|
||||
minimap.handleWheel(wheelEvent)
|
||||
|
||||
expect(mockCanvas.ds.scale).toBe(0.1)
|
||||
})
|
||||
|
||||
it('should update container rect if needed', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
clientX: 150,
|
||||
clientY: 150
|
||||
})
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||
value: preventDefault
|
||||
})
|
||||
|
||||
minimap.handleWheel(wheelEvent)
|
||||
|
||||
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('viewport updates', () => {
|
||||
it('should update viewport transform correctly', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
await nextTick()
|
||||
|
||||
const viewportStyles = minimap.viewportStyles.value
|
||||
|
||||
expect(viewportStyles).toBeDefined()
|
||||
expect(viewportStyles.transform).toMatch(
|
||||
/translate\(-?\d+(\.\d+)?px, -?\d+(\.\d+)?px\)/
|
||||
)
|
||||
expect(viewportStyles.width).toMatch(/\d+(\.\d+)?px/)
|
||||
expect(viewportStyles.height).toMatch(/\d+(\.\d+)?px/)
|
||||
expect(viewportStyles.border).toBe('2px solid #FFF')
|
||||
})
|
||||
|
||||
it('should handle canvas dimension updates', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
mockCanvas.canvas.clientWidth = 1200
|
||||
mockCanvas.canvas.clientHeight = 900
|
||||
|
||||
const resizeHandler = (window.addEventListener as any).mock.calls.find(
|
||||
(call: any) => call[0] === 'resize'
|
||||
)?.[1]
|
||||
|
||||
if (resizeHandler) {
|
||||
resizeHandler()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(minimap.viewportStyles.value).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('graph change handling', () => {
|
||||
it('should handle node addition', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const newNode = {
|
||||
id: 'node3',
|
||||
pos: [300, 200],
|
||||
size: [100, 100],
|
||||
constructor: { color: '#666' }
|
||||
}
|
||||
|
||||
mockGraph._nodes.push(newNode)
|
||||
if (mockGraph.onNodeAdded) {
|
||||
mockGraph.onNodeAdded(newNode)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
})
|
||||
|
||||
it('should handle node removal', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const removedNode = mockGraph._nodes[0]
|
||||
mockGraph._nodes.splice(0, 1)
|
||||
|
||||
if (mockGraph.onNodeRemoved) {
|
||||
mockGraph.onNodeRemoved(removedNode)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
})
|
||||
|
||||
it('should handle connection changes', async () => {
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
if (mockGraph.onConnectionChange) {
|
||||
mockGraph.onConnectionChange(mockGraph._nodes[0])
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
})
|
||||
})
|
||||
|
||||
describe('container styles', () => {
|
||||
it('should provide correct container styles for dark theme', () => {
|
||||
const minimap = useMinimap()
|
||||
const styles = minimap.containerStyles.value
|
||||
|
||||
expect(styles.width).toBe('253px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.border).toBe('1px solid var(--interface-stroke)')
|
||||
expect(styles.borderRadius).toBe('8px')
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle missing node outputs', async () => {
|
||||
mockGraph._nodes[0].outputs = null
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await expect(minimap.init()).resolves.not.toThrow()
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle invalid link references', async () => {
|
||||
mockGraph.links.link1.target_id = 'invalid-node'
|
||||
mockGraph.getNodeById.mockReturnValue(null)
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await expect(minimap.init()).resolves.not.toThrow()
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle high DPI displays', async () => {
|
||||
window.devicePixelRatio = 2
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
expect(minimap.initialized.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle nodes without color', async () => {
|
||||
mockGraph._nodes[0].color = undefined
|
||||
|
||||
const minimap = await createAndInitializeMinimap()
|
||||
|
||||
await minimap.init()
|
||||
|
||||
const renderMinimap = (minimap as any).renderMinimap
|
||||
if (renderMinimap) {
|
||||
renderMinimap()
|
||||
}
|
||||
|
||||
expect(mockContext2D.fillRect).toHaveBeenCalled()
|
||||
expect(mockContext2D.fillStyle).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMinimapRef', () => {
|
||||
it('should set minimap reference', () => {
|
||||
const minimap = useMinimap()
|
||||
const ref = document.createElement('div')
|
||||
|
||||
minimap.setMinimapRef(ref)
|
||||
|
||||
expect(() => minimap.setMinimapRef(ref)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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: () => void
|
||||
|
||||
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: (worldX: number, worldY: number) => void
|
||||
|
||||
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<(worldX: number, worldY: number) => void>()
|
||||
})
|
||||
|
||||
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 | null>(null)
|
||||
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,266 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref, shallowRef } 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 = shallowRef<HTMLCanvasElement | null>(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 = shallowRef<HTMLCanvasElement | null>(null)
|
||||
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,125 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useMinimapSettings } from '@/renderer/extensions/minimap/composables/useMinimapSettings'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
|
||||
vi.mock('@/platform/settings/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('253px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.border).toBe('1px solid var(--interface-stroke)')
|
||||
expect(styles.borderRadius).toBe('8px')
|
||||
})
|
||||
|
||||
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.width).toBe('253px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.border).toBe('1px solid var(--interface-stroke)')
|
||||
expect(styles.borderRadius).toBe('8px')
|
||||
})
|
||||
|
||||
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.width).toBe('210px')
|
||||
expect(styles.height).toBe('200px')
|
||||
expect(styles.border).toBe('1px solid var(--interface-stroke)')
|
||||
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,287 @@
|
||||
import { useRafFn } from '@vueuse/core'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
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('@vueuse/core')
|
||||
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(useRafFn, { partial: true }).mockReturnValue({
|
||||
resume: vi.fn(),
|
||||
pause: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
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(useRafFn, { partial: true }).mockReturnValue({
|
||||
resume: startSyncMock,
|
||||
pause: stopSyncMock
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
})
|
||||
})
|
||||
175
src/renderer/extensions/minimap/data/MinimapDataSource.test.ts
Normal file
175
src/renderer/extensions/minimap/data/MinimapDataSource.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { computed } from 'vue'
|
||||
import type { ComputedRef } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { NodeLayout } from '@/renderer/core/layout/types'
|
||||
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
|
||||
|
||||
// Mock layoutStore
|
||||
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
|
||||
layoutStore: {
|
||||
getAllNodes: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// Helper to create mock links that satisfy LGraph['links'] type
|
||||
function createMockLinks(): LGraph['links'] {
|
||||
const map = new Map<number, LLink>()
|
||||
return Object.assign(map, {}) as LGraph['links']
|
||||
}
|
||||
|
||||
describe('MinimapDataSource', () => {
|
||||
describe('MinimapDataSourceFactory', () => {
|
||||
it('should create LayoutStoreDataSource when LayoutStore has data', () => {
|
||||
// Arrange
|
||||
const mockNodes = new Map<string, NodeLayout>([
|
||||
[
|
||||
'node1',
|
||||
{
|
||||
id: 'node1',
|
||||
position: { x: 0, y: 0 },
|
||||
size: { width: 100, height: 50 },
|
||||
zIndex: 0,
|
||||
visible: true,
|
||||
bounds: { x: 0, y: 0, width: 100, height: 50 }
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
// Create a computed ref that returns the map
|
||||
const computedNodes: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => mockNodes)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedNodes)
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource).toBeDefined()
|
||||
expect(dataSource.hasData()).toBe(true)
|
||||
expect(dataSource.getNodeCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('should create LiteGraphDataSource when LayoutStore is empty', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockNode: Pick<
|
||||
LGraphNode,
|
||||
'id' | 'pos' | 'size' | 'bgcolor' | 'mode' | 'has_errors' | 'outputs'
|
||||
> = {
|
||||
id: 'node1' as NodeId,
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
bgcolor: '#fff',
|
||||
mode: 0,
|
||||
has_errors: false,
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [mockNode as LGraphNode],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource).toBeDefined()
|
||||
expect(dataSource.hasData()).toBe(true)
|
||||
expect(dataSource.getNodeCount()).toBe(1)
|
||||
|
||||
const nodes = dataSource.getNodes()
|
||||
expect(nodes).toHaveLength(1)
|
||||
expect(nodes[0]).toMatchObject({
|
||||
id: 'node1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 50
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty graph correctly', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
|
||||
// Assert
|
||||
expect(dataSource.hasData()).toBe(false)
|
||||
expect(dataSource.getNodeCount()).toBe(0)
|
||||
expect(dataSource.getNodes()).toEqual([])
|
||||
expect(dataSource.getLinks()).toEqual([])
|
||||
expect(dataSource.getGroups()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bounds calculation', () => {
|
||||
it('should calculate correct bounds from nodes', () => {
|
||||
// Arrange
|
||||
const emptyMap = new Map<string, NodeLayout>()
|
||||
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
|
||||
computed(() => emptyMap)
|
||||
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
|
||||
|
||||
const mockNode1: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
|
||||
id: 'node1' as NodeId,
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockNode2: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
|
||||
id: 'node2' as NodeId,
|
||||
pos: [200, 100],
|
||||
size: [150, 75],
|
||||
outputs: []
|
||||
}
|
||||
|
||||
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
|
||||
_nodes: [mockNode1 as LGraphNode, mockNode2 as LGraphNode],
|
||||
_groups: [],
|
||||
links: createMockLinks()
|
||||
}
|
||||
|
||||
// Act
|
||||
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
|
||||
const bounds = dataSource.getBounds()
|
||||
|
||||
// Assert
|
||||
expect(bounds).toEqual({
|
||||
minX: 0,
|
||||
minY: 0,
|
||||
maxX: 350,
|
||||
maxY: 175,
|
||||
width: 350,
|
||||
height: 175
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
324
src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
Normal file
324
src/renderer/extensions/minimap/minimapCanvasRenderer.test.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { renderMinimapToCanvas } from '@/renderer/extensions/minimap/minimapCanvasRenderer'
|
||||
import type { MinimapRenderContext } from '@/renderer/extensions/minimap/types'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
vi.mock('@/stores/workspace/colorPaletteStore')
|
||||
vi.mock('@/utils/colorUtil', () => ({
|
||||
adjustColor: vi.fn((color: string) => color + '_adjusted')
|
||||
}))
|
||||
|
||||
describe('minimapCanvasRenderer', () => {
|
||||
let mockCanvas: HTMLCanvasElement
|
||||
let mockContext: CanvasRenderingContext2D
|
||||
let mockGraph: LGraph
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
mockContext = {
|
||||
clearRect: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
strokeRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillStyle: '',
|
||||
strokeStyle: '',
|
||||
lineWidth: 1
|
||||
} as any
|
||||
|
||||
mockCanvas = {
|
||||
getContext: vi.fn().mockReturnValue(mockContext)
|
||||
} as any
|
||||
|
||||
mockGraph = {
|
||||
_nodes: [
|
||||
{
|
||||
id: '1',
|
||||
pos: [100, 100],
|
||||
size: [150, 80],
|
||||
bgcolor: '#FF0000',
|
||||
mode: LGraphEventMode.ALWAYS,
|
||||
has_errors: false,
|
||||
outputs: []
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
pos: [300, 200],
|
||||
size: [120, 60],
|
||||
bgcolor: '#00FF00',
|
||||
mode: LGraphEventMode.BYPASS,
|
||||
has_errors: true,
|
||||
outputs: []
|
||||
}
|
||||
] as unknown as LGraphNode[],
|
||||
_groups: [],
|
||||
links: {},
|
||||
getNodeById: vi.fn()
|
||||
} as any
|
||||
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: false }
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should clear canvas and render nodes', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: true,
|
||||
renderError: true
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should clear the canvas first
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
|
||||
// Should render nodes (batch by color)
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle empty graph', () => {
|
||||
mockGraph._nodes = []
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 250, 200)
|
||||
expect(mockContext.fillRect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should batch render nodes by color', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should set fill style for each color group
|
||||
const fillStyleCalls = []
|
||||
let currentStyle = ''
|
||||
|
||||
mockContext.fillStyle = ''
|
||||
Object.defineProperty(mockContext, 'fillStyle', {
|
||||
get: () => currentStyle,
|
||||
set: (value) => {
|
||||
currentStyle = value
|
||||
fillStyleCalls.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Different colors for different nodes
|
||||
expect(fillStyleCalls.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should render bypass nodes with special color', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: true,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Node 2 is in bypass mode, should be rendered
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render error outlines when enabled', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: true
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should set stroke style for errors
|
||||
expect(mockContext.strokeStyle).toBe('#FF0000')
|
||||
expect(mockContext.strokeRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render groups when enabled', () => {
|
||||
mockGraph._groups = [
|
||||
{
|
||||
pos: [50, 50],
|
||||
size: [400, 300],
|
||||
color: '#0000FF'
|
||||
}
|
||||
] as any
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: true,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Groups should be rendered before nodes
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render connections when enabled', () => {
|
||||
const targetNode = {
|
||||
id: '2',
|
||||
pos: [300, 200],
|
||||
size: [120, 60]
|
||||
}
|
||||
|
||||
mockGraph._nodes[0].outputs = [
|
||||
{
|
||||
links: [1]
|
||||
}
|
||||
] as any
|
||||
|
||||
// Create a hybrid Map/Object for links as LiteGraph expects
|
||||
const linksMap = new Map([[1, { id: 1, target_id: 2 }]])
|
||||
const links = Object.assign(linksMap, {
|
||||
1: { id: 1, target_id: 2 }
|
||||
})
|
||||
mockGraph.links = links as any
|
||||
|
||||
mockGraph.getNodeById = vi.fn().mockReturnValue(targetNode)
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: false,
|
||||
showLinks: true,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Should draw connection lines
|
||||
expect(mockContext.beginPath).toHaveBeenCalled()
|
||||
expect(mockContext.moveTo).toHaveBeenCalled()
|
||||
expect(mockContext.lineTo).toHaveBeenCalled()
|
||||
expect(mockContext.stroke).toHaveBeenCalled()
|
||||
|
||||
// Should draw connection slots
|
||||
expect(mockContext.arc).toHaveBeenCalled()
|
||||
expect(mockContext.fill).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle light theme colors', () => {
|
||||
vi.mocked(useColorPaletteStore).mockReturnValue({
|
||||
completedActivePalette: { light_theme: true }
|
||||
} as any)
|
||||
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: true,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// Color adjustment should be called for light theme
|
||||
expect(adjustColor).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should calculate correct offsets for centering', () => {
|
||||
const context: MinimapRenderContext = {
|
||||
bounds: { minX: 0, minY: 0, width: 200, height: 100 },
|
||||
scale: 0.5,
|
||||
settings: {
|
||||
nodeColors: false,
|
||||
showLinks: false,
|
||||
showGroups: false,
|
||||
renderBypass: false,
|
||||
renderError: false
|
||||
},
|
||||
width: 250,
|
||||
height: 200
|
||||
}
|
||||
|
||||
renderMinimapToCanvas(mockCanvas, mockGraph, context)
|
||||
|
||||
// With bounds 200x100 at scale 0.5 = 100x50
|
||||
// Canvas is 250x200, so offset should be (250-100)/2 = 75, (200-50)/2 = 75
|
||||
// This affects node positioning
|
||||
expect(mockContext.fillRect).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user