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

View File

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

View File

@@ -14,20 +14,31 @@ describe('useComfyRegistrySearchProvider', () => {
const mockListAllPacksCall = vi.fn() const mockListAllPacksCall = vi.fn()
const mockListAllPacksClear = 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(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
// Setup store mock // Setup store mock
vi.mocked(useComfyRegistryStore).mockReturnValue({ vi.mocked(useComfyRegistryStore).mockReturnValue(createMockStore())
search: {
call: mockSearchCall,
clear: mockSearchClear
},
listAllPacks: {
call: mockListAllPacksCall,
clear: mockListAllPacksClear
}
} as any)
}) })
describe('searchPacks', () => { describe('searchPacks', () => {

View File

@@ -127,7 +127,9 @@ describe('useComfyRegistryStore', () => {
} }
vi.mocked(useComfyRegistryService).mockReturnValue( 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() const store = useComfyRegistryStore()
vi.spyOn(store.getPackById, 'call').mockResolvedValueOnce(null) 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(result).toBeNull()
expect(mockRegistryService.getPackById).not.toHaveBeenCalled() expect(mockRegistryService.getPackById).not.toHaveBeenCalled()

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,19 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */ /** (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) { for (const key of keys) {
if (key in metadata) { if (key in metadata) {
return metadata[key] const value = metadata[key]
return value || null
} }
for (const k in metadata) { for (const k in metadata) {
if (k.endsWith(key)) { 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', () => { it('should not register provider when nodeDef is undefined', () => {
const modelToNodeStore = useModelToNodeStore() const modelToNodeStore = useModelToNodeStore()
const providerWithoutNodeDef = new ModelNodeProvider( const providerWithoutNodeDef = new ModelNodeProvider(
undefined as any, undefined!,
'custom_key' 'custom_key'
) )
@@ -507,15 +507,11 @@ describe('useModelToNodeStore', () => {
modelToNodeStore.registerDefaults() modelToNodeStore.registerDefaults()
// These should not throw but return undefined // These should not throw but return undefined
expect(modelToNodeStore.getCategoryForNodeType(null!)).toBeUndefined()
expect( expect(
modelToNodeStore.getCategoryForNodeType(null as any) modelToNodeStore.getCategoryForNodeType(undefined!)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(undefined as any)
).toBeUndefined()
expect(
modelToNodeStore.getCategoryForNodeType(123 as any)
).toBeUndefined() ).toBeUndefined()
expect(modelToNodeStore.getCategoryForNodeType('123')).toBeUndefined()
}) })
it('should be case-sensitive for node type matching', () => { it('should be case-sensitive for node type matching', () => {

View File

@@ -38,7 +38,11 @@ function createHistoryJob(createTime: number, id: string): JobListItem {
const createTaskOutput = ( const createTaskOutput = (
nodeId: string = 'node-1', nodeId: string = 'node-1',
images: any[] = [] images: {
type?: 'output' | 'input' | 'temp'
filename?: string
subfolder?: string
}[] = []
): TaskOutput => ({ ): TaskOutput => ({
[nodeId]: { [nodeId]: {
images images
@@ -490,7 +494,7 @@ describe('useQueueStore', () => {
it('should recreate TaskItemImpl when outputs_count changes', async () => { it('should recreate TaskItemImpl when outputs_count changes', async () => {
// Initial load without outputs_count // Initial load without outputs_count
const jobWithoutOutputsCount = createHistoryJob(10, 'job-1') const jobWithoutOutputsCount = createHistoryJob(10, 'job-1')
delete (jobWithoutOutputsCount as any).outputs_count delete jobWithoutOutputsCount.outputs_count
mockGetQueue.mockResolvedValue({ Running: [], Pending: [] }) mockGetQueue.mockResolvedValue({ Running: [], Pending: [] })
mockGetHistory.mockResolvedValue([jobWithoutOutputsCount]) 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 type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' 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', () => { vi.mock('@/scripts/app', () => {
const mockCanvas = { const mockCanvas = {
@@ -38,7 +40,7 @@ vi.mock('@/scripts/app', () => {
vi.mock('@/renderer/core/canvas/canvasStore', () => ({ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({ useCanvasStore: () => ({
getCanvas: () => (app as any).canvas getCanvas: () => app.canvas
}) })
})) }))
@@ -63,7 +65,8 @@ describe('useSubgraphNavigationStore', () => {
} as ComfyWorkflow } as ComfyWorkflow
// Set the active workflow (cast to bypass TypeScript check in test) // 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 // Simulate being in a subgraph by restoring state
navigationStore.restoreState(['subgraph-1', 'subgraph-2']) navigationStore.restoreState(['subgraph-1', 'subgraph-2'])
@@ -72,7 +75,9 @@ describe('useSubgraphNavigationStore', () => {
// Simulate a change to the workflow's internal state // Simulate a change to the workflow's internal state
// (e.g., changeTracker.activeState being reassigned) // (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 // The navigation stack should NOT be cleared because the path hasn't changed
expect(navigationStore.exportState()).toHaveLength(2) expect(navigationStore.exportState()).toHaveLength(2)
@@ -87,14 +92,15 @@ describe('useSubgraphNavigationStore', () => {
const workflow1 = { const workflow1 = {
path: 'workflow1.json', path: 'workflow1.json',
filename: 'workflow1.json', filename: 'workflow1.json',
changeTracker: { changeTracker: createMockChangeTracker({
restore: vi.fn(), restore: vi.fn(),
store: vi.fn() store: vi.fn()
} })
} as unknown as ComfyWorkflow } as Partial<ComfyWorkflow> as ComfyWorkflow
// Set the active workflow // 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 // Simulate the restore process that happens when loading a workflow
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation // Since subgraphState is private, we'll simulate the effect by directly restoring navigation
@@ -108,13 +114,14 @@ describe('useSubgraphNavigationStore', () => {
const workflow2 = { const workflow2 = {
path: 'workflow2.json', path: 'workflow2.json',
filename: 'workflow2.json', filename: 'workflow2.json',
changeTracker: { changeTracker: createMockChangeTracker({
restore: vi.fn(), restore: vi.fn(),
store: 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 // Simulate the restore process for workflow2
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation // Since subgraphState is private, we'll simulate the effect by directly restoring navigation
@@ -124,7 +131,8 @@ describe('useSubgraphNavigationStore', () => {
expect(navigationStore.exportState()).toHaveLength(0) expect(navigationStore.exportState()).toHaveLength(0)
// Switch back to workflow1 // Switch back to workflow1
workflowStore.activeWorkflow = workflow1 as any workflowStore.activeWorkflow =
workflow1 as typeof workflowStore.activeWorkflow
// Simulate the restore process for workflow1 again // Simulate the restore process for workflow1 again
// Since subgraphState is private, we'll simulate the effect by directly restoring navigation // 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 () => { it('should clear navigation when activeSubgraph becomes undefined', async () => {
const navigationStore = useSubgraphNavigationStore() const navigationStore = useSubgraphNavigationStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { findSubgraphPathById } = await import('@/utils/graphTraversalUtil')
// Create mock subgraph and graph structure // Create mock subgraph and graph structure
const mockSubgraph = { const mockSubgraph = {
id: 'subgraph-1', id: 'subgraph-1',
rootGraph: (app as any).graph, rootGraph: app.graph,
_nodes: [], _nodes: [],
nodes: [] nodes: []
} } as Partial<Subgraph> as Subgraph
// Add the subgraph to the graph's subgraphs map // 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 // First set an active workflow
const mockWorkflow = { const mockWorkflow = {
@@ -156,13 +165,14 @@ describe('useSubgraphNavigationStore', () => {
filename: 'test-workflow.json' filename: 'test-workflow.json'
} as ComfyWorkflow } as ComfyWorkflow
workflowStore.activeWorkflow = mockWorkflow as any workflowStore.activeWorkflow =
mockWorkflow as typeof workflowStore.activeWorkflow
// Mock findSubgraphPathById to return the correct path // Mock findSubgraphPathById to return the correct path
vi.mocked(findSubgraphPathById).mockReturnValue(['subgraph-1']) vi.mocked(findSubgraphPathById).mockReturnValue(['subgraph-1'])
// Set canvas.subgraph and trigger update to set activeSubgraph // Set canvas.subgraph and trigger update to set activeSubgraph
;(app as any).canvas.subgraph = mockSubgraph app.canvas.subgraph = mockSubgraph
workflowStore.updateActiveGraph() workflowStore.updateActiveGraph()
// Wait for Vue's reactivity to process the change // Wait for Vue's reactivity to process the change
@@ -173,7 +183,7 @@ describe('useSubgraphNavigationStore', () => {
expect(navigationStore.exportState()).toEqual(['subgraph-1']) expect(navigationStore.exportState()).toEqual(['subgraph-1'])
// Clear canvas.subgraph and trigger update (simulating navigating back to root) // Clear canvas.subgraph and trigger update (simulating navigating back to root)
;(app as any).canvas.subgraph = null app.canvas.subgraph = undefined
workflowStore.updateActiveGraph() workflowStore.updateActiveGraph()
// Wait for Vue's reactivity to process the change // 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 { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app' import { app } from '@/scripts/app'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
const { mockSetDirty } = vi.hoisted(() => ({
mockSetDirty: vi.fn()
}))
vi.mock('@/scripts/app', () => { vi.mock('@/scripts/app', () => {
const mockCanvas = { const mockCanvas = {
subgraph: null, subgraph: null,
@@ -19,7 +24,7 @@ vi.mock('@/scripts/app', () => {
offset: [0, 0] offset: [0, 0]
} }
}, },
setDirty: vi.fn() setDirty: mockSetDirty
} }
return { return {
@@ -38,12 +43,12 @@ vi.mock('@/scripts/app', () => {
// Mock canvasStore // Mock canvasStore
vi.mock('@/renderer/core/canvas/canvasStore', () => ({ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({ useCanvasStore: () => ({
getCanvas: () => (app as any).canvas getCanvas: () => app.canvas
}) })
})) }))
// Get reference to mock canvas // Get reference to mock canvas
const mockCanvas = app.canvas as any const mockCanvas = app.canvas
describe('useSubgraphNavigationStore - Viewport Persistence', () => { describe('useSubgraphNavigationStore - Viewport Persistence', () => {
beforeEach(() => { beforeEach(() => {
@@ -53,7 +58,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
mockCanvas.ds.offset = [0, 0] mockCanvas.ds.offset = [0, 0]
mockCanvas.ds.state.scale = 1 mockCanvas.ds.state.scale = 1
mockCanvas.ds.state.offset = [0, 0] mockCanvas.ds.state.offset = [0, 0]
mockCanvas.setDirty.mockClear() mockSetDirty.mockClear()
}) })
describe('saveViewport', () => { describe('saveViewport', () => {
@@ -99,7 +104,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Mock being in a subgraph // Mock being in a subgraph
const mockSubgraph = { id: 'sub-456' } const mockSubgraph = { id: 'sub-456' }
workflowStore.activeSubgraph = mockSubgraph as any workflowStore.activeSubgraph = mockSubgraph as Subgraph
// Set viewport state // Set viewport state
mockCanvas.ds.state.scale = 3 mockCanvas.ds.state.scale = 3
@@ -159,7 +164,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Reset canvas // Reset canvas
mockCanvas.ds.scale = 1 mockCanvas.ds.scale = 1
mockCanvas.ds.offset = [0, 0] mockCanvas.ds.offset = [0, 0]
mockCanvas.setDirty.mockClear() mockSetDirty.mockClear()
// Try to restore non-existent viewport // Try to restore non-existent viewport
navigationStore.restoreViewport('non-existent') navigationStore.restoreViewport('non-existent')
@@ -167,7 +172,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
// Canvas should not change // Canvas should not change
expect(mockCanvas.ds.scale).toBe(1) expect(mockCanvas.ds.scale).toBe(1)
expect(mockCanvas.ds.offset).toEqual([0, 0]) 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: [], nodes: [],
subgraphs: new Map(), subgraphs: new Map(),
getNodeById: vi.fn() getNodeById: vi.fn()
} } as Partial<LGraph> as LGraph
const subgraph1 = { const subgraph1 = {
id: 'sub1', id: 'sub1',
rootGraph: mockRootGraph, rootGraph: mockRootGraph,
@@ -195,7 +200,7 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
mockCanvas.ds.state.offset = [100, 100] mockCanvas.ds.state.offset = [100, 100]
// Navigate to subgraph // Navigate to subgraph
workflowStore.activeSubgraph = subgraph1 as any workflowStore.activeSubgraph = subgraph1 as Partial<Subgraph> as Subgraph
await nextTick() await nextTick()
// Root viewport should have been saved automatically // Root viewport should have been saved automatically
@@ -240,10 +245,14 @@ describe('useSubgraphNavigationStore - Viewport Persistence', () => {
const workflow1 = { path: 'workflow1.json' } as ComfyWorkflow const workflow1 = { path: 'workflow1.json' } as ComfyWorkflow
const workflow2 = { path: 'workflow2.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() await nextTick()
workflowStore.activeWorkflow = workflow2 as any workflowStore.activeWorkflow = workflow2 as ReturnType<
typeof useWorkflowStore
>['activeWorkflow']
await nextTick() await nextTick()
// Cache should be preserved (LRU will manage memory) // Cache should be preserved (LRU will manage memory)

View File

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

View File

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

View File

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

View File

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

View File

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