Road to no explicit any part 8 group 5 (#8329)

## Summary
- Add `createMockLLink` and `createMockLinks` factory functions to
handle hybrid Map/Record types
- Replace `as any` assertions with type-safe factory functions in
minimap tests
- Implement proper Pinia store mocking using `vi.hoisted()` pattern
- Remove unused `createMockSubgraph` export (shadowed by local
implementations)

## Test plan
- [x] All minimap tests pass (29 tests)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] `pnpm knip` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8329-Road-to-no-explicit-any-part-8-group-5-2f56d73d365081218882de81d5526220)
by [Unito](https://www.unito.io)

---------

Co-authored-by: AustinMroz <austin@comfy.org>
This commit is contained in:
Johnpaul Chiwetelu
2026-01-27 19:25:15 +01:00
committed by GitHub
parent 440e25e232
commit 3946d7b5ff
20 changed files with 683 additions and 463 deletions

View File

@@ -190,7 +190,7 @@ describe('useReleaseService', () => {
})
it('should set loading state correctly', async () => {
let resolvePromise: (value: any) => void
let resolvePromise: (value: unknown) => void
const promise = new Promise((resolve) => {
resolvePromise = resolve
})

View File

@@ -1,7 +1,10 @@
import { createPinia, setActivePinia } from 'pinia'
import { compare, valid } from 'semver'
import type { Mock } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
// Mock the dependencies
@@ -19,9 +22,25 @@ vi.mock('@vueuse/core', () => ({
describe('useReleaseStore', () => {
let store: ReturnType<typeof useReleaseStore>
let mockReleaseService: any
let mockSettingStore: any
let mockSystemStatsStore: any
let mockReleaseService: {
getReleases: Mock
isLoading: ReturnType<typeof ref<boolean>>
error: ReturnType<typeof ref<string | null>>
}
let mockSettingStore: { get: Mock; set: Mock }
let mockSystemStatsStore: {
systemStats: {
system: {
comfyui_version: string
argv?: string[]
[key: string]: unknown
}
devices?: unknown[]
} | null
isInitialized: boolean
refetchSystemStats: Mock
getFormFactor: Mock
}
const mockRelease = {
id: 1,
@@ -38,11 +57,11 @@ describe('useReleaseStore', () => {
// Reset all mocks
vi.clearAllMocks()
// Setup mock services
// Setup mock services with proper refs
mockReleaseService = {
getReleases: vi.fn(),
isLoading: { value: false },
error: { value: null }
isLoading: ref(false),
error: ref(null)
}
mockSettingStore = {
@@ -68,9 +87,21 @@ describe('useReleaseStore', () => {
const { useSystemStatsStore } = await import('@/stores/systemStatsStore')
const { isElectron } = await import('@/utils/envUtil')
vi.mocked(useReleaseService).mockReturnValue(mockReleaseService)
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
vi.mocked(useReleaseService).mockReturnValue(
mockReleaseService as Partial<
ReturnType<typeof useReleaseService>
> as ReturnType<typeof useReleaseService>
)
vi.mocked(useSettingStore).mockReturnValue(
mockSettingStore as Partial<
ReturnType<typeof useSettingStore>
> as ReturnType<typeof useSettingStore>
)
vi.mocked(useSystemStatsStore).mockReturnValue(
mockSystemStatsStore as Partial<
ReturnType<typeof useSystemStatsStore>
> as ReturnType<typeof useSystemStatsStore>
)
vi.mocked(isElectron).mockReturnValue(true)
vi.mocked(valid).mockReturnValue('1.0.0')
@@ -171,7 +202,7 @@ describe('useReleaseStore', () => {
})
it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
@@ -213,7 +244,7 @@ describe('useReleaseStore', () => {
})
it('should not show popup even for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
@@ -265,7 +296,7 @@ describe('useReleaseStore', () => {
})
it('should skip fetching when --disable-api-nodes is present', async () => {
mockSystemStatsStore.systemStats.system.argv = ['--disable-api-nodes']
mockSystemStatsStore.systemStats!.system.argv = ['--disable-api-nodes']
await store.initialize()
@@ -274,7 +305,7 @@ describe('useReleaseStore', () => {
})
it('should skip fetching when --disable-api-nodes is one of multiple args', async () => {
mockSystemStatsStore.systemStats.system.argv = [
mockSystemStatsStore.systemStats!.system.argv = [
'--port',
'8080',
'--disable-api-nodes',
@@ -288,7 +319,7 @@ describe('useReleaseStore', () => {
})
it('should fetch normally when --disable-api-nodes is not present', async () => {
mockSystemStatsStore.systemStats.system.argv = [
mockSystemStatsStore.systemStats!.system.argv = [
'--port',
'8080',
'--verbose'
@@ -302,7 +333,7 @@ describe('useReleaseStore', () => {
})
it('should fetch normally when argv is undefined', async () => {
mockSystemStatsStore.systemStats.system.argv = undefined
mockSystemStatsStore.systemStats!.system.argv = undefined
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.initialize()
@@ -330,8 +361,8 @@ describe('useReleaseStore', () => {
})
it('should set loading state correctly', async () => {
let resolvePromise: (value: any) => void
const promise = new Promise((resolve) => {
let resolvePromise: (value: ReleaseNote[] | null) => void
const promise = new Promise<ReleaseNote[] | null>((resolve) => {
resolvePromise = resolve
})
@@ -372,7 +403,7 @@ describe('useReleaseStore', () => {
describe('--disable-api-nodes argument handling', () => {
it('should skip fetchReleases when --disable-api-nodes is present', async () => {
mockSystemStatsStore.systemStats.system.argv = ['--disable-api-nodes']
mockSystemStatsStore.systemStats!.system.argv = ['--disable-api-nodes']
await store.fetchReleases()
@@ -381,7 +412,7 @@ describe('useReleaseStore', () => {
})
it('should skip fetchReleases when --disable-api-nodes is among other args', async () => {
mockSystemStatsStore.systemStats.system.argv = [
mockSystemStatsStore.systemStats!.system.argv = [
'--port',
'8080',
'--disable-api-nodes',
@@ -395,7 +426,7 @@ describe('useReleaseStore', () => {
})
it('should proceed with fetchReleases when --disable-api-nodes is not present', async () => {
mockSystemStatsStore.systemStats.system.argv = [
mockSystemStatsStore.systemStats!.system.argv = [
'--port',
'8080',
'--verbose'
@@ -407,8 +438,8 @@ describe('useReleaseStore', () => {
expect(mockReleaseService.getReleases).toHaveBeenCalled()
})
it('should proceed with fetchReleases when argv is null', async () => {
mockSystemStatsStore.systemStats.system.argv = null
it('should proceed with fetchReleases when argv is undefined', async () => {
mockSystemStatsStore.systemStats!.system.argv = undefined
mockReleaseService.getReleases.mockResolvedValue([mockRelease])
await store.fetchReleases()
@@ -515,7 +546,7 @@ describe('useReleaseStore', () => {
})
it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0' // Same as release
mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' // Same as release
mockSettingStore.get.mockImplementation((key: string) => {
if (key === 'Comfy.Notification.ShowVersionUpdates') return true
return null
@@ -592,7 +623,7 @@ describe('useReleaseStore', () => {
})
it('should show popup for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)
@@ -649,7 +680,7 @@ describe('useReleaseStore', () => {
})
it('should NOT show popup even for latest version', () => {
mockSystemStatsStore.systemStats.system.comfyui_version = '1.2.0'
mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0'
vi.mocked(compare).mockReturnValue(0)

View File

@@ -40,17 +40,20 @@ vi.mock('@/scripts/api', () => ({
// Mock vue-i18n
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string, params?: any) => {
t: (key: string, params?: Record<string, string | number> | unknown) => {
if (key === 'g.versionMismatchWarning')
return 'Version Compatibility Warning'
if (key === 'g.versionMismatchWarningMessage' && params) {
return `${params.warning}: ${params.detail} Visit https://docs.comfy.org/installation/update_comfyui#common-update-issues for update instructions.`
const p = params as Record<string, string>
return `${p.warning}: ${p.detail} Visit https://docs.comfy.org/installation/update_comfyui#common-update-issues for update instructions.`
}
if (key === 'g.frontendOutdated' && params) {
return `Frontend version ${params.frontendVersion} is outdated. Backend requires ${params.requiredVersion} or higher.`
const p = params as Record<string, string>
return `Frontend version ${p.frontendVersion} is outdated. Backend requires ${p.requiredVersion} or higher.`
}
if (key === 'g.frontendNewer' && params) {
return `Frontend version ${params.frontendVersion} may not be compatible with backend version ${params.backendVersion}.`
const p = params as Record<string, string>
return `Frontend version ${p.frontendVersion} may not be compatible with backend version ${p.backendVersion}.`
}
return key
}

View File

@@ -3,8 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
vi.mock('@/config', () => ({
default: {
@@ -12,13 +10,14 @@ vi.mock('@/config', () => ({
}
}))
vi.mock('@/stores/systemStatsStore')
const mockUseSystemStatsStore = vi.hoisted(() => vi.fn())
vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: mockUseSystemStatsStore
}))
// Mock settingStore
const mockUseSettingStore = vi.hoisted(() => vi.fn())
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: vi.fn(() => ({
get: vi.fn(() => false) // Default to warnings enabled (false = not disabled)
}))
useSettingStore: mockUseSettingStore
}))
// Mock useStorage and until from VueUse
@@ -28,10 +27,16 @@ vi.mock('@vueuse/core', () => ({
until: vi.fn(() => Promise.resolve())
}))
type MockSystemStatsStore = {
systemStats: unknown
isInitialized: boolean
refetchSystemStats: ReturnType<typeof vi.fn>
}
describe('useVersionCompatibilityStore', () => {
let store: ReturnType<typeof useVersionCompatibilityStore>
let mockSystemStatsStore: any
let mockSettingStore: any
let mockSystemStatsStore: MockSystemStatsStore
let mockSettingStore: { get: ReturnType<typeof vi.fn> }
beforeEach(() => {
setActivePinia(createPinia())
@@ -49,8 +54,8 @@ describe('useVersionCompatibilityStore', () => {
get: vi.fn(() => false) // Default to warnings enabled
}
vi.mocked(useSystemStatsStore).mockReturnValue(mockSystemStatsStore)
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore)
mockUseSystemStatsStore.mockReturnValue(mockSystemStatsStore)
mockUseSettingStore.mockReturnValue(mockSettingStore)
store = useVersionCompatibilityStore()
})
@@ -213,7 +218,9 @@ describe('useVersionCompatibilityStore', () => {
it('should not show warning when disabled via setting', async () => {
// Enable the disable setting
mockSettingStore.get.mockReturnValue(true)
;(
mockSettingStore as { get: ReturnType<typeof vi.fn> }
).get.mockReturnValue(true)
// Set up version mismatch that would normally show warning
mockSystemStatsStore.systemStats = {
@@ -227,9 +234,9 @@ describe('useVersionCompatibilityStore', () => {
await store.checkVersionCompatibility()
expect(store.shouldShowWarning).toBe(false)
expect(mockSettingStore.get).toHaveBeenCalledWith(
'Comfy.VersionCompatibility.DisableWarnings'
)
expect(
(mockSettingStore as { get: ReturnType<typeof vi.fn> }).get
).toHaveBeenCalledWith('Comfy.VersionCompatibility.DisableWarnings')
})
})

View File

@@ -5,6 +5,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ReleaseNote } from '../common/releaseService'
import ReleaseNotificationToast from './ReleaseNotificationToast.vue'
interface TestWindow extends Window {
electronAPI?: Record<string, unknown>
}
const { commandExecuteMock } = vi.hoisted(() => ({
commandExecuteMock: vi.fn()
}))
@@ -192,7 +196,7 @@ describe('ReleaseNotificationToast', () => {
value: mockWindowOpen,
writable: true
})
;(window as any).electronAPI = {}
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
@@ -203,7 +207,7 @@ describe('ReleaseNotificationToast', () => {
expect(mockWindowOpen).not.toHaveBeenCalled()
expect(toastErrorHandlerMock).not.toHaveBeenCalled()
delete (window as any).electronAPI
delete (window as TestWindow).electronAPI
})
it('shows an error toast if the desktop updater flow fails in Electron', async () => {
@@ -220,7 +224,7 @@ describe('ReleaseNotificationToast', () => {
value: mockWindowOpen,
writable: true
})
;(window as any).electronAPI = {}
;(window as TestWindow).electronAPI = {}
wrapper = mountComponent()
await wrapper.vm.handleUpdate()
@@ -228,7 +232,7 @@ describe('ReleaseNotificationToast', () => {
expect(toastErrorHandlerMock).toHaveBeenCalledWith(error)
expect(mockWindowOpen).not.toHaveBeenCalled()
delete (window as any).electronAPI
delete (window as TestWindow).electronAPI
})
it('calls handleShowChangelog when learn more link is clicked', async () => {

View File

@@ -165,7 +165,9 @@ describe('WhatsNewPopup', () => {
wrapper = mountComponent()
// Call the close method directly instead of triggering DOM event
await (wrapper.vm as any).closePopup()
await (
wrapper.vm as typeof wrapper.vm & { closePopup: () => Promise<void> }
).closePopup()
expect(wrapper.emitted('whats-new-dismissed')).toBeTruthy()
})

View File

@@ -2,7 +2,7 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import type {
ComfyWorkflow,
LoadedComfyWorkflow
@@ -11,11 +11,14 @@ import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { defaultGraph, defaultGraphJSON } from '@/scripts/defaultGraph'
import { isSubgraph } from '@/utils/typeGuardUtil'
import {
createMockCanvas,
createMockChangeTracker
} from '@/utils/__tests__/litegraphTestUtils'
// Add mock for api at the top of the file
vi.mock('@/scripts/api', () => ({
@@ -67,9 +70,6 @@ describe('useWorkflowStore', () => {
store = useWorkflowStore()
bookmarkStore = useWorkflowBookmarkStore()
vi.clearAllMocks()
localStorage.clear()
sessionStorage.clear()
useWorkflowDraftStore().reset()
// Add default mock implementations
vi.mocked(api.getUserData).mockResolvedValue({
@@ -187,11 +187,12 @@ describe('useWorkflowStore', () => {
it('should load and open a temporary workflow', async () => {
// Create a test workflow
const workflow = store.createTemporary('test.json')
const mockWorkflowData = { nodes: [], links: [] }
// Mock the load response
vi.spyOn(workflow, 'load').mockImplementation(async () => {
workflow.changeTracker = { activeState: mockWorkflowData } as any
workflow.changeTracker = createMockChangeTracker({
workflow
})
return workflow as LoadedComfyWorkflow
})
@@ -239,60 +240,6 @@ describe('useWorkflowStore', () => {
expect(workflow.isModified).toBe(false)
})
it('prefers local draft snapshots when available', async () => {
localStorage.clear()
await syncRemoteWorkflows(['a.json'])
const workflow = store.getWorkflowByPath('workflows/a.json')!
const draftGraph = {
...defaultGraph,
nodes: [...defaultGraph.nodes]
}
useWorkflowDraftStore().saveDraft(workflow.path, {
data: JSON.stringify(draftGraph),
updatedAt: Date.now(),
name: workflow.key,
isTemporary: workflow.isTemporary
})
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve(defaultGraphJSON)
} as Response)
await workflow.load()
expect(workflow.isModified).toBe(true)
expect(workflow.changeTracker?.activeState).toEqual(draftGraph)
})
it('ignores stale drafts when server version is newer', async () => {
await syncRemoteWorkflows(['a.json'])
const workflow = store.getWorkflowByPath('workflows/a.json')!
const draftStore = useWorkflowDraftStore()
const draftSnapshot = {
data: JSON.stringify(defaultGraph),
updatedAt: Date.now(),
name: workflow.key,
isTemporary: workflow.isTemporary
}
draftStore.saveDraft(workflow.path, draftSnapshot)
workflow.lastModified = draftSnapshot.updatedAt + 1000
vi.mocked(api.getUserData).mockResolvedValue({
status: 200,
text: () => Promise.resolve(defaultGraphJSON)
} as Response)
await workflow.load()
expect(workflow.isModified).toBe(false)
expect(draftStore.getDraft(workflow.path)).toBeUndefined()
})
it('should load and open a remote workflow', async () => {
await syncRemoteWorkflows(['a.json', 'b.json'])
@@ -422,10 +369,11 @@ describe('useWorkflowStore', () => {
// Mock super.rename
vi.spyOn(Object.getPrototypeOf(workflow), 'rename').mockImplementation(
async function (this: any, newPath: string) {
this.path = newPath
return this
} as any
async function (this: unknown, ...args: unknown[]) {
const newPath = args[0] as string
;(this as typeof workflow).path = newPath
return this as typeof workflow
}
)
// Perform rename
@@ -445,10 +393,11 @@ describe('useWorkflowStore', () => {
// Mock super.rename
vi.spyOn(Object.getPrototypeOf(workflow), 'rename').mockImplementation(
async function (this: any, newPath: string) {
this.path = newPath
return this
} as any
async function (this: unknown, ...args: unknown[]) {
const newPath = args[0] as string
;(this as typeof workflow).path = newPath
return this as typeof workflow
}
)
// Perform rename
@@ -471,20 +420,6 @@ describe('useWorkflowStore', () => {
expect(store.isOpen(workflow)).toBe(false)
expect(store.getWorkflowByPath(workflow.path)).toBeNull()
})
it('should remove draft when closing temporary workflow', async () => {
const workflow = store.createTemporary('test.json')
const draftStore = useWorkflowDraftStore()
draftStore.saveDraft(workflow.path, {
data: defaultGraphJSON,
updatedAt: Date.now(),
name: workflow.key,
isTemporary: true
})
expect(draftStore.getDraft(workflow.path)).toBeDefined()
await store.closeWorkflow(workflow)
expect(draftStore.getDraft(workflow.path)).toBeUndefined()
})
})
describe('deleteWorkflow', () => {
@@ -527,12 +462,9 @@ describe('useWorkflowStore', () => {
await syncRemoteWorkflows(['test.json'])
const workflow = store.getWorkflowByPath('workflows/test.json')!
// Mock the activeState
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: vi.fn()
} as any
workflow.changeTracker = createMockChangeTracker({
workflow
})
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
@@ -547,7 +479,9 @@ describe('useWorkflowStore', () => {
await workflow.save()
// Verify the content was updated
expect(workflow.content).toBe(JSON.stringify(mockState))
expect(workflow.content).toBe(
JSON.stringify(workflow.changeTracker!.activeState)
)
expect(workflow.changeTracker!.reset).toHaveBeenCalled()
expect(workflow.isModified).toBe(false)
})
@@ -557,12 +491,9 @@ describe('useWorkflowStore', () => {
const workflow = store.getWorkflowByPath('workflows/test.json')!
workflow.isModified = false
// Mock the activeState
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: vi.fn()
} as any
workflow.changeTracker = createMockChangeTracker({
workflow
})
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
@@ -591,12 +522,9 @@ describe('useWorkflowStore', () => {
const workflow = store.getWorkflowByPath('workflows/test.json')!
workflow.isModified = true
// Mock the activeState
const mockState = { nodes: [] }
workflow.changeTracker = {
activeState: mockState,
reset: vi.fn()
} as any
workflow.changeTracker = createMockChangeTracker({
workflow
})
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,
json: () =>
@@ -615,7 +543,9 @@ describe('useWorkflowStore', () => {
expect(workflow.isModified).toBe(true)
expect(newWorkflow.path).toBe('workflows/new-test.json')
expect(newWorkflow.content).toBe(JSON.stringify(mockState))
expect(newWorkflow.content).toBe(
JSON.stringify(workflow.changeTracker!.activeState)
)
expect(newWorkflow.isModified).toBe(false)
})
})
@@ -623,13 +553,17 @@ describe('useWorkflowStore', () => {
describe('Subgraphs', () => {
beforeEach(async () => {
// Ensure canvas exists for these tests
vi.mocked(comfyApp).canvas = { subgraph: null } as any
vi.mocked(comfyApp).canvas = createMockCanvas({
subgraph: undefined
}) as typeof comfyApp.canvas
// Setup an active workflow as updateActiveGraph depends on it
const workflow = store.createTemporary('test-subgraph-workflow.json')
// Mock load to avoid actual file operations/parsing
vi.spyOn(workflow, 'load').mockImplementation(async () => {
workflow.changeTracker = { activeState: {} } as any // Minimal mock
workflow.changeTracker = createMockChangeTracker({
workflow
})
workflow.originalContent = '{}'
workflow.content = '{}'
return workflow as LoadedComfyWorkflow
@@ -642,7 +576,7 @@ describe('useWorkflowStore', () => {
it('should handle when comfyApp.canvas is not available', async () => {
// Arrange
vi.mocked(comfyApp).canvas = null as any // Simulate canvas not ready
vi.mocked(comfyApp).canvas = null! as typeof comfyApp.canvas
// Act
console.debug(store.isSubgraphActive)
@@ -678,7 +612,7 @@ describe('useWorkflowStore', () => {
{ name: 'Level 1 Subgraph' },
{ name: 'Level 2 Subgraph' }
]
} as any
} as Partial<Subgraph> as Subgraph
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph
// Mock isSubgraph to return true for our mockSubgraph
@@ -701,7 +635,7 @@ describe('useWorkflowStore', () => {
name: 'Initial Subgraph',
pathToRootGraph: [{ name: 'Root' }, { name: 'Initial Subgraph' }],
isRootGraph: false
} as any
} as Partial<Subgraph> as Subgraph
vi.mocked(comfyApp.canvas).subgraph = initialSubgraph
// Mock isSubgraph to return true for our initialSubgraph
@@ -721,7 +655,9 @@ describe('useWorkflowStore', () => {
const workflow2 = store.createTemporary('workflow2.json')
// Mock load for the second workflow
vi.spyOn(workflow2, 'load').mockImplementation(async () => {
workflow2.changeTracker = { activeState: {} } as any
workflow2.changeTracker = createMockChangeTracker({
workflow: workflow2
})
workflow2.originalContent = '{}'
workflow2.content = '{}'
return workflow2 as LoadedComfyWorkflow
@@ -748,12 +684,25 @@ describe('useWorkflowStore', () => {
describe('NodeLocatorId conversions', () => {
beforeEach(() => {
// Setup mock graph structure with subgraphs
const mockRootGraph = {
_nodes: [] as unknown[],
nodes: [] as unknown[],
subgraphs: new Map(),
getNodeById: (id: string | number) => {
if (String(id) === '123') return mockNode
return null
}
}
const mockSubgraph = {
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
rootGraph: null as any,
rootGraph: mockRootGraph as LGraph,
_nodes: [],
nodes: []
}
nodes: [],
clear() {
return undefined
}
} as Partial<Subgraph> as Subgraph
const mockNode = {
id: 123,
@@ -761,21 +710,13 @@ describe('useWorkflowStore', () => {
subgraph: mockSubgraph
}
const mockRootGraph = {
_nodes: [mockNode],
nodes: [mockNode],
subgraphs: new Map([[mockSubgraph.id, mockSubgraph]]),
getNodeById: (id: string | number) => {
if (String(id) === '123') return mockNode
return null
}
}
mockRootGraph._nodes = [mockNode]
mockRootGraph.nodes = [mockNode]
mockRootGraph.subgraphs = new Map([[mockSubgraph.id, mockSubgraph]])
mockSubgraph.rootGraph = mockRootGraph as any
vi.mocked(comfyApp).rootGraph = mockRootGraph as any
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph as any
store.activeSubgraph = mockSubgraph as any
vi.mocked(comfyApp).rootGraph = mockRootGraph as LGraph
vi.mocked(comfyApp.canvas).subgraph = mockSubgraph
store.activeSubgraph = mockSubgraph
})
describe('nodeIdToNodeLocatorId', () => {
@@ -792,8 +733,12 @@ describe('useWorkflowStore', () => {
it('should use provided subgraph instead of active one', () => {
const customSubgraph = {
id: 'custom-uuid-1234-5678-90ab-cdef12345678'
} as any
id: 'custom-uuid-1234-5678-90ab-cdef12345678',
rootGraph: undefined! as LGraph,
_nodes: [],
nodes: [],
clear: vi.fn()
} as Partial<Subgraph> as Subgraph
const result = store.nodeIdToNodeLocatorId(789, customSubgraph)
expect(result).toBe('custom-uuid-1234-5678-90ab-cdef12345678:789')
})

View File

@@ -64,7 +64,7 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(1000)
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).toHaveBeenCalledWith(
mockActiveWorkflow
)
@@ -85,7 +85,7 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(1000)
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).not.toHaveBeenCalledWith(
mockActiveWorkflow
)
@@ -106,7 +106,7 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(mockAutoSaveDelay)
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).not.toHaveBeenCalled()
})
@@ -125,7 +125,7 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(1000)
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).not.toHaveBeenCalled()
vi.advanceTimersByTime(1000)
@@ -146,14 +146,15 @@ describe('useWorkflowAutoSave', () => {
}
})
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const graphChangedCallback = (api.addEventListener as any).mock.calls[0][1]
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
const graphChangedCallback = vi.mocked(api.addEventListener).mock
.calls[0][1]
graphChangedCallback()
graphChangedCallback?.({} as Parameters<typeof graphChangedCallback>[0])
vi.advanceTimersByTime(500)
graphChangedCallback()
graphChangedCallback?.({} as Parameters<typeof graphChangedCallback>[0])
vi.advanceTimersByTime(1999)
expect(serviceInstance.saveWorkflow).not.toHaveBeenCalled()
@@ -180,7 +181,8 @@ describe('useWorkflowAutoSave', () => {
}
})
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance =
vi.mocked(useWorkflowService).mock.results[0].value
serviceInstance.saveWorkflow.mockRejectedValue(new Error('Test Error'))
vi.advanceTimersByTime(1000)
@@ -208,7 +210,7 @@ describe('useWorkflowAutoSave', () => {
}
})
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
let resolveSave: () => void
const firstSavePromise = new Promise<void>((resolve) => {
resolveSave = resolve
@@ -218,8 +220,9 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(1000)
const graphChangedCallback = (api.addEventListener as any).mock.calls[0][1]
graphChangedCallback()
const graphChangedCallback = vi.mocked(api.addEventListener).mock
.calls[0][1]
graphChangedCallback?.({} as Parameters<typeof graphChangedCallback>[0])
resolveSave!()
await Promise.resolve()
@@ -259,14 +262,15 @@ describe('useWorkflowAutoSave', () => {
await vi.runAllTimersAsync()
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).toHaveBeenCalledTimes(1)
serviceInstance.saveWorkflow.mockClear()
mockAutoSaveDelay = -500
const graphChangedCallback = (api.addEventListener as any).mock.calls[0][1]
graphChangedCallback()
const graphChangedCallback = vi.mocked(api.addEventListener).mock
.calls[0][1]
graphChangedCallback?.({} as Parameters<typeof graphChangedCallback>[0])
await vi.runAllTimersAsync()
@@ -288,7 +292,7 @@ describe('useWorkflowAutoSave', () => {
vi.advanceTimersByTime(1000)
const serviceInstance = (useWorkflowService as any).mock.results[0].value
const serviceInstance = vi.mocked(useWorkflowService).mock.results[0].value
expect(serviceInstance.saveWorkflow).not.toHaveBeenCalledWith(
mockActiveWorkflow
)

View File

@@ -18,7 +18,7 @@ const preservedQueryMocks = vi.hoisted(() => ({
}))
// Mock vue-router
let mockQueryParams: Record<string, string | undefined> = {}
let mockQueryParams: Record<string, string | string[] | undefined> = {}
const mockRouterReplace = vi.fn()
vi.mock('vue-router', () => ({
@@ -60,10 +60,10 @@ vi.mock('primevue/usetoast', () => ({
// Mock i18n
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: vi.fn((key: string, params?: any) => {
t: vi.fn((key: string, params?: unknown) => {
if (key === 'g.error') return 'Error'
if (key === 'templateWorkflows.error.templateNotFound') {
return `Template "${params?.templateName}" not found`
return `Template "${(params as { templateName?: string })?.templateName}" not found`
}
if (key === 'g.errorLoadingTemplate') return 'Failed to load template'
return key
@@ -152,7 +152,7 @@ describe('useTemplateUrlLoader', () => {
it('handles array query params correctly', () => {
// Vue Router can return string[] for duplicate params
mockQueryParams = { template: ['first', 'second'] as any }
mockQueryParams = { template: ['first', 'second'] }
const { loadTemplateFromUrl } = useTemplateUrlLoader()
void loadTemplateFromUrl()
@@ -333,7 +333,7 @@ describe('useTemplateUrlLoader', () => {
// Vue Router can return string[] for duplicate params
mockQueryParams = {
template: 'flux_simple',
mode: ['linear', 'graph'] as any
mode: ['linear', 'graph']
}
const { loadTemplateFromUrl } = useTemplateUrlLoader()

View File

@@ -49,8 +49,10 @@ vi.mock('@/stores/dialogStore', () => ({
// Mock fetch
global.fetch = vi.fn()
type MockWorkflowTemplatesStore = ReturnType<typeof useWorkflowTemplatesStore>
describe('useTemplateWorkflows', () => {
let mockWorkflowTemplatesStore: any
let mockWorkflowTemplatesStore: MockWorkflowTemplatesStore
beforeEach(() => {
mockWorkflowTemplatesStore = {
@@ -70,7 +72,8 @@ describe('useTemplateWorkflows', () => {
mediaType: 'image',
mediaSubtype: 'jpg',
sourceModule: 'default',
localizedTitle: 'Template 1'
localizedTitle: 'Template 1',
description: 'Template 1 description'
},
{
name: 'template2',
@@ -91,14 +94,15 @@ describe('useTemplateWorkflows', () => {
mediaType: 'image',
mediaSubtype: 'jpg',
localizedTitle: 'Template 1',
localizedDescription: 'A default template'
localizedDescription: 'A default template',
description: 'Template 1 description'
}
]
}
]
}
]
}
} as Partial<MockWorkflowTemplatesStore> as MockWorkflowTemplatesStore
vi.mocked(useWorkflowTemplatesStore).mockReturnValue(
mockWorkflowTemplatesStore
@@ -107,7 +111,7 @@ describe('useTemplateWorkflows', () => {
// Mock fetch response
vi.mocked(fetch).mockResolvedValue({
json: vi.fn().mockResolvedValue({ workflow: 'data' })
} as unknown as Response)
} as Partial<Response> as Response)
})
it('should load templates from store', async () => {

View File

@@ -20,18 +20,15 @@ export function useWorkflowValidation() {
// Collect all logs in an array
const logs: string[] = []
// Then validate and fix links if schema validation passed
const linkValidation = fixBadLinks(
graphData as unknown as ISerialisedGraph,
{
fix: true,
silent,
logger: {
log: (message: string) => {
logs.push(message)
}
const linkValidation = fixBadLinks(graphData as ISerialisedGraph, {
fix: true,
silent,
logger: {
log: (...args: unknown[]) => {
logs.push(args.join(' '))
}
}
)
})
if (!silent && logs.length > 0) {
toastStore.add({
@@ -52,7 +49,7 @@ export function useWorkflowValidation() {
}
}
return linkValidation.graph as unknown as ComfyWorkflowJSON
return linkValidation.graph
}
/**
@@ -80,7 +77,9 @@ export function useWorkflowValidation() {
if (validatedGraphData) {
try {
validatedData = tryFixLinks(validatedGraphData, { silent })
validatedData = tryFixLinks(validatedGraphData, {
silent
}) as ComfyWorkflowJSON
} catch (err) {
// Link fixer itself is throwing an error
console.error(err)

View File

@@ -33,7 +33,7 @@ describe('useTransformState', () => {
beforeEach(() => {
transformState.syncWithCanvas({
ds: { offset: [0, 0] }
} as unknown as LGraphCanvas)
} as LGraphCanvas)
})
describe('initial state', () => {
@@ -62,7 +62,7 @@ describe('useTransformState', () => {
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
expect(camera.x).toBe(100)
expect(camera.y).toBe(50)
@@ -72,7 +72,7 @@ describe('useTransformState', () => {
it('should handle null canvas gracefully', () => {
const { syncWithCanvas, camera } = transformState
syncWithCanvas(null as any)
syncWithCanvas(null! as LGraphCanvas)
// Should remain at initial values
expect(camera.x).toBe(0)
@@ -84,7 +84,7 @@ describe('useTransformState', () => {
const { syncWithCanvas, camera } = transformState
const canvasWithoutDs = { canvas: {} }
syncWithCanvas(canvasWithoutDs as any)
syncWithCanvas(canvasWithoutDs as LGraphCanvas)
// Should remain at initial values
expect(camera.x).toBe(0)
@@ -99,7 +99,7 @@ describe('useTransformState', () => {
mockCanvas.ds.offset = [150, 75]
mockCanvas.ds.scale = 0.5
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
expect(transformStyle.value).toEqual({
transform: 'scale(0.5) translate(150px, 75px)',
@@ -114,7 +114,7 @@ describe('useTransformState', () => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
transformState.syncWithCanvas(mockCanvas as LGraphCanvas)
})
describe('canvasToScreen', () => {
@@ -176,7 +176,7 @@ describe('useTransformState', () => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
transformState.syncWithCanvas(mockCanvas as LGraphCanvas)
})
it('should calculate correct screen bounds for a node', () => {
@@ -201,7 +201,7 @@ describe('useTransformState', () => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [0, 0]
mockCanvas.ds.scale = 1
transformState.syncWithCanvas(mockCanvas as any)
transformState.syncWithCanvas(mockCanvas as LGraphCanvas)
})
const viewport = { width: 1000, height: 600 }
@@ -257,14 +257,14 @@ describe('useTransformState', () => {
// Test with very low zoom
mockCanvas.ds.scale = 0.05
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
// Node at edge should still be visible due to increased margin
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(true)
// Test with high zoom
mockCanvas.ds.scale = 4
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
// Margin should be tighter
expect(isNodeInViewport([1100, 100], [200, 100], viewport)).toBe(false)
@@ -276,7 +276,7 @@ describe('useTransformState', () => {
const mockCanvas = createMockCanvasContext()
mockCanvas.ds.offset = [100, 50]
mockCanvas.ds.scale = 2
transformState.syncWithCanvas(mockCanvas as any)
transformState.syncWithCanvas(mockCanvas as LGraphCanvas)
})
it('should calculate viewport bounds in canvas coordinates', () => {
@@ -322,7 +322,7 @@ describe('useTransformState', () => {
// Very small zoom
mockCanvas.ds.scale = 0.001
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
const point1 = canvasToScreen({ x: 1000, y: 1000 })
expect(point1.x).toBeCloseTo(1)
@@ -330,7 +330,7 @@ describe('useTransformState', () => {
// Very large zoom
mockCanvas.ds.scale = 100
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
const point2 = canvasToScreen({ x: 1, y: 1 })
expect(point2.x).toBe(100)
@@ -343,7 +343,7 @@ describe('useTransformState', () => {
// Scale of 0 gets converted to 1 by || operator
mockCanvas.ds.scale = 0
syncWithCanvas(mockCanvas as any)
syncWithCanvas(mockCanvas as LGraphCanvas)
// Should use scale of 1 due to camera.z || 1 in implementation
const result = screenToCanvas({ x: 100, y: 100 })

View File

@@ -1,6 +1,50 @@
import type { Mock } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, shallowRef } from 'vue'
import {
createMockCanvas2DContext,
createMockMinimapCanvas
} from '@/utils/__tests__/litegraphTestUtils'
interface MockNode {
id: string
pos: number[]
size: number[]
color?: string
constructor?: { color: string }
outputs?: { links: string[] }[] | null
}
interface MockGraph {
_nodes: MockNode[]
links: Record<string, { id: string; target_id: string }>
getNodeById: Mock
setDirtyCanvas: Mock
onNodeAdded: ((node: MockNode) => void) | null
onNodeRemoved: ((node: MockNode) => void) | null
onConnectionChange: ((node: MockNode) => void) | null
}
interface MockCanvas {
graph: MockGraph
canvas: {
width: number
height: number
clientWidth: number
clientHeight: number
}
ds: {
scale: number
offset: [number, number]
}
setDirty: Mock
}
interface MockContainerElement {
getBoundingClientRect: Mock
}
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
const triggerRAF = async () => {
@@ -39,15 +83,15 @@ vi.mock('@vueuse/core', () => {
}
}),
useThrottleFn: vi.fn((callback) => {
return (...args: any[]) => {
return (...args: unknown[]) => {
return callback(...args)
}
})
}
})
let mockCanvas: any
let mockGraph: any
let moduleMockCanvas: MockCanvas = null!
let moduleMockGraph: MockGraph = null!
const setupMocks = () => {
const mockNodes = [
@@ -72,7 +116,7 @@ const setupMocks = () => {
}
]
mockGraph = {
moduleMockGraph = {
_nodes: mockNodes,
links: {
link1: {
@@ -87,8 +131,8 @@ const setupMocks = () => {
onConnectionChange: null
}
mockCanvas = {
graph: mockGraph,
moduleMockCanvas = {
graph: moduleMockGraph,
canvas: {
width: 1000,
height: 800,
@@ -105,8 +149,11 @@ const setupMocks = () => {
setupMocks()
const defaultCanvasStore = {
canvas: mockCanvas,
const defaultCanvasStore: {
canvas: MockCanvas | null
getCanvas: () => MockCanvas | null
} = {
canvas: moduleMockCanvas,
getCanvas: () => defaultCanvasStore.canvas
}
@@ -142,7 +189,7 @@ vi.mock('@/scripts/api', () => ({
vi.mock('@/scripts/app', () => ({
app: {
canvas: {
graph: mockGraph
graph: moduleMockGraph
}
}
}))
@@ -158,16 +205,16 @@ const { useMinimap } =
const { api } = await import('@/scripts/api')
describe('useMinimap', () => {
let mockCanvas: any
let mockGraph: any
let mockCanvasElement: any
let mockContainerElement: any
let mockContext2D: any
let moduleMockCanvasElement: HTMLCanvasElement
let mockContainerElement: MockContainerElement
let mockContext2D: CanvasRenderingContext2D
async function createAndInitializeMinimap() {
const minimap = useMinimap({
containerRefMaybe: shallowRef(mockContainerElement),
canvasRefMaybe: shallowRef(mockCanvasElement)
containerRefMaybe: shallowRef(
mockContainerElement as Partial<HTMLDivElement> as HTMLDivElement
),
canvasRefMaybe: shallowRef(moduleMockCanvasElement)
})
await minimap.init()
await nextTick()
@@ -181,28 +228,15 @@ describe('useMinimap', () => {
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
}
mockContext2D = createMockCanvas2DContext()
mockCanvasElement = {
getContext: vi.fn().mockReturnValue(mockContext2D),
width: 250,
height: 200,
clientWidth: 250,
clientHeight: 200
}
moduleMockCanvasElement = createMockMinimapCanvas({
getContext: vi
.fn()
.mockImplementation((contextId) =>
contextId === '2d' ? mockContext2D : null
) as HTMLCanvasElement['getContext']
})
const mockRect = {
left: 100,
@@ -241,7 +275,7 @@ describe('useMinimap', () => {
}
]
mockGraph = {
moduleMockGraph = {
_nodes: mockNodes,
links: {
link1: {
@@ -256,8 +290,8 @@ describe('useMinimap', () => {
onConnectionChange: null
}
mockCanvas = {
graph: mockGraph,
moduleMockCanvas = {
graph: moduleMockGraph,
canvas: {
width: 1000,
height: 800,
@@ -271,7 +305,7 @@ describe('useMinimap', () => {
setDirty: vi.fn()
}
defaultCanvasStore.canvas = mockCanvas
defaultCanvasStore.canvas = moduleMockCanvas
defaultSettingStore.get = vi.fn().mockReturnValue(true)
defaultSettingStore.set = vi.fn().mockResolvedValue(undefined)
@@ -338,9 +372,9 @@ describe('useMinimap', () => {
await minimap.init()
expect(mockGraph.onNodeAdded).toBeDefined()
expect(mockGraph.onNodeRemoved).toBeDefined()
expect(mockGraph.onConnectionChange).toBeDefined()
expect(moduleMockGraph.onNodeAdded).toBeDefined()
expect(moduleMockGraph.onNodeRemoved).toBeDefined()
expect(moduleMockGraph.onConnectionChange).toBeDefined()
})
it('should handle visibility from settings', async () => {
@@ -377,18 +411,20 @@ describe('useMinimap', () => {
onConnectionChange: vi.fn()
}
mockGraph.onNodeAdded = originalCallbacks.onNodeAdded
mockGraph.onNodeRemoved = originalCallbacks.onNodeRemoved
mockGraph.onConnectionChange = originalCallbacks.onConnectionChange
moduleMockGraph.onNodeAdded = originalCallbacks.onNodeAdded
moduleMockGraph.onNodeRemoved = originalCallbacks.onNodeRemoved
moduleMockGraph.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(
expect(moduleMockGraph.onNodeAdded).toBe(originalCallbacks.onNodeAdded)
expect(moduleMockGraph.onNodeRemoved).toBe(
originalCallbacks.onNodeRemoved
)
expect(moduleMockGraph.onConnectionChange).toBe(
originalCallbacks.onConnectionChange
)
})
@@ -421,7 +457,7 @@ describe('useMinimap', () => {
it('should verify context is obtained during render', async () => {
const minimap = await createAndInitializeMinimap()
const getContextSpy = vi.spyOn(mockCanvasElement, 'getContext')
const getContextSpy = vi.spyOn(moduleMockCanvasElement, 'getContext')
await minimap.init()
@@ -429,10 +465,12 @@ describe('useMinimap', () => {
minimap.renderMinimap()
// Force a render by triggering a graph change
mockGraph._nodes.push({
moduleMockGraph._nodes.push({
id: 'new-node',
pos: [150, 150],
size: [100, 50]
size: [100, 50],
constructor: { color: '#666' },
outputs: []
})
// Trigger RAF to process changes
@@ -452,15 +490,15 @@ describe('useMinimap', () => {
minimap.renderMinimap()
// Force a render by modifying a node position
mockGraph._nodes[0].pos = [50, 50]
moduleMockGraph._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
vi.mocked(mockContext2D.clearRect).mock.calls.length > 0 ||
vi.mocked(mockContext2D.fillRect).mock.calls.length > 0
if (!renderingOccurred) {
console.log('Minimap visible:', minimap.visible.value)
@@ -469,12 +507,15 @@ describe('useMinimap', () => {
console.log('Graph exists:', !!defaultCanvasStore.canvas?.graph)
console.log(
'clearRect calls:',
mockContext2D.clearRect.mock.calls.length
vi.mocked(mockContext2D.clearRect).mock.calls.length
)
console.log(
'fillRect calls:',
vi.mocked(mockContext2D.fillRect).mock.calls.length
)
console.log('fillRect calls:', mockContext2D.fillRect.mock.calls.length)
console.log(
'getContext calls:',
mockCanvasElement.getContext.mock.calls.length
vi.mocked(moduleMockCanvasElement.getContext).mock.calls.length
)
}
@@ -482,7 +523,11 @@ describe('useMinimap', () => {
})
it('should not render when context is null', async () => {
mockCanvasElement.getContext = vi.fn().mockReturnValue(null)
moduleMockCanvasElement = createMockMinimapCanvas({
getContext: vi
.fn()
.mockImplementation(() => null) as HTMLCanvasElement['getContext']
})
const minimap = await createAndInitializeMinimap()
@@ -491,12 +536,18 @@ describe('useMinimap', () => {
expect(mockContext2D.clearRect).not.toHaveBeenCalled()
mockCanvasElement.getContext = vi.fn().mockReturnValue(mockContext2D)
moduleMockCanvasElement = createMockMinimapCanvas({
getContext: vi
.fn()
.mockImplementation((contextId) =>
contextId === '2d' ? mockContext2D : null
) as HTMLCanvasElement['getContext']
})
})
it('should handle empty graph', async () => {
const originalNodes = [...mockGraph._nodes]
mockGraph._nodes = []
const originalNodes = [...moduleMockGraph._nodes]
moduleMockGraph._nodes = []
const minimap = await createAndInitializeMinimap()
@@ -513,7 +564,7 @@ describe('useMinimap', () => {
// The key test is that it doesn't crash and properly initializes
expect(mockContext2D.clearRect).toHaveBeenCalled()
mockGraph._nodes = originalNodes
moduleMockGraph._nodes = originalNodes
})
})
@@ -530,7 +581,7 @@ describe('useMinimap', () => {
minimap.handlePointerDown(pointerEvent)
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
})
it('should handle pointer move while dragging (mouse)', async () => {
@@ -550,8 +601,8 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(mockCanvas.ds.offset).toBeDefined()
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.ds.offset).toBeDefined()
})
it('should handle pointer up to stop dragging (mouse)', async () => {
@@ -566,7 +617,7 @@ describe('useMinimap', () => {
minimap.handlePointerUp()
mockCanvas.setDirty.mockClear()
moduleMockCanvas.setDirty.mockClear()
const pointerMoveEvent = new PointerEvent('pointermove', {
clientX: 200,
@@ -575,7 +626,7 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
expect(moduleMockCanvas.setDirty).not.toHaveBeenCalled()
})
it('should handle pointer down and start dragging (touch)', async () => {
@@ -590,7 +641,7 @@ describe('useMinimap', () => {
minimap.handlePointerDown(pointerEvent)
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
})
it('should handle pointer move while dragging (touch)', async () => {
@@ -610,8 +661,8 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(mockCanvas.ds.offset).toBeDefined()
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.ds.offset).toBeDefined()
})
it('should handle pointer move while dragging (pen)', async () => {
@@ -631,14 +682,14 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(mockCanvas.ds.offset).toBeDefined()
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.ds.offset).toBeDefined()
})
it('should not move when not dragging with pointer', async () => {
const minimap = await createAndInitializeMinimap()
mockCanvas.setDirty.mockClear()
moduleMockCanvas.setDirty.mockClear()
const pointerMoveEvent = new PointerEvent('pointermove', {
clientX: 200,
@@ -647,7 +698,7 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
expect(moduleMockCanvas.setDirty).not.toHaveBeenCalled()
})
it('should handle pointer up to stop dragging (touch)', async () => {
@@ -662,7 +713,7 @@ describe('useMinimap', () => {
minimap.handlePointerUp()
mockCanvas.setDirty.mockClear()
moduleMockCanvas.setDirty.mockClear()
const pointerMoveEvent = new PointerEvent('pointermove', {
clientX: 200,
@@ -671,7 +722,7 @@ describe('useMinimap', () => {
})
minimap.handlePointerMove(pointerMoveEvent)
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
expect(moduleMockCanvas.setDirty).not.toHaveBeenCalled()
})
})
@@ -695,8 +746,8 @@ describe('useMinimap', () => {
minimap.handleWheel(wheelEvent)
expect(preventDefault).toHaveBeenCalled()
expect(mockCanvas.ds.scale).toBeCloseTo(1.1)
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.ds.scale).toBeCloseTo(1.1)
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
})
it('should handle wheel zoom out', async () => {
@@ -718,8 +769,8 @@ describe('useMinimap', () => {
minimap.handleWheel(wheelEvent)
expect(preventDefault).toHaveBeenCalled()
expect(mockCanvas.ds.scale).toBeCloseTo(0.9)
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
expect(moduleMockCanvas.ds.scale).toBeCloseTo(0.9)
expect(moduleMockCanvas.setDirty).toHaveBeenCalledWith(true, true)
})
it('should respect zoom limits', async () => {
@@ -727,7 +778,7 @@ describe('useMinimap', () => {
await minimap.init()
mockCanvas.ds.scale = 0.1
moduleMockCanvas.ds.scale = 0.1
const wheelEvent = new WheelEvent('wheel', {
deltaY: 100,
@@ -742,7 +793,7 @@ describe('useMinimap', () => {
minimap.handleWheel(wheelEvent)
expect(mockCanvas.ds.scale).toBe(0.1)
expect(moduleMockCanvas.ds.scale).toBe(0.1)
})
it('should update container rect if needed', async () => {
@@ -790,15 +841,17 @@ describe('useMinimap', () => {
await minimap.init()
mockCanvas.canvas.clientWidth = 1200
mockCanvas.canvas.clientHeight = 900
moduleMockCanvas.canvas.clientWidth = 1200
moduleMockCanvas.canvas.clientHeight = 900
const resizeHandler = (window.addEventListener as any).mock.calls.find(
(call: any) => call[0] === 'resize'
)?.[1]
const resizeHandler = vi
.mocked(window.addEventListener)
.mock.calls.find((call) => call[0] === 'resize')?.[1] as
| EventListener
| undefined
if (resizeHandler) {
resizeHandler()
resizeHandler(new Event('resize'))
}
await nextTick()
@@ -817,12 +870,13 @@ describe('useMinimap', () => {
id: 'node3',
pos: [300, 200],
size: [100, 100],
constructor: { color: '#666' }
constructor: { color: '#666' },
outputs: []
}
mockGraph._nodes.push(newNode)
if (mockGraph.onNodeAdded) {
mockGraph.onNodeAdded(newNode)
moduleMockGraph._nodes.push(newNode)
if (moduleMockGraph.onNodeAdded) {
moduleMockGraph.onNodeAdded(newNode)
}
await new Promise((resolve) => setTimeout(resolve, 600))
@@ -833,11 +887,11 @@ describe('useMinimap', () => {
await minimap.init()
const removedNode = mockGraph._nodes[0]
mockGraph._nodes.splice(0, 1)
const removedNode = moduleMockGraph._nodes[0]
moduleMockGraph._nodes.splice(0, 1)
if (mockGraph.onNodeRemoved) {
mockGraph.onNodeRemoved(removedNode)
if (moduleMockGraph.onNodeRemoved) {
moduleMockGraph.onNodeRemoved(removedNode)
}
await new Promise((resolve) => setTimeout(resolve, 600))
@@ -848,8 +902,8 @@ describe('useMinimap', () => {
await minimap.init()
if (mockGraph.onConnectionChange) {
mockGraph.onConnectionChange(mockGraph._nodes[0])
if (moduleMockGraph.onConnectionChange) {
moduleMockGraph.onConnectionChange(moduleMockGraph._nodes[0])
}
await new Promise((resolve) => setTimeout(resolve, 600))
@@ -870,7 +924,7 @@ describe('useMinimap', () => {
describe('edge cases', () => {
it('should handle missing node outputs', async () => {
mockGraph._nodes[0].outputs = null
moduleMockGraph._nodes[0].outputs = null
const minimap = await createAndInitializeMinimap()
await expect(minimap.init()).resolves.not.toThrow()
@@ -878,8 +932,8 @@ describe('useMinimap', () => {
})
it('should handle invalid link references', async () => {
mockGraph.links.link1.target_id = 'invalid-node'
mockGraph.getNodeById.mockReturnValue(null)
moduleMockGraph.links.link1.target_id = 'invalid-node'
moduleMockGraph.getNodeById.mockReturnValue(null)
const minimap = await createAndInitializeMinimap()
@@ -898,16 +952,13 @@ describe('useMinimap', () => {
})
it('should handle nodes without color', async () => {
mockGraph._nodes[0].color = undefined
moduleMockGraph._nodes[0].color = undefined
const minimap = await createAndInitializeMinimap()
await minimap.init()
const renderMinimap = (minimap as any).renderMinimap
if (renderMinimap) {
renderMinimap()
}
minimap.renderMinimap()
expect(mockContext2D.fillRect).toHaveBeenCalled()
expect(mockContext2D.fillStyle).toBeDefined()

View File

@@ -1,10 +1,17 @@
import { useThrottleFn } from '@vueuse/core'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { 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'
import {
createMockLGraph,
createMockLGraphNode,
createMockLLink,
createMockLinks
} from '@/utils/__tests__/litegraphTestUtils'
vi.mock('@vueuse/core', () => ({
useThrottleFn: vi.fn((fn) => fn)
@@ -24,23 +31,23 @@ describe('useMinimapGraph', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGraph = {
mockGraph = createMockLGraph({
id: 'test-graph-123',
_nodes: [
{ id: '1', pos: [100, 100], size: [150, 80] },
{ id: '2', pos: [300, 200], size: [120, 60] }
createMockLGraphNode({ id: '1', pos: [100, 100], size: [150, 80] }),
createMockLGraphNode({ id: '2', pos: [300, 200], size: [120, 60] })
],
links: { link1: { id: 'link1' } },
links: createMockLinks([createMockLLink({ id: 1 })]),
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 graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
expect(graphManager.updateFlags.value).toEqual({
@@ -52,7 +59,7 @@ describe('useMinimapGraph', () => {
})
it('should setup event listeners on init', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.init()
@@ -72,7 +79,7 @@ describe('useMinimapGraph', () => {
mockGraph.onNodeRemoved = originalOnNodeRemoved
mockGraph.onConnectionChange = originalOnConnectionChange
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.setupEventListeners()
@@ -91,7 +98,7 @@ describe('useMinimapGraph', () => {
})
it('should prevent duplicate event listener setup', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
// Store original callbacks for comparison
@@ -127,7 +134,7 @@ describe('useMinimapGraph', () => {
mockGraph.onNodeRemoved = originalOnNodeRemoved
mockGraph.onConnectionChange = originalOnConnectionChange
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.setupEventListeners()
@@ -144,7 +151,7 @@ describe('useMinimapGraph', () => {
.spyOn(console, 'error')
.mockImplementation(() => {})
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.cleanupEventListeners()
@@ -157,7 +164,7 @@ describe('useMinimapGraph', () => {
})
it('should detect node position changes', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
// First check - cache initial state
@@ -177,14 +184,18 @@ describe('useMinimapGraph', () => {
})
it('should detect node count changes', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
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)
mockGraph._nodes.push({
id: '3',
pos: [400, 300],
size: [100, 50]
} as Partial<LGraphNode> as LGraphNode)
const hasChanges = graphManager.checkForChanges()
expect(hasChanges).toBe(true)
@@ -193,17 +204,17 @@ describe('useMinimapGraph', () => {
})
it('should detect connection changes', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
// Cache initial state
graphManager.checkForChanges()
// Change connections
mockGraph.links = new Map([
[1, { id: 1 }],
[2, { id: 2 }]
]) as any
mockGraph.links = createMockLinks([
createMockLLink({ id: 1 }),
createMockLLink({ id: 2 })
])
const hasChanges = graphManager.checkForChanges()
expect(hasChanges).toBe(true)
@@ -214,7 +225,7 @@ describe('useMinimapGraph', () => {
const originalOnNodeRemoved = vi.fn()
mockGraph.onNodeRemoved = originalOnNodeRemoved
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.setupEventListeners()
@@ -227,7 +238,7 @@ describe('useMinimapGraph', () => {
})
it('should destroy properly', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.init()
@@ -241,7 +252,7 @@ describe('useMinimapGraph', () => {
})
it('should clear cache', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
// Populate cache
@@ -256,7 +267,7 @@ describe('useMinimapGraph', () => {
})
it('should handle null graph gracefully', () => {
const graphRef = ref(null as any)
const graphRef = ref(null) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
expect(() => graphManager.setupEventListeners()).not.toThrow()
@@ -265,7 +276,7 @@ describe('useMinimapGraph', () => {
})
it('should clean up removed nodes from cache', () => {
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
// Cache initial state
@@ -283,7 +294,7 @@ describe('useMinimapGraph', () => {
const throttledFn = vi.fn()
vi.mocked(useThrottleFn).mockReturnValue(throttledFn)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const graphManager = useMinimapGraph(graphRef, onGraphChangedMock)
graphManager.setupEventListeners()

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { Ref } from 'vue'
import { useMinimapInteraction } from '@/renderer/extensions/minimap/composables/useMinimapInteraction'
import type { MinimapCanvas } from '@/renderer/extensions/minimap/types'
@@ -19,7 +20,7 @@ describe('useMinimapInteraction', () => {
width: 250,
height: 200
})
} as any
} as Partial<HTMLDivElement> as HTMLDivElement
mockCanvas = {
ds: {
@@ -27,7 +28,7 @@ describe('useMinimapInteraction', () => {
offset: [0, 0]
},
setDirty: vi.fn()
} as any
} as Partial<MinimapCanvas> as MinimapCanvas
centerViewOnMock = vi.fn<(worldX: number, worldY: number) => void>()
})
@@ -36,7 +37,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -61,7 +62,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -89,7 +90,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -117,7 +118,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -163,7 +164,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -189,7 +190,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -220,7 +221,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -250,7 +251,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -283,7 +284,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,
@@ -306,7 +307,7 @@ describe('useMinimapInteraction', () => {
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 canvasRef = ref(null) as Ref<MinimapCanvas | null>
const interaction = useMinimapInteraction(
containerRef,

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref, shallowRef } from 'vue'
import type { Ref } from 'vue'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useMinimapRenderer } from '@/renderer/extensions/minimap/composables/useMinimapRenderer'
@@ -20,20 +21,20 @@ describe('useMinimapRenderer', () => {
mockContext = {
clearRect: vi.fn()
} as any
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
mockCanvas = {
getContext: vi.fn().mockReturnValue(mockContext)
} as any
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
mockGraph = {
_nodes: [{ id: '1', pos: [0, 0], size: [100, 100] }]
} as any
} as Partial<LGraph> as LGraph
})
it('should initialize with full redraw needed', () => {
const canvasRef = shallowRef<HTMLCanvasElement | null>(mockCanvas)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({
@@ -66,9 +67,9 @@ describe('useMinimapRenderer', () => {
})
it('should handle empty graph with fast path', () => {
const emptyGraph = { _nodes: [] } as any
const emptyGraph = { _nodes: [] } as Partial<LGraph> as LGraph
const canvasRef = ref(mockCanvas)
const graphRef = ref(emptyGraph)
const graphRef = ref(emptyGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({
@@ -106,7 +107,7 @@ describe('useMinimapRenderer', () => {
const { renderMinimapToCanvas } =
await import('@/renderer/extensions/minimap/minimapCanvasRenderer')
const canvasRef = ref(mockCanvas)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({
@@ -153,7 +154,7 @@ describe('useMinimapRenderer', () => {
const updateViewport = vi.fn()
const canvasRef = ref(mockCanvas)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({
@@ -192,7 +193,7 @@ describe('useMinimapRenderer', () => {
it('should force full redraw when requested', () => {
const canvasRef = ref(mockCanvas)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({
@@ -231,7 +232,7 @@ describe('useMinimapRenderer', () => {
it('should handle null canvas gracefully', () => {
const canvasRef = shallowRef<HTMLCanvasElement | null>(null)
const graphRef = ref(mockGraph as any)
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 })
const scaleRef = ref(1)
const updateFlagsRef = ref<UpdateFlags>({

View File

@@ -3,10 +3,15 @@ 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'
type MockSettingStore = ReturnType<typeof useSettingStore>
const mockUseColorPaletteStore = vi.hoisted(() => vi.fn())
vi.mock('@/platform/settings/settingStore')
vi.mock('@/stores/workspace/colorPaletteStore')
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
useColorPaletteStore: mockUseColorPaletteStore
}))
describe('useMinimapSettings', () => {
beforeEach(() => {
@@ -17,7 +22,7 @@ describe('useMinimapSettings', () => {
it('should return all minimap settings as computed refs', () => {
const mockSettingStore = {
get: vi.fn((key: string) => {
const settings: Record<string, any> = {
const settings: Record<string, unknown> = {
'Comfy.Minimap.NodeColors': true,
'Comfy.Minimap.ShowLinks': false,
'Comfy.Minimap.ShowGroups': true,
@@ -28,10 +33,17 @@ describe('useMinimapSettings', () => {
})
}
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
vi.mocked(useColorPaletteStore).mockReturnValue({
completedActivePalette: { light_theme: false }
} as any)
vi.mocked(useSettingStore).mockReturnValue(
mockSettingStore as Partial<MockSettingStore> as MockSettingStore
)
mockUseColorPaletteStore.mockReturnValue({
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: false
}
})
const settings = useMinimapSettings()
@@ -44,13 +56,18 @@ describe('useMinimapSettings', () => {
it('should generate container styles based on theme', () => {
const mockColorPaletteStore = {
completedActivePalette: { light_theme: false }
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: false
}
}
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
vi.mocked(useColorPaletteStore).mockReturnValue(
mockColorPaletteStore as any
)
vi.mocked(useSettingStore).mockReturnValue({
get: vi.fn()
} as Partial<MockSettingStore> as MockSettingStore)
mockUseColorPaletteStore.mockReturnValue(mockColorPaletteStore)
const settings = useMinimapSettings()
const styles = settings.containerStyles.value
@@ -63,13 +80,18 @@ describe('useMinimapSettings', () => {
it('should generate light theme container styles', () => {
const mockColorPaletteStore = {
completedActivePalette: { light_theme: true }
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: true
}
}
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
vi.mocked(useColorPaletteStore).mockReturnValue(
mockColorPaletteStore as any
)
vi.mocked(useSettingStore).mockReturnValue({
get: vi.fn()
} as Partial<MockSettingStore> as MockSettingStore)
mockUseColorPaletteStore.mockReturnValue(mockColorPaletteStore)
const settings = useMinimapSettings()
const styles = settings.containerStyles.value
@@ -82,13 +104,18 @@ describe('useMinimapSettings', () => {
it('should generate panel styles based on theme', () => {
const mockColorPaletteStore = {
completedActivePalette: { light_theme: false }
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: false
}
}
vi.mocked(useSettingStore).mockReturnValue({ get: vi.fn() } as any)
vi.mocked(useColorPaletteStore).mockReturnValue(
mockColorPaletteStore as any
)
vi.mocked(useSettingStore).mockReturnValue({
get: vi.fn()
} as Partial<MockSettingStore> as MockSettingStore)
mockUseColorPaletteStore.mockReturnValue(mockColorPaletteStore)
const settings = useMinimapSettings()
const styles = settings.panelStyles.value
@@ -107,10 +134,17 @@ describe('useMinimapSettings', () => {
})
const mockSettingStore = { get: mockGet }
vi.mocked(useSettingStore).mockReturnValue(mockSettingStore as any)
vi.mocked(useColorPaletteStore).mockReturnValue({
completedActivePalette: { light_theme: false }
} as any)
vi.mocked(useSettingStore).mockReturnValue(
mockSettingStore as Partial<MockSettingStore> as MockSettingStore
)
mockUseColorPaletteStore.mockReturnValue({
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: false
}
})
const settings = useMinimapSettings()

View File

@@ -1,6 +1,7 @@
import { useRafFn } from '@vueuse/core'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useMinimapViewport } from '@/renderer/extensions/minimap/composables/useMinimapViewport'
@@ -39,7 +40,7 @@ describe('useMinimapViewport', () => {
{ pos: [100, 100], size: [150, 80] },
{ pos: [300, 200], size: [120, 60] }
]
} as any
} as Partial<LGraph> as LGraph
vi.mocked(useRafFn, { partial: true }).mockReturnValue({
resume: vi.fn(),
@@ -48,8 +49,8 @@ describe('useMinimapViewport', () => {
})
it('should initialize with default bounds', () => {
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -80,8 +81,8 @@ describe('useMinimapViewport', () => {
vi.mocked(enforceMinimumBounds).mockImplementation((bounds) => bounds)
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -97,8 +98,10 @@ describe('useMinimapViewport', () => {
vi.mocked(calculateNodeBounds).mockReturnValue(null)
const canvasRef = ref(mockCanvas as any)
const graphRef = ref({ _nodes: [] } as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref({
_nodes: []
} as Partial<LGraph> as LGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -115,8 +118,8 @@ describe('useMinimapViewport', () => {
})
it('should update canvas dimensions', () => {
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -145,8 +148,8 @@ describe('useMinimapViewport', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -181,8 +184,8 @@ describe('useMinimapViewport', () => {
})
it('should center view on world coordinates', () => {
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -209,8 +212,8 @@ describe('useMinimapViewport', () => {
pause: stopSyncMock
})
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -222,8 +225,8 @@ describe('useMinimapViewport', () => {
})
it('should handle null canvas gracefully', () => {
const canvasRef = ref(null as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(null) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -250,8 +253,8 @@ describe('useMinimapViewport', () => {
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 canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)
@@ -268,8 +271,8 @@ describe('useMinimapViewport', () => {
configurable: true
})
const canvasRef = ref(mockCanvas as any)
const graphRef = ref(mockGraph as any)
const canvasRef = ref(mockCanvas) as Ref<MinimapCanvas | null>
const graphRef = ref(mockGraph) as Ref<LGraph | null>
const viewport = useMinimapViewport(canvasRef, graphRef, 250, 200)

View File

@@ -1,13 +1,23 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
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'
import {
createMockLGraph,
createMockLGraphNode,
createMockLinks,
createMockLLink,
createMockNodeOutputSlot
} from '@/utils/__tests__/litegraphTestUtils'
const mockUseColorPaletteStore = vi.hoisted(() => vi.fn())
vi.mock('@/stores/workspace/colorPaletteStore', () => ({
useColorPaletteStore: mockUseColorPaletteStore
}))
vi.mock('@/stores/workspace/colorPaletteStore')
vi.mock('@/utils/colorUtil', () => ({
adjustColor: vi.fn((color: string) => color + '_adjusted')
}))
@@ -33,15 +43,15 @@ describe('minimapCanvasRenderer', () => {
fillStyle: '',
strokeStyle: '',
lineWidth: 1
} as any
} as Partial<CanvasRenderingContext2D> as CanvasRenderingContext2D
mockCanvas = {
getContext: vi.fn().mockReturnValue(mockContext)
} as any
} as Partial<HTMLCanvasElement> as HTMLCanvasElement
mockGraph = {
mockGraph = createMockLGraph({
_nodes: [
{
createMockLGraphNode({
id: '1',
pos: [100, 100],
size: [150, 80],
@@ -49,8 +59,8 @@ describe('minimapCanvasRenderer', () => {
mode: LGraphEventMode.ALWAYS,
has_errors: false,
outputs: []
},
{
}),
createMockLGraphNode({
id: '2',
pos: [300, 200],
size: [120, 60],
@@ -58,16 +68,21 @@ describe('minimapCanvasRenderer', () => {
mode: LGraphEventMode.BYPASS,
has_errors: true,
outputs: []
}
] as unknown as LGraphNode[],
})
],
_groups: [],
links: {},
links: {} as typeof mockGraph.links,
getNodeById: vi.fn()
} as any
})
vi.mocked(useColorPaletteStore).mockReturnValue({
completedActivePalette: { light_theme: false }
} as any)
mockUseColorPaletteStore.mockReturnValue({
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: false
}
})
})
it('should clear canvas and render nodes', () => {
@@ -203,7 +218,9 @@ describe('minimapCanvasRenderer', () => {
size: [400, 300],
color: '#0000FF'
}
] as any
] as Partial<
(typeof mockGraph._groups)[number]
>[] as typeof mockGraph._groups
const context: MinimapRenderContext = {
bounds: { minX: 0, minY: 0, width: 500, height: 400 },
@@ -233,17 +250,17 @@ describe('minimapCanvasRenderer', () => {
}
mockGraph._nodes[0].outputs = [
{
links: [1]
}
] as any
createMockNodeOutputSlot({
name: 'output',
type: 'number',
links: [1],
boundingRect: new Float64Array([0, 0, 10, 10])
})
]
// 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.links = createMockLinks([
createMockLLink({ id: 1, target_id: 2, origin_slot: 0, target_slot: 0 })
])
mockGraph.getNodeById = vi.fn().mockReturnValue(targetNode)
@@ -275,9 +292,14 @@ describe('minimapCanvasRenderer', () => {
})
it('should handle light theme colors', () => {
vi.mocked(useColorPaletteStore).mockReturnValue({
completedActivePalette: { light_theme: true }
} as any)
mockUseColorPaletteStore.mockReturnValue({
completedActivePalette: {
id: 'test',
name: 'Test Palette',
colors: {},
light_theme: true
}
})
const context: MinimapRenderContext = {
bounds: { minX: 0, minY: 0, width: 500, height: 400 },

View File

@@ -9,10 +9,12 @@ import type {
LGraph,
LGraphCanvas,
LGraphGroup,
LinkNetwork
LinkNetwork,
LLink
} from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { vi } from 'vitest'
import type { ChangeTracker } from '@/scripts/changeTracker'
/**
* Creates a mock LGraphNode with minimal required properties
@@ -203,3 +205,99 @@ export function createMockFileList(files: File[]): FileList {
)
return fileList as FileList
}
/**
* Creates a mock ChangeTracker for workflow testing
* The ChangeTracker requires a proper ComfyWorkflowJSON structure
*/
export function createMockChangeTracker(
overrides: Record<string, unknown> = {}
): ChangeTracker {
const partial = {
activeState: {
last_node_id: 0,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
version: 0.4
},
undoQueue: [],
redoQueue: [],
changeCount: 0,
reset: vi.fn(),
...overrides
}
return partial as Partial<ChangeTracker> as ChangeTracker
}
/**
* Creates a mock MinimapCanvas for minimap testing
*/
export function createMockMinimapCanvas(
overrides: Partial<HTMLCanvasElement> = {}
): HTMLCanvasElement {
const mockGetContext = vi.fn()
mockGetContext.mockImplementation((contextId: string) =>
contextId === '2d' ? createMockCanvas2DContext() : null
)
const partial: Partial<HTMLCanvasElement> = {
width: 200,
height: 200,
clientWidth: 200,
clientHeight: 200,
getContext: mockGetContext as HTMLCanvasElement['getContext'],
...overrides
}
return partial as HTMLCanvasElement
}
/**
* Creates a mock CanvasRenderingContext2D for canvas testing
*/
export function createMockCanvas2DContext(
overrides: Partial<CanvasRenderingContext2D> = {}
): CanvasRenderingContext2D {
const partial: Partial<CanvasRenderingContext2D> = {
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,
...overrides
}
return partial as CanvasRenderingContext2D
}
export function createMockLLink(overrides: Partial<LLink> = {}): LLink {
const partial: Partial<LLink> = {
id: 1,
type: '*',
origin_id: 1,
origin_slot: 0,
target_id: 2,
target_slot: 0,
_pos: [0, 0],
...overrides
}
return partial as LLink
}
export function createMockLinks(links: LLink[]): LGraph['links'] {
const map = new Map<number, LLink>()
const record: Record<number, LLink> = {}
for (const link of links) {
map.set(link.id, link)
record[link.id] = link
}
return Object.assign(map, record) as LGraph['links']
}