mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
* [ci] ignore local browser tests files this is where i have claude put its one off playwright scripts * [feat] carve out path to call asset browser in combo widget * [feat] use buttons on Model Loaders when Asset API setting is on
354 lines
12 KiB
TypeScript
354 lines
12 KiB
TypeScript
import { createPinia, setActivePinia } from 'pinia'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema'
|
|
import {
|
|
ModelNodeProvider,
|
|
useModelToNodeStore
|
|
} from '@/stores/modelToNodeStore'
|
|
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
|
|
|
const EXPECTED_DEFAULT_TYPES = [
|
|
'checkpoints',
|
|
'loras',
|
|
'vae',
|
|
'controlnet',
|
|
'unet',
|
|
'upscale_models',
|
|
'style_models',
|
|
'gligen'
|
|
] as const
|
|
|
|
type NodeDefStoreType = typeof import('@/stores/nodeDefStore')
|
|
|
|
// Create minimal but valid ComfyNodeDefImpl for testing
|
|
function createMockNodeDef(name: string): ComfyNodeDefImpl {
|
|
const def: ComfyNodeDefV1 = {
|
|
name,
|
|
display_name: name,
|
|
category: 'test',
|
|
python_module: 'nodes',
|
|
description: '',
|
|
input: { required: {}, optional: {} },
|
|
output: [],
|
|
output_name: [],
|
|
output_is_list: [],
|
|
output_node: false
|
|
}
|
|
return new ComfyNodeDefImpl(def)
|
|
}
|
|
|
|
const MOCK_NODE_NAMES = [
|
|
'CheckpointLoaderSimple',
|
|
'ImageOnlyCheckpointLoader',
|
|
'LoraLoader',
|
|
'LoraLoaderModelOnly',
|
|
'VAELoader',
|
|
'ControlNetLoader',
|
|
'UNETLoader',
|
|
'UpscaleModelLoader',
|
|
'StyleModelLoader',
|
|
'GLIGENLoader'
|
|
] as const
|
|
|
|
const mockNodeDefsByName = Object.fromEntries(
|
|
MOCK_NODE_NAMES.map((name) => [name, createMockNodeDef(name)])
|
|
)
|
|
|
|
// Mock nodeDefStore dependency - modelToNodeStore relies on this for registration
|
|
// Most tests expect this to be populated; tests that need empty state can override
|
|
vi.mock('@/stores/nodeDefStore', async (importOriginal) => {
|
|
const original = await importOriginal<NodeDefStoreType>()
|
|
|
|
return {
|
|
...original,
|
|
useNodeDefStore: vi.fn(() => ({
|
|
nodeDefsByName: mockNodeDefsByName
|
|
}))
|
|
}
|
|
})
|
|
|
|
describe('useModelToNodeStore', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('modelToNodeMap', () => {
|
|
it('should initialize as empty', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
expect(Object.keys(modelToNodeStore.modelToNodeMap)).toHaveLength(0)
|
|
})
|
|
|
|
it('should populate after registration', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
expect(Object.keys(modelToNodeStore.modelToNodeMap)).toEqual(
|
|
expect.arrayContaining(['checkpoints', 'unet'])
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('getNodeProvider', () => {
|
|
it('should return provider for registered model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
const provider = modelToNodeStore.getNodeProvider('checkpoints')
|
|
expect(provider).toBeDefined()
|
|
// After asserting provider is defined, we can safely access its properties
|
|
expect(provider?.nodeDef?.name).toBe('CheckpointLoaderSimple')
|
|
expect(provider?.key).toBe('ckpt_name')
|
|
})
|
|
|
|
it('should return undefined for unregistered model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
expect(modelToNodeStore.getNodeProvider('nonexistent')).toBeUndefined()
|
|
})
|
|
|
|
it('should return first registered provider when multiple providers exist for same model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
const provider = modelToNodeStore.getNodeProvider('checkpoints')
|
|
// Using optional chaining for safety since getNodeProvider() can return undefined
|
|
expect(provider?.nodeDef?.name).toBe('CheckpointLoaderSimple')
|
|
})
|
|
|
|
it('should trigger lazy registration when called before registerDefaults', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
|
|
const provider = modelToNodeStore.getNodeProvider('checkpoints')
|
|
expect(provider).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('getAllNodeProviders', () => {
|
|
it('should return all providers for model type with multiple nodes', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
const checkpointProviders =
|
|
modelToNodeStore.getAllNodeProviders('checkpoints')
|
|
expect(checkpointProviders).toHaveLength(2)
|
|
expect(checkpointProviders).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
nodeDef: expect.objectContaining({ name: 'CheckpointLoaderSimple' })
|
|
}),
|
|
expect.objectContaining({
|
|
nodeDef: expect.objectContaining({
|
|
name: 'ImageOnlyCheckpointLoader'
|
|
})
|
|
})
|
|
])
|
|
)
|
|
|
|
const loraProviders = modelToNodeStore.getAllNodeProviders('loras')
|
|
expect(loraProviders).toHaveLength(2)
|
|
})
|
|
|
|
it('should return single provider for model type with one node', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
const unetProviders = modelToNodeStore.getAllNodeProviders('unet')
|
|
expect(unetProviders).toHaveLength(1)
|
|
expect(unetProviders[0].nodeDef.name).toBe('UNETLoader')
|
|
})
|
|
|
|
it('should return empty array for unregistered model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
expect(modelToNodeStore.getAllNodeProviders('nonexistent')).toEqual([])
|
|
})
|
|
|
|
it('should trigger lazy registration when called before registerDefaults', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
|
|
const providers = modelToNodeStore.getAllNodeProviders('checkpoints')
|
|
expect(providers.length).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('registerNodeProvider', () => {
|
|
it('should register provider directly', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
const nodeDefStore = useNodeDefStore()
|
|
const customProvider = new ModelNodeProvider(
|
|
nodeDefStore.nodeDefsByName['UNETLoader'],
|
|
'custom_key'
|
|
)
|
|
|
|
modelToNodeStore.registerNodeProvider('custom_type', customProvider)
|
|
|
|
const retrieved = modelToNodeStore.getNodeProvider('custom_type')
|
|
expect(retrieved).toStrictEqual(customProvider)
|
|
// Optional chaining for consistency with getNodeProvider() return type
|
|
expect(retrieved?.key).toBe('custom_key')
|
|
})
|
|
|
|
it('should handle multiple providers for same model type and return first as primary', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
const nodeDefStore = useNodeDefStore()
|
|
const provider1 = new ModelNodeProvider(
|
|
nodeDefStore.nodeDefsByName['UNETLoader'],
|
|
'key1'
|
|
)
|
|
const provider2 = new ModelNodeProvider(
|
|
nodeDefStore.nodeDefsByName['VAELoader'],
|
|
'key2'
|
|
)
|
|
|
|
modelToNodeStore.registerNodeProvider('multi_type', provider1)
|
|
modelToNodeStore.registerNodeProvider('multi_type', provider2)
|
|
|
|
const allProviders = modelToNodeStore.getAllNodeProviders('multi_type')
|
|
expect(allProviders).toHaveLength(2)
|
|
expect(modelToNodeStore.getNodeProvider('multi_type')).toStrictEqual(
|
|
provider1
|
|
)
|
|
})
|
|
|
|
it('should initialize new model type when first provider is registered', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
const nodeDefStore = useNodeDefStore()
|
|
expect(modelToNodeStore.modelToNodeMap['new_type']).toBeUndefined()
|
|
|
|
const provider = new ModelNodeProvider(
|
|
nodeDefStore.nodeDefsByName['UNETLoader'],
|
|
'test_key'
|
|
)
|
|
modelToNodeStore.registerNodeProvider('new_type', provider)
|
|
|
|
expect(modelToNodeStore.modelToNodeMap['new_type']).toBeDefined()
|
|
expect(modelToNodeStore.modelToNodeMap['new_type']).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('quickRegister', () => {
|
|
it('should connect node class to model type with parameter mapping', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.quickRegister('test_type', 'UNETLoader', 'test_param')
|
|
|
|
const provider = modelToNodeStore.getNodeProvider('test_type')
|
|
expect(provider).toBeDefined()
|
|
// After asserting provider is defined, we can safely access its properties
|
|
expect(provider!.nodeDef.name).toBe('UNETLoader')
|
|
expect(provider!.key).toBe('test_param')
|
|
})
|
|
|
|
it('should handle registration of non-existent node classes gracefully', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
expect(() => {
|
|
modelToNodeStore.quickRegister(
|
|
'test_type',
|
|
'NonExistentLoader',
|
|
'test_param'
|
|
)
|
|
}).not.toThrow()
|
|
|
|
const provider = modelToNodeStore.getNodeProvider('test_type')
|
|
// Optional chaining needed since getNodeProvider() can return undefined
|
|
expect(provider?.nodeDef).toBeUndefined()
|
|
})
|
|
|
|
it('should allow multiple node classes for same model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.quickRegister('multi_type', 'UNETLoader', 'param1')
|
|
modelToNodeStore.quickRegister('multi_type', 'VAELoader', 'param2')
|
|
|
|
const providers = modelToNodeStore.getAllNodeProviders('multi_type')
|
|
expect(providers).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe('registerDefaults integration', () => {
|
|
it('should register all expected model types based on mock data', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
for (const modelType of EXPECTED_DEFAULT_TYPES) {
|
|
expect.soft(modelToNodeStore.getNodeProvider(modelType)).toBeDefined()
|
|
}
|
|
})
|
|
|
|
it('should be idempotent', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
const firstCheckpointCount =
|
|
modelToNodeStore.getAllNodeProviders('checkpoints').length
|
|
|
|
modelToNodeStore.registerDefaults() // Call again
|
|
const secondCheckpointCount =
|
|
modelToNodeStore.getAllNodeProviders('checkpoints').length
|
|
|
|
expect(secondCheckpointCount).toBe(firstCheckpointCount)
|
|
})
|
|
|
|
it('should not register when nodeDefStore is empty', () => {
|
|
// Create fresh Pinia for this test to avoid state persistence
|
|
setActivePinia(createPinia())
|
|
|
|
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
|
|
nodeDefsByName: {}
|
|
})
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
expect(modelToNodeStore.getNodeProvider('checkpoints')).toBeUndefined()
|
|
|
|
// Restore original mock for subsequent tests
|
|
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
|
|
nodeDefsByName: mockNodeDefsByName
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('getRegisteredNodeTypes', () => {
|
|
it('should return a Set instance', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
const result = modelToNodeStore.getRegisteredNodeTypes()
|
|
expect(result).toBeInstanceOf(Set)
|
|
})
|
|
|
|
it('should return empty set when nodeDefStore is empty', () => {
|
|
// Create fresh Pinia for this test to avoid state persistence
|
|
setActivePinia(createPinia())
|
|
|
|
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
|
|
nodeDefsByName: {}
|
|
})
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
|
|
const result = modelToNodeStore.getRegisteredNodeTypes()
|
|
expect(result.size).toBe(0)
|
|
|
|
// Restore original mock for subsequent tests
|
|
vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({
|
|
nodeDefsByName: mockNodeDefsByName
|
|
})
|
|
})
|
|
|
|
it('should contain node types for efficient Set.has() lookups', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
modelToNodeStore.registerDefaults()
|
|
|
|
const result = modelToNodeStore.getRegisteredNodeTypes()
|
|
|
|
// Test Set.has() functionality which assetService depends on
|
|
expect(result.has('CheckpointLoaderSimple')).toBe(true)
|
|
expect(result.has('LoraLoader')).toBe(true)
|
|
expect(result.has('NonExistentNode')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty string model type', () => {
|
|
const modelToNodeStore = useModelToNodeStore()
|
|
expect(modelToNodeStore.getNodeProvider('')).toBeUndefined()
|
|
expect(modelToNodeStore.getAllNodeProviders('')).toEqual([])
|
|
})
|
|
})
|
|
})
|