Road to No explicit any: Group 8 (part 7) test files (#8459)

## Summary

This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.

### Key Changes

#### Type Safety Improvements
- Removed improper `as unknown as Type` patterns from 17 test files in
Group 8 part 7
- Replaced with proper TypeScript patterns using factory functions and
Mock types
- Fixed createTestingPinia usage in test files (was incorrectly using
createPinia)
- Fixed vi.hoisted pattern for mockSetDirty in viewport tests  
- Fixed vi.doMock lint issues with vi.mock and vi.hoisted pattern
- Retained necessary `as unknown as` casts only for complex mock objects
where direct type assertions would fail

### Files Changed

Test files (Group 8 part 7 - services, stores, utils):
- src/services/nodeOrganizationService.test.ts
- src/services/providers/algoliaSearchProvider.test.ts
- src/services/providers/registrySearchProvider.test.ts
- src/stores/comfyRegistryStore.test.ts
- src/stores/domWidgetStore.test.ts
- src/stores/executionStore.test.ts
- src/stores/firebaseAuthStore.test.ts
- src/stores/modelToNodeStore.test.ts
- src/stores/queueStore.test.ts
- src/stores/subgraphNavigationStore.test.ts
- src/stores/subgraphNavigationStore.viewport.test.ts
- src/stores/subgraphStore.test.ts
- src/stores/systemStatsStore.test.ts
- src/stores/workspace/nodeHelpStore.test.ts
- src/utils/colorUtil.test.ts
- src/utils/executableGroupNodeChildDTO.test.ts

Source files:
- src/stores/modelStore.ts - Improved type handling

### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- All affected test files pass (`pnpm test:unit`)
- Linting passes without errors (`pnpm lint`)
- Code formatting applied (`pnpm format`)

Part of the "Road to No Explicit Any" initiative, cleaning up type
casting issues from branch `fix/remove-any-types-part8`.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459 (this PR)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8459-Road-to-No-explicit-any-Group-8-part-7-test-files-2f86d73d36508114ad28d82e72a3a5e9)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2026-01-30 03:38:06 +01:00
committed by GitHub
parent 067d80c4ed
commit 13311a46ea
17 changed files with 247 additions and 150 deletions

View File

@@ -5,7 +5,7 @@ import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { NodeSourceType } from '@/types/nodeSource'
describe('nodeOrganizationService', () => {
const createMockNodeDef = (overrides: any = {}) => {
const createMockNodeDef = (overrides: Partial<ComfyNodeDefImpl> = {}) => {
const mockNodeDef = {
name: 'TestNode',
display_name: 'Test Node',
@@ -273,7 +273,7 @@ describe('nodeOrganizationService', () => {
it('should handle unknown source type', () => {
const nodeDef = createMockNodeDef({
nodeSource: {
type: 'unknown' as any,
type: 'unknown' as NodeSourceType,
className: 'unknown',
displayText: 'Unknown',
badgeText: '?'

View File

@@ -1,20 +1,34 @@
import type { Mock } from 'vitest'
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { components } from '@/types/comfyRegistryTypes'
import { useAlgoliaSearchProvider } from '@/services/providers/algoliaSearchProvider'
import { SortableAlgoliaField } from '@/workbench/extensions/manager/types/comfyManagerTypes'
type RegistryNodePack = components['schemas']['Node']
type GlobalWithAlgolia = typeof globalThis & {
__ALGOLIA_APP_ID__: string
__ALGOLIA_API_KEY__: string
}
// Mock global Algolia constants
;(global as any).__ALGOLIA_APP_ID__ = 'test-app-id'
;(global as any).__ALGOLIA_API_KEY__ = 'test-api-key'
const globalWithAlgolia = globalThis as GlobalWithAlgolia
globalWithAlgolia.__ALGOLIA_APP_ID__ = 'test-app-id'
globalWithAlgolia.__ALGOLIA_API_KEY__ = 'test-api-key'
// Mock algoliasearch
vi.mock('algoliasearch/dist/lite/builds/browser', () => ({
liteClient: vi.fn()
}))
interface MockSearchClient {
search: Mock
}
describe('useAlgoliaSearchProvider', () => {
let mockSearchClient: any
let mockSearchClient: MockSearchClient
beforeEach(() => {
vi.clearAllMocks()
@@ -24,7 +38,11 @@ describe('useAlgoliaSearchProvider', () => {
search: vi.fn()
}
vi.mocked(algoliasearch).mockReturnValue(mockSearchClient)
vi.mocked(algoliasearch).mockReturnValue(
mockSearchClient as Partial<
ReturnType<typeof algoliasearch>
> as ReturnType<typeof algoliasearch>
)
})
afterEach(() => {
@@ -252,7 +270,7 @@ describe('useAlgoliaSearchProvider', () => {
})
describe('getSortValue', () => {
const testPack = {
const testPack: Partial<RegistryNodePack> = {
id: '1',
name: 'Test Pack',
downloads: 100,
@@ -279,7 +297,10 @@ describe('useAlgoliaSearchProvider', () => {
const createdTimestamp = new Date('2024-01-01T10:00:00Z').getTime()
expect(
provider.getSortValue(testPack as any, SortableAlgoliaField.Created)
provider.getSortValue(
testPack as RegistryNodePack,
SortableAlgoliaField.Created
)
).toBe(createdTimestamp)
const updatedTimestamp = new Date('2024-01-15T10:00:00Z').getTime()
@@ -289,23 +310,35 @@ describe('useAlgoliaSearchProvider', () => {
})
it('should handle missing values', () => {
const incompletePack = { id: '1', name: 'Incomplete' }
const incompletePack: Partial<RegistryNodePack> = {
id: '1',
name: 'Incomplete'
}
const provider = useAlgoliaSearchProvider()
expect(
provider.getSortValue(incompletePack, SortableAlgoliaField.Downloads)
provider.getSortValue(
incompletePack as RegistryNodePack,
SortableAlgoliaField.Downloads
)
).toBe(0)
expect(
provider.getSortValue(incompletePack, SortableAlgoliaField.Publisher)
provider.getSortValue(
incompletePack as RegistryNodePack,
SortableAlgoliaField.Publisher
)
).toBe('')
expect(
provider.getSortValue(
incompletePack as any,
incompletePack as RegistryNodePack,
SortableAlgoliaField.Created
)
).toBe(0)
expect(
provider.getSortValue(incompletePack, SortableAlgoliaField.Updated)
provider.getSortValue(
incompletePack as RegistryNodePack,
SortableAlgoliaField.Updated
)
).toBe(0)
})
})

View File

@@ -14,20 +14,31 @@ describe('useComfyRegistrySearchProvider', () => {
const mockListAllPacksCall = vi.fn()
const mockListAllPacksClear = vi.fn()
const createMockStore = (
params: Partial<ReturnType<typeof useComfyRegistryStore>> = {}
) => {
return {
search: {
call: mockSearchCall,
clear: mockSearchClear,
cancel: vi.fn()
},
listAllPacks: {
call: mockListAllPacksCall,
clear: mockListAllPacksClear,
cancel: vi.fn()
},
...params
} as Partial<ReturnType<typeof useComfyRegistryStore>> as ReturnType<
typeof useComfyRegistryStore
>
}
beforeEach(() => {
vi.clearAllMocks()
// Setup store mock
vi.mocked(useComfyRegistryStore).mockReturnValue({
search: {
call: mockSearchCall,
clear: mockSearchClear
},
listAllPacks: {
call: mockListAllPacksCall,
clear: mockListAllPacksClear
}
} as any)
vi.mocked(useComfyRegistryStore).mockReturnValue(createMockStore())
})
describe('searchPacks', () => {

View File

@@ -127,7 +127,9 @@ describe('useComfyRegistryStore', () => {
}
vi.mocked(useComfyRegistryService).mockReturnValue(
mockRegistryService as any
mockRegistryService as Partial<
ReturnType<typeof useComfyRegistryService>
> as ReturnType<typeof useComfyRegistryService>
)
})
@@ -177,7 +179,7 @@ describe('useComfyRegistryStore', () => {
const store = useComfyRegistryStore()
vi.spyOn(store.getPackById, 'call').mockResolvedValueOnce(null)
const result = await store.getPackById.call(null as any)
const result = await store.getPackById.call(null!)
expect(result).toBeNull()
expect(mockRegistryService.getPackById).not.toHaveBeenCalled()

View File

@@ -1,8 +1,9 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { createTestingPinia } from '@pinia/testing'
// Mock DOM widget for testing
const createMockDOMWidget = (id: string) => {
@@ -15,7 +16,7 @@ const createMockDOMWidget = (id: string) => {
title: 'Test Node',
pos: [0, 0],
size: [200, 100]
} as any,
} as Partial<LGraphNode> as LGraphNode,
name: 'test_widget',
type: 'text',
value: 'test',
@@ -23,7 +24,7 @@ const createMockDOMWidget = (id: string) => {
y: 0,
margin: 10,
isVisible: () => true,
containerNode: undefined as any
containerNode: undefined
}
}

View File

@@ -1,7 +1,5 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
@@ -11,6 +9,8 @@ const mockNodeIdToNodeLocatorId = vi.fn()
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
import { createTestingPinia } from '@pinia/testing'
// Mock the workflowStore
vi.mock('@/platform/workflow/management/stores/workflowStore', async () => {
@@ -72,12 +72,11 @@ describe('useExecutionStore - NodeLocatorId conversions', () => {
nodes: []
}
const mockNode = {
const mockNode = createMockLGraphNode({
id: 123,
isSubgraphNode: () => true,
subgraph: mockSubgraph
} as any
})
// Mock app.rootGraph.getNodeById to return the mock node
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode)
@@ -178,11 +177,11 @@ describe('useExecutionStore - Node Error Lookups', () => {
nodes: []
}
const mockNode = {
const mockNode = createMockLGraphNode({
id: 123,
isSubgraphNode: () => true,
subgraph: mockSubgraph
} as any
})
vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode)

View File

@@ -1,12 +1,28 @@
import { FirebaseError } from 'firebase/app'
import type { User, UserCredential } from 'firebase/auth'
import * as firebaseAuth from 'firebase/auth'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import type { Mock } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as vuefire from 'vuefire'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { createTestingPinia } from '@pinia/testing'
// Hoisted mocks for dynamic imports
const { mockDistributionTypes } = vi.hoisted(() => ({
mockDistributionTypes: {
isCloud: true,
isDesktop: true
}
}))
type MockUser = Omit<User, 'getIdToken'> & {
getIdToken: Mock
}
type MockAuth = Record<string, unknown>
// Mock fetch
const mockFetch = vi.fn()
@@ -83,35 +99,20 @@ vi.mock('@/stores/toastStore', () => ({
// Mock useDialogService
vi.mock('@/services/dialogService')
const mockDistributionTypes = vi.hoisted(() => ({
isCloud: false,
isDesktop: false
}))
vi.mock('@/platform/distribution/types', () => mockDistributionTypes)
const mockApiKeyStore = vi.hoisted(() => ({
getAuthHeader: vi.fn().mockReturnValue(null)
}))
vi.mock('@/stores/apiKeyAuthStore', () => ({
useApiKeyAuthStore: () => mockApiKeyStore
}))
describe('useFirebaseAuthStore', () => {
let store: ReturnType<typeof useFirebaseAuthStore>
let authStateCallback: (user: any) => void
let idTokenCallback: (user: any) => void
let authStateCallback: (user: User | null) => void
let idTokenCallback: (user: User | null) => void
const mockAuth = {
const mockAuth: MockAuth = {
/* mock Auth object */
}
const mockUser = {
const mockUser: MockUser = {
uid: 'test-user-id',
email: 'test@example.com',
getIdToken: vi.fn().mockResolvedValue('mock-id-token')
}
} as Partial<User> as MockUser
beforeEach(() => {
vi.resetAllMocks()
@@ -123,14 +124,18 @@ describe('useFirebaseAuthStore', () => {
})
// Mock useFirebaseAuth to return our mock auth object
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(
mockAuth as Partial<
ReturnType<typeof vuefire.useFirebaseAuth>
> as ReturnType<typeof vuefire.useFirebaseAuth>
)
// Mock onAuthStateChanged to capture the callback and simulate initial auth state
vi.mocked(firebaseAuth.onAuthStateChanged).mockImplementation(
(_, callback) => {
authStateCallback = callback as (user: any) => void
authStateCallback = callback as (user: User | null) => void
// Call the callback with our mock user
;(callback as (user: any) => void)(mockUser)
;(callback as (user: User | null) => void)(mockUser)
// Return an unsubscribe function
return vi.fn()
}
@@ -163,21 +168,26 @@ describe('useFirebaseAuthStore', () => {
})
describe('token refresh events', () => {
beforeEach(() => {
mockDistributionTypes.isCloud = true
mockDistributionTypes.isDesktop = true
beforeEach(async () => {
vi.resetModules()
vi.mock('@/platform/distribution/types', () => mockDistributionTypes)
vi.mocked(firebaseAuth.onIdTokenChanged).mockImplementation(
(_auth, callback) => {
idTokenCallback = callback as (user: any) => void
idTokenCallback = callback as (user: User | null) => void
return vi.fn()
}
)
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(
mockAuth as Partial<
ReturnType<typeof vuefire.useFirebaseAuth>
> as ReturnType<typeof vuefire.useFirebaseAuth>
)
setActivePinia(createTestingPinia({ stubActions: false }))
store = useFirebaseAuthStore()
const storeModule = await import('@/stores/firebaseAuthStore')
store = storeModule.useFirebaseAuthStore()
})
it("should not increment tokenRefreshTrigger on the user's first ID token event", () => {
@@ -192,14 +202,14 @@ describe('useFirebaseAuthStore', () => {
})
it('should not increment when ID token event is for a different user UID', () => {
const otherUser = { uid: 'other-user-id' }
const otherUser = { uid: 'other-user-id' } as Partial<User> as User
idTokenCallback?.(mockUser)
idTokenCallback?.(otherUser)
expect(store.tokenRefreshTrigger).toBe(0)
})
it('should increment after switching to a new UID and receiving a second event for that UID', () => {
const otherUser = { uid: 'other-user-id' }
const otherUser = { uid: 'other-user-id' } as Partial<User> as User
idTokenCallback?.(mockUser)
idTokenCallback?.(otherUser)
idTokenCallback?.(otherUser)
@@ -238,7 +248,7 @@ describe('useFirebaseAuthStore', () => {
// Now, succeed on next attempt
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValueOnce({
user: mockUser
} as any)
} as Partial<UserCredential> as UserCredential)
await store.login('test@example.com', 'correct-password')
})
@@ -247,7 +257,7 @@ describe('useFirebaseAuthStore', () => {
it('should login with valid credentials', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const result = await store.login('test@example.com', 'password')
@@ -283,7 +293,7 @@ describe('useFirebaseAuthStore', () => {
// Set up multiple login promises
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const loginPromise1 = store.login('user1@example.com', 'password1')
@@ -301,7 +311,7 @@ describe('useFirebaseAuthStore', () => {
it('should register a new user', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.createUserWithEmailAndPassword).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const result = await store.register('new@example.com', 'password')
@@ -378,7 +388,7 @@ describe('useFirebaseAuthStore', () => {
// Setup mock for login
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithEmailAndPassword).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
// Login
@@ -454,9 +464,6 @@ describe('useFirebaseAuthStore', () => {
// This test reproduces the issue where getAuthHeader fails due to network errors
// when Firebase Auth tries to refresh tokens offline
// Configure mockApiKeyStore to return null (no API key fallback)
mockApiKeyStore.getAuthHeader.mockReturnValue(null)
// Setup user with network error on token refresh
mockUser.getIdToken.mockReset()
const networkError = new FirebaseError(
@@ -475,7 +482,7 @@ describe('useFirebaseAuthStore', () => {
it('should sign in with Google', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const result = await store.loginWithGoogle()
@@ -508,7 +515,7 @@ describe('useFirebaseAuthStore', () => {
it('should sign in with Github', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const result = await store.loginWithGithub()
@@ -540,7 +547,7 @@ describe('useFirebaseAuthStore', () => {
it('should handle concurrent social login attempts correctly', async () => {
const mockUserCredential = { user: mockUser }
vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue(
mockUserCredential as any
mockUserCredential as Partial<UserCredential> as UserCredential
)
const googleLoginPromise = store.loginWithGoogle()

View File

@@ -7,14 +7,19 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api'
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */
function _findInMetadata(metadata: any, ...keys: string[]): string | null {
function _findInMetadata(
metadata: Record<string, string | null>,
...keys: string[]
): string | null {
for (const key of keys) {
if (key in metadata) {
return metadata[key]
const value = metadata[key]
return value || null
}
for (const k in metadata) {
if (k.endsWith(key)) {
return metadata[k]
const value = metadata[k]
return value || null
}
}
}

View File

@@ -239,7 +239,7 @@ describe('useModelToNodeStore', () => {
it('should not register provider when nodeDef is undefined', () => {
const modelToNodeStore = useModelToNodeStore()
const providerWithoutNodeDef = new ModelNodeProvider(
undefined as any,
undefined!,
'custom_key'
)
@@ -507,15 +507,11 @@ describe('useModelToNodeStore', () => {
modelToNodeStore.registerDefaults()
// These should not throw but return undefined
expect(modelToNodeStore.getCategoryForNodeType(null!)).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(null as any)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(undefined as any)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(123 as any)
modelToNodeStore.getCategoryForNodeType(undefined!)
).toBeUndefined()
expect(modelToNodeStore.getCategoryForNodeType('123')).toBeUndefined()
})
it('should be case-sensitive for node type matching', () => {

View File

@@ -38,7 +38,11 @@ function createHistoryJob(createTime: number, id: string): JobListItem {
const createTaskOutput = (
nodeId: string = 'node-1',
images: any[] = []
images: {
type?: 'output' | 'input' | 'temp'
filename?: string
subfolder?: string
}[] = []
): TaskOutput => ({
[nodeId]: {
images
@@ -490,7 +494,7 @@ describe('useQueueStore', () => {
it('should recreate TaskItemImpl when outputs_count changes', async () => {
// Initial load without outputs_count
const jobWithoutOutputsCount = createHistoryJob(10, 'job-1')
delete (jobWithoutOutputsCount as any).outputs_count
delete jobWithoutOutputsCount.outputs_count
mockGetQueue.mockResolvedValue({ Running: [], Pending: [] })
mockGetHistory.mockResolvedValue([jobWithoutOutputsCount])

View File

@@ -7,7 +7,9 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
import { createMockChangeTracker } from '@/utils/__tests__/litegraphTestUtils'
import type { Subgraph } from '@/lib/litegraph/src/LGraph'
vi.mock('@/scripts/app', () => {
const mockCanvas = {
@@ -38,7 +40,7 @@ vi.mock('@/scripts/app', () => {
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
getCanvas: () => (app as any).canvas
getCanvas: () => app.canvas
})
}))
@@ -63,7 +65,8 @@ describe('useSubgraphNavigationStore', () => {
} as ComfyWorkflow
// Set the active workflow (cast to bypass TypeScript check in test)
workflowStore.activeWorkflow = mockWorkflow as any
workflowStore.activeWorkflow =
mockWorkflow as typeof workflowStore.activeWorkflow
// Simulate being in a subgraph by restoring state
navigationStore.restoreState(['subgraph-1', 'subgraph-2'])
@@ -72,7 +75,9 @@ describe('useSubgraphNavigationStore', () => {
// Simulate a change to the workflow's internal state
// (e.g., changeTracker.activeState being reassigned)
mockWorkflow.changeTracker = { activeState: {} } as any
mockWorkflow.changeTracker = {
activeState: {}
} as typeof mockWorkflow.changeTracker
// The navigation stack should NOT be cleared because the path hasn't changed
expect(navigationStore.exportState()).toHaveLength(2)
@@ -87,14 +92,15 @@ describe('useSubgraphNavigationStore', () => {
const workflow1 = {
path: 'workflow1.json',
filename: 'workflow1.json',
changeTracker: {
changeTracker: createMockChangeTracker({
restore: vi.fn(),
store: vi.fn()
}
} as unknown as ComfyWorkflow
})
} as Partial<ComfyWorkflow> as ComfyWorkflow
// Set the active workflow
workflowStore.activeWorkflow = workflow1 as any
workflowStore.activeWorkflow =
workflow1 as typeof workflowStore.activeWorkflow
// Simulate the restore process that happens when loading a workflow
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation
@@ -108,13 +114,14 @@ describe('useSubgraphNavigationStore', () => {
const workflow2 = {
path: 'workflow2.json',
filename: 'workflow2.json',
changeTracker: {
changeTracker: createMockChangeTracker({
restore: vi.fn(),
store: vi.fn()
}
} as unknown as ComfyWorkflow
})
} as Partial<ComfyWorkflow> as ComfyWorkflow
workflowStore.activeWorkflow = workflow2 as any
workflowStore.activeWorkflow =
workflow2 as typeof workflowStore.activeWorkflow
// Simulate the restore process for workflow2
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation
@@ -124,7 +131,8 @@ describe('useSubgraphNavigationStore', () => {
expect(navigationStore.exportState()).toHaveLength(0)
// Switch back to workflow1
workflowStore.activeWorkflow = workflow1 as any
workflowStore.activeWorkflow =
workflow1 as typeof workflowStore.activeWorkflow
// Simulate the restore process for workflow1 again
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation
@@ -138,17 +146,18 @@ describe('useSubgraphNavigationStore', () => {
it('should clear navigation when activeSubgraph becomes undefined', async () => {
const navigationStore = useSubgraphNavigationStore()
const workflowStore = useWorkflowStore()
const { findSubgraphPathById } = await import('@/utils/graphTraversalUtil')
// Create mock subgraph and graph structure
const mockSubgraph = {
id: 'subgraph-1',
rootGraph: (app as any).graph,
rootGraph: app.graph,
_nodes: [],
nodes: []
}
} as Partial<Subgraph> as Subgraph
// Add the subgraph to the graph's subgraphs map
;(app as any).graph.subgraphs.set('subgraph-1', mockSubgraph)
app.graph.subgraphs.set('subgraph-1', mockSubgraph)
// First set an active workflow
const mockWorkflow = {
@@ -156,13 +165,14 @@ describe('useSubgraphNavigationStore', () => {
filename: 'test-workflow.json'
} as ComfyWorkflow
workflowStore.activeWorkflow = mockWorkflow as any
workflowStore.activeWorkflow =
mockWorkflow as typeof workflowStore.activeWorkflow
// Mock findSubgraphPathById to return the correct path
vi.mocked(findSubgraphPathById).mockReturnValue(['subgraph-1'])
// Set canvas.subgraph and trigger update to set activeSubgraph
;(app as any).canvas.subgraph = mockSubgraph
app.canvas.subgraph = mockSubgraph
workflowStore.updateActiveGraph()
// Wait for Vue's reactivity to process the change
@@ -173,7 +183,7 @@ describe('useSubgraphNavigationStore', () => {
expect(navigationStore.exportState()).toEqual(['subgraph-1'])
// Clear canvas.subgraph and trigger update (simulating navigating back to root)
;(app as any).canvas.subgraph = null
app.canvas.subgraph = undefined
workflowStore.updateActiveGraph()
// Wait for Vue's reactivity to process the change

View File

@@ -3,11 +3,16 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
const { mockSetDirty } = vi.hoisted(() => ({
mockSetDirty: vi.fn()
}))
vi.mock('@/scripts/app', () => {
const mockCanvas = {
subgraph: null,
@@ -19,7 +24,7 @@ vi.mock('@/scripts/app', () => {
offset: [0, 0]
}
},
setDirty: vi.fn()
setDirty: mockSetDirty
}
return {
@@ -38,12 +43,12 @@ vi.mock('@/scripts/app', () => {
// Mock canvasStore
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({
getCanvas: () => (app as any).canvas
getCanvas: () => app.canvas
})
}))
// Get reference to mock canvas
const mockCanvas = app.canvas as any
const mockCanvas = app.canvas
describe('useSubgraphNavigationStore - Viewport Persistence', () => {
beforeEach(() => {
@@ -53,7 +58,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
mockCanvas.ds.offset = [0, 0]
mockCanvas.ds.state.scale = 1
mockCanvas.ds.state.offset = [0, 0]
mockCanvas.setDirty.mockClear()
mockSetDirty.mockClear()
})
describe('saveViewport', () => {
@@ -99,7 +104,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Mock being in a subgraph
const mockSubgraph = { id: 'sub-456' }
workflowStore.activeSubgraph = mockSubgraph as any
workflowStore.activeSubgraph = mockSubgraph as Subgraph
// Set viewport state
mockCanvas.ds.state.scale = 3
@@ -159,7 +164,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Reset canvas
mockCanvas.ds.scale = 1
mockCanvas.ds.offset = [0, 0]
mockCanvas.setDirty.mockClear()
mockSetDirty.mockClear()
// Try to restore non-existent viewport
navigationStore.restoreViewport('non-existent')
@@ -167,7 +172,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Canvas should not change
expect(mockCanvas.ds.scale).toBe(1)
expect(mockCanvas.ds.offset).toEqual([0, 0])
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
expect(mockSetDirty).not.toHaveBeenCalled()
})
})
@@ -182,7 +187,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
nodes: [],
subgraphs: new Map(),
getNodeById: vi.fn()
}
} as Partial<LGraph> as LGraph
const subgraph1 = {
id: 'sub1',
rootGraph: mockRootGraph,
@@ -195,7 +200,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
mockCanvas.ds.state.offset = [100, 100]
// Navigate to subgraph
workflowStore.activeSubgraph = subgraph1 as any
workflowStore.activeSubgraph = subgraph1 as Partial<Subgraph> as Subgraph
await nextTick()
// Root viewport should have been saved automatically
@@ -240,10 +245,14 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
const workflow1 = { path: 'workflow1.json' } as ComfyWorkflow
const workflow2 = { path: 'workflow2.json' } as ComfyWorkflow
workflowStore.activeWorkflow = workflow1 as any
workflowStore.activeWorkflow = workflow1 as ReturnType<
typeof useWorkflowStore
>['activeWorkflow']
await nextTick()
workflowStore.activeWorkflow = workflow2 as any
workflowStore.activeWorkflow = workflow2 as ReturnType<
typeof useWorkflowStore
>['activeWorkflow']
await nextTick()
// Cache should be preserved (LRU will manage memory)

View File

@@ -1,9 +1,9 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
import type { GlobalSubgraphData } from '@/scripts/api'
import type { ExportedSubgraph } from '@/lib/litegraph/src/types/serialisation'
import { api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'
import { useLitegraphService } from '@/services/litegraphService'
@@ -14,6 +14,7 @@ import {
createTestSubgraph,
createTestSubgraphNode
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
import { createTestingPinia } from '@pinia/testing'
// Mock telemetry to break circular dependency (telemetry → workflowStore → app → telemetry)
vi.mock('@/platform/telemetry', () => ({
@@ -96,10 +97,18 @@ describe('useSubgraphStore', () => {
const graph = subgraphNode.graph
graph.add(subgraphNode)
vi.mocked(comfyApp.canvas).selectedItems = new Set([subgraphNode])
vi.mocked(comfyApp.canvas)._serializeItems = vi.fn(() => ({
nodes: [subgraphNode.serialize()],
subgraphs: [subgraph.serialize() as any]
}))
vi.mocked(comfyApp.canvas)._serializeItems = vi.fn(() => {
const serializedSubgraph = {
...subgraph.serialize(),
links: [],
groups: [],
version: 1
} as Partial<ExportedSubgraph> as ExportedSubgraph
return {
nodes: [subgraphNode.serialize()],
subgraphs: [serializedSubgraph]
}
})
//mock saving of file
vi.mocked(api.storeUserData).mockResolvedValue({
status: 200,

View File

@@ -2,6 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SystemStats } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
import { isElectron } from '@/utils/envUtil'
@@ -25,7 +26,7 @@ describe('useSystemStatsStore', () => {
beforeEach(() => {
// Mock API to prevent automatic fetch on store creation
vi.mocked(api.getSystemStats).mockResolvedValue(null as any)
vi.mocked(api.getSystemStats).mockResolvedValue(null!)
setActivePinia(createTestingPinia({ stubActions: false }))
store = useSystemStatsStore()
vi.clearAllMocks()
@@ -90,8 +91,8 @@ describe('useSystemStatsStore', () => {
})
it('should set loading state correctly', async () => {
let resolvePromise: (value: any) => void = () => {}
const promise = new Promise<any>((resolve) => {
let resolvePromise: (value: SystemStats) => void = () => {}
const promise = new Promise<SystemStats>((resolve) => {
resolvePromise = resolve
})
vi.mocked(api.getSystemStats).mockReturnValue(promise)
@@ -99,7 +100,7 @@ describe('useSystemStatsStore', () => {
const fetchPromise = store.refetchSystemStats()
expect(store.isLoading).toBe(true)
resolvePromise({})
resolvePromise({} as SystemStats)
await fetchPromise
expect(store.isLoading).toBe(false)
@@ -153,7 +154,7 @@ describe('useSystemStatsStore', () => {
argv: [],
ram_total: 16000000000,
ram_free: 8000000000
} as any,
} as Partial<SystemStats['system']> as SystemStats['system'],
devices: []
}

View File

@@ -2,6 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
describe('nodeHelpStore', () => {
@@ -27,7 +28,9 @@ describe('nodeHelpStore', () => {
it('should open help for a node', () => {
const nodeHelpStore = useNodeHelpStore()
nodeHelpStore.openHelp(mockCoreNode as any)
nodeHelpStore.openHelp(
mockCoreNode as Partial<ComfyNodeDefImpl> as ComfyNodeDefImpl
)
expect(nodeHelpStore.currentHelpNode).toStrictEqual(mockCoreNode)
expect(nodeHelpStore.isHelpOpen).toBe(true)
@@ -36,7 +39,9 @@ describe('nodeHelpStore', () => {
it('should close help', () => {
const nodeHelpStore = useNodeHelpStore()
nodeHelpStore.openHelp(mockCoreNode as any)
nodeHelpStore.openHelp(
mockCoreNode as Partial<ComfyNodeDefImpl> as ComfyNodeDefImpl
)
expect(nodeHelpStore.isHelpOpen).toBe(true)
nodeHelpStore.closeHelp()

View File

@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import type { ColorAdjustOptions } from '@/utils/colorUtil'
import {
adjustColor,
hexToRgb,
@@ -22,13 +23,16 @@ interface ColorTestCase {
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
vi.mock('es-toolkit/compat', () => ({
memoize: (fn: any) => fn
memoize: <T extends (...args: unknown[]) => unknown>(fn: T) => fn
}))
const targetOpacity = 0.5
const targetLightness = 0.5
const assertColorVariationsMatch = (variations: string[], adjustment: any) => {
const assertColorVariationsMatch = (
variations: string[],
adjustment: ColorAdjustOptions
) => {
for (let i = 0; i < variations.length - 1; i++) {
expect(adjustColor(variations[i], adjustment)).toBe(
adjustColor(variations[i + 1], adjustment)

View File

@@ -7,6 +7,7 @@ import type {
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { ExecutableGroupNodeChildDTO } from '@/utils/executableGroupNodeChildDTO'
import { createMockLGraphNode } from './__tests__/litegraphTestUtils'
describe('ExecutableGroupNodeChildDTO', () => {
let mockNode: LGraphNode
@@ -16,18 +17,18 @@ describe('ExecutableGroupNodeChildDTO', () => {
beforeEach(() => {
// Create mock nodes
mockNode = {
mockNode = createMockLGraphNode({
id: '3', // Simple node ID for most tests
graph: {},
getInputNode: vi.fn(),
getInputLink: vi.fn(),
inputs: []
} as any
})
mockInputNode = {
mockInputNode = createMockLGraphNode({
id: '1',
graph: {}
} as any
})
// Create the nodesByExecutionId map
mockNodesByExecutionId = new Map()
@@ -38,7 +39,7 @@ describe('ExecutableGroupNodeChildDTO', () => {
describe('resolveInput', () => {
it('should resolve input from external node (node outside the group)', () => {
// Setup: Group node child with ID '10:3'
const groupNodeChild = {
const groupNodeChild = createMockLGraphNode({
id: '10:3',
graph: {},
getInputNode: vi.fn().mockReturnValue(mockInputNode),
@@ -46,7 +47,7 @@ describe('ExecutableGroupNodeChildDTO', () => {
origin_slot: 0
}),
inputs: []
} as any
})
// External node with ID '1'
const externalNodeDto = {
@@ -75,19 +76,19 @@ describe('ExecutableGroupNodeChildDTO', () => {
it('should resolve input from internal node (node inside the same group)', () => {
// Setup: Group node child with ID '10:3'
const groupNodeChild = {
const groupNodeChild = createMockLGraphNode({
id: '10:3',
graph: {},
getInputNode: vi.fn(),
getInputLink: vi.fn(),
inputs: []
} as any
})
// Internal node with ID '10:2'
const internalInputNode = {
const internalInputNode = createMockLGraphNode({
id: '10:2',
graph: {}
} as LGraphNode
})
const internalNodeDto = {
id: '2',
@@ -97,10 +98,10 @@ describe('ExecutableGroupNodeChildDTO', () => {
// Internal nodes are stored with just their index
mockNodesByExecutionId.set('2', internalNodeDto)
groupNodeChild.getInputNode.mockReturnValue(internalInputNode)
groupNodeChild.getInputLink.mockReturnValue({
vi.mocked(groupNodeChild.getInputNode).mockReturnValue(internalInputNode)
vi.mocked(groupNodeChild.getInputLink).mockReturnValue({
origin_slot: 1
})
} as ReturnType<LGraphNode['getInputLink']>)
const dto = new ExecutableGroupNodeChildDTO(
groupNodeChild,
@@ -172,7 +173,7 @@ describe('ExecutableGroupNodeChildDTO', () => {
it('should throw error for group nodes inside subgraphs (unsupported)', () => {
// Setup: Group node child inside a subgraph (execution ID has more than 2 segments)
const nestedGroupNode = {
const nestedGroupNode = createMockLGraphNode({
id: '1:2:3', // subgraph:groupnode:innernode
graph: {},
getInputNode: vi.fn().mockReturnValue(mockInputNode),
@@ -180,7 +181,7 @@ describe('ExecutableGroupNodeChildDTO', () => {
origin_slot: 0
}),
inputs: []
} as any
})
// Create DTO with deeply nested path to simulate group node inside subgraph
const dto = new ExecutableGroupNodeChildDTO(