mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-28 08:17:36 +00:00
Compare commits
3 Commits
main
...
test/prior
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de2a41cfb0 | ||
|
|
719d2d9b0b | ||
|
|
04c10981d2 |
211
src/stores/commandStore.test.ts
Normal file
211
src/stores/commandStore.test.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
useErrorHandling: () => ({
|
||||
wrapWithErrorHandlingAsync:
|
||||
(fn: () => Promise<void>, errorHandler?: (e: unknown) => void) =>
|
||||
async () => {
|
||||
try {
|
||||
await fn()
|
||||
} catch (e) {
|
||||
if (errorHandler) errorHandler(e)
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/keybindings/keybindingStore', () => ({
|
||||
useKeybindingStore: () => ({
|
||||
getKeybindingByCommandId: () => null
|
||||
})
|
||||
}))
|
||||
|
||||
describe('commandStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
describe('registerCommand', () => {
|
||||
it('registers a command by id', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'test.command',
|
||||
function: vi.fn()
|
||||
})
|
||||
expect(store.isRegistered('test.command')).toBe(true)
|
||||
})
|
||||
|
||||
it('warns on duplicate registration', () => {
|
||||
const store = useCommandStore()
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
store.registerCommand({ id: 'dup', function: vi.fn() })
|
||||
store.registerCommand({ id: 'dup', function: vi.fn() })
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith('Command dup already registered')
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('registerCommands', () => {
|
||||
it('registers multiple commands at once', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommands([
|
||||
{ id: 'cmd.a', function: vi.fn() },
|
||||
{ id: 'cmd.b', function: vi.fn() }
|
||||
])
|
||||
expect(store.isRegistered('cmd.a')).toBe(true)
|
||||
expect(store.isRegistered('cmd.b')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getCommand', () => {
|
||||
it('returns the registered command', () => {
|
||||
const store = useCommandStore()
|
||||
const fn = vi.fn()
|
||||
store.registerCommand({ id: 'get.test', function: fn, label: 'Test' })
|
||||
const cmd = store.getCommand('get.test')
|
||||
expect(cmd).toBeDefined()
|
||||
expect(cmd?.label).toBe('Test')
|
||||
})
|
||||
|
||||
it('returns undefined for unregistered command', () => {
|
||||
const store = useCommandStore()
|
||||
expect(store.getCommand('nonexistent')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('commands getter', () => {
|
||||
it('returns all registered commands as an array', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({ id: 'a', function: vi.fn() })
|
||||
store.registerCommand({ id: 'b', function: vi.fn() })
|
||||
expect(store.commands).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('execute', () => {
|
||||
it('executes a registered command', async () => {
|
||||
const store = useCommandStore()
|
||||
const fn = vi.fn()
|
||||
store.registerCommand({ id: 'exec.test', function: fn })
|
||||
await store.execute('exec.test')
|
||||
expect(fn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws for unregistered command', async () => {
|
||||
const store = useCommandStore()
|
||||
await expect(store.execute('missing')).rejects.toThrow(
|
||||
'Command missing not found'
|
||||
)
|
||||
})
|
||||
|
||||
it('passes metadata to the command function', async () => {
|
||||
const store = useCommandStore()
|
||||
const fn = vi.fn()
|
||||
store.registerCommand({ id: 'meta.test', function: fn })
|
||||
await store.execute('meta.test', { metadata: { source: 'keyboard' } })
|
||||
expect(fn).toHaveBeenCalledWith({ source: 'keyboard' })
|
||||
})
|
||||
|
||||
it('calls errorHandler on failure', async () => {
|
||||
const store = useCommandStore()
|
||||
const error = new Error('fail')
|
||||
store.registerCommand({
|
||||
id: 'err.test',
|
||||
function: () => {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
const handler = vi.fn()
|
||||
await store.execute('err.test', { errorHandler: handler })
|
||||
expect(handler).toHaveBeenCalledWith(error)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRegistered', () => {
|
||||
it('returns false for unregistered command', () => {
|
||||
const store = useCommandStore()
|
||||
expect(store.isRegistered('nope')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadExtensionCommands', () => {
|
||||
it('registers commands from an extension', () => {
|
||||
const store = useCommandStore()
|
||||
store.loadExtensionCommands({
|
||||
name: 'test-ext',
|
||||
commands: [
|
||||
{ id: 'ext.cmd1', function: vi.fn(), label: 'Cmd 1' },
|
||||
{ id: 'ext.cmd2', function: vi.fn(), label: 'Cmd 2' }
|
||||
]
|
||||
})
|
||||
expect(store.isRegistered('ext.cmd1')).toBe(true)
|
||||
expect(store.isRegistered('ext.cmd2')).toBe(true)
|
||||
})
|
||||
|
||||
it('skips extensions without commands', () => {
|
||||
const store = useCommandStore()
|
||||
store.loadExtensionCommands({ name: 'no-commands' })
|
||||
expect(store.commands).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ComfyCommandImpl', () => {
|
||||
it('resolves label as string', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'label.str',
|
||||
function: vi.fn(),
|
||||
label: 'Static'
|
||||
})
|
||||
expect(store.getCommand('label.str')?.label).toBe('Static')
|
||||
})
|
||||
|
||||
it('resolves label as function', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'label.fn',
|
||||
function: vi.fn(),
|
||||
label: () => 'Dynamic'
|
||||
})
|
||||
expect(store.getCommand('label.fn')?.label).toBe('Dynamic')
|
||||
})
|
||||
|
||||
it('resolves icon as function', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'icon.fn',
|
||||
function: vi.fn(),
|
||||
icon: () => 'pi pi-check'
|
||||
})
|
||||
expect(store.getCommand('icon.fn')?.icon).toBe('pi pi-check')
|
||||
})
|
||||
|
||||
it('uses label as default menubarLabel', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'mbl.default',
|
||||
function: vi.fn(),
|
||||
label: 'My Label'
|
||||
})
|
||||
expect(store.getCommand('mbl.default')?.menubarLabel).toBe('My Label')
|
||||
})
|
||||
|
||||
it('uses explicit menubarLabel over label', () => {
|
||||
const store = useCommandStore()
|
||||
store.registerCommand({
|
||||
id: 'mbl.explicit',
|
||||
function: vi.fn(),
|
||||
label: 'Label',
|
||||
menubarLabel: 'Menu Label'
|
||||
})
|
||||
expect(store.getCommand('mbl.explicit')?.menubarLabel).toBe('Menu Label')
|
||||
})
|
||||
})
|
||||
})
|
||||
154
src/stores/extensionStore.test.ts
Normal file
154
src/stores/extensionStore.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
|
||||
describe('extensionStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
describe('registerExtension', () => {
|
||||
it('registers an extension by name', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'test.ext' })
|
||||
expect(store.isExtensionInstalled('test.ext')).toBe(true)
|
||||
})
|
||||
|
||||
it('throws for extension without name', () => {
|
||||
const store = useExtensionStore()
|
||||
expect(() => store.registerExtension({ name: '' })).toThrow(
|
||||
"Extensions must have a 'name' property."
|
||||
)
|
||||
})
|
||||
|
||||
it('throws for duplicate registration', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'dup' })
|
||||
expect(() => store.registerExtension({ name: 'dup' })).toThrow(
|
||||
"Extension named 'dup' already registered."
|
||||
)
|
||||
})
|
||||
|
||||
it('warns when registering a disabled extension', () => {
|
||||
const store = useExtensionStore()
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
store.loadDisabledExtensionNames(['disabled.ext'])
|
||||
store.registerExtension({ name: 'disabled.ext' })
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'Extension disabled.ext is disabled.'
|
||||
)
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extensions getter', () => {
|
||||
it('returns all registered extensions', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'ext.a' })
|
||||
store.registerExtension({ name: 'ext.b' })
|
||||
expect(store.extensions).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isExtensionInstalled', () => {
|
||||
it('returns false for uninstalled extension', () => {
|
||||
const store = useExtensionStore()
|
||||
expect(store.isExtensionInstalled('missing')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isExtensionEnabled / loadDisabledExtensionNames', () => {
|
||||
it('all extensions are enabled by default', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'fresh' })
|
||||
expect(store.isExtensionEnabled('fresh')).toBe(true)
|
||||
})
|
||||
|
||||
it('disables extensions from provided list', () => {
|
||||
const store = useExtensionStore()
|
||||
store.loadDisabledExtensionNames(['off.ext'])
|
||||
store.registerExtension({ name: 'off.ext' })
|
||||
expect(store.isExtensionEnabled('off.ext')).toBe(false)
|
||||
})
|
||||
|
||||
it('always disables hardcoded extensions', () => {
|
||||
const store = useExtensionStore()
|
||||
store.loadDisabledExtensionNames([])
|
||||
expect(store.isExtensionEnabled('pysssss.Locking')).toBe(false)
|
||||
expect(store.isExtensionEnabled('pysssss.SnapToGrid')).toBe(false)
|
||||
expect(store.isExtensionEnabled('pysssss.FaviconStatus')).toBe(false)
|
||||
expect(store.isExtensionEnabled('KJNodes.browserstatus')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('enabledExtensions', () => {
|
||||
it('filters out disabled extensions', () => {
|
||||
const store = useExtensionStore()
|
||||
store.loadDisabledExtensionNames(['ext.off'])
|
||||
store.registerExtension({ name: 'ext.on' })
|
||||
store.registerExtension({ name: 'ext.off' })
|
||||
|
||||
const enabled = store.enabledExtensions
|
||||
expect(enabled).toHaveLength(1)
|
||||
expect(enabled[0].name).toBe('ext.on')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isExtensionReadOnly', () => {
|
||||
it('returns true for always-disabled extensions', () => {
|
||||
const store = useExtensionStore()
|
||||
expect(store.isExtensionReadOnly('pysssss.Locking')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for normal extensions', () => {
|
||||
const store = useExtensionStore()
|
||||
expect(store.isExtensionReadOnly('some.custom.ext')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inactiveDisabledExtensionNames', () => {
|
||||
it('returns disabled names not currently installed', () => {
|
||||
const store = useExtensionStore()
|
||||
store.loadDisabledExtensionNames(['ghost.ext', 'installed.ext'])
|
||||
store.registerExtension({ name: 'installed.ext' })
|
||||
|
||||
expect(store.inactiveDisabledExtensionNames).toContain('ghost.ext')
|
||||
expect(store.inactiveDisabledExtensionNames).not.toContain(
|
||||
'installed.ext'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('core extensions', () => {
|
||||
it('captures current extensions as core', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'core.a' })
|
||||
store.registerExtension({ name: 'core.b' })
|
||||
store.captureCoreExtensions()
|
||||
|
||||
expect(store.isCoreExtension('core.a')).toBe(true)
|
||||
expect(store.isCoreExtension('core.b')).toBe(true)
|
||||
})
|
||||
|
||||
it('identifies third-party extensions registered after capture', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'core.x' })
|
||||
store.captureCoreExtensions()
|
||||
|
||||
expect(store.hasThirdPartyExtensions).toBe(false)
|
||||
|
||||
store.registerExtension({ name: 'third.party' })
|
||||
expect(store.hasThirdPartyExtensions).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for isCoreExtension before capture', () => {
|
||||
const store = useExtensionStore()
|
||||
store.registerExtension({ name: 'ext.pre' })
|
||||
expect(store.isCoreExtension('ext.pre')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
95
src/stores/widgetStore.test.ts
Normal file
95
src/stores/widgetStore.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec as InputSpecV1 } from '@/schemas/nodeDefSchema'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
|
||||
/** Cast shorthand — the mock bypasses Zod validation, so we only need the shape `inputIsWidget` reads. */
|
||||
const v1 = (spec: unknown) => spec as InputSpecV1
|
||||
const v2 = (spec: unknown) => spec as InputSpecV2
|
||||
|
||||
vi.mock('@/scripts/widgets', () => ({
|
||||
ComfyWidgets: {
|
||||
INT: vi.fn(),
|
||||
FLOAT: vi.fn(),
|
||||
STRING: vi.fn(),
|
||||
BOOLEAN: vi.fn(),
|
||||
COMBO: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/schemas/nodeDefSchema', () => ({
|
||||
getInputSpecType: (spec: unknown[]) => spec[0]
|
||||
}))
|
||||
|
||||
describe('widgetStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
describe('widgets getter', () => {
|
||||
it('includes core widgets', () => {
|
||||
const store = useWidgetStore()
|
||||
expect(store.widgets.has('INT')).toBe(true)
|
||||
expect(store.widgets.has('FLOAT')).toBe(true)
|
||||
expect(store.widgets.has('STRING')).toBe(true)
|
||||
})
|
||||
|
||||
it('includes custom widgets after registration', () => {
|
||||
const store = useWidgetStore()
|
||||
const customFn = vi.fn()
|
||||
store.registerCustomWidgets({ CUSTOM_TYPE: customFn })
|
||||
expect(store.widgets.has('CUSTOM_TYPE')).toBe(true)
|
||||
})
|
||||
|
||||
it('core widgets take precedence over custom widgets with same key', () => {
|
||||
const store = useWidgetStore()
|
||||
const override = vi.fn()
|
||||
store.registerCustomWidgets({ INT: override })
|
||||
// Core widgets are spread last, so they win
|
||||
expect(store.widgets.get('INT')).not.toBe(override)
|
||||
})
|
||||
})
|
||||
|
||||
describe('registerCustomWidgets', () => {
|
||||
it('registers multiple custom widgets', () => {
|
||||
const store = useWidgetStore()
|
||||
store.registerCustomWidgets({
|
||||
TYPE_A: vi.fn(),
|
||||
TYPE_B: vi.fn()
|
||||
})
|
||||
expect(store.widgets.has('TYPE_A')).toBe(true)
|
||||
expect(store.widgets.has('TYPE_B')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('inputIsWidget', () => {
|
||||
it('returns true for known widget type (v1 spec)', () => {
|
||||
const store = useWidgetStore()
|
||||
expect(store.inputIsWidget(v1(['INT', {}]))).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for unknown type (v1 spec)', () => {
|
||||
const store = useWidgetStore()
|
||||
expect(store.inputIsWidget(v1(['UNKNOWN_TYPE', {}]))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for v2 spec with known type', () => {
|
||||
const store = useWidgetStore()
|
||||
expect(store.inputIsWidget(v2({ type: 'STRING' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for v2 spec with unknown type', () => {
|
||||
const store = useWidgetStore()
|
||||
expect(store.inputIsWidget(v2({ type: 'LATENT' }))).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for custom registered type', () => {
|
||||
const store = useWidgetStore()
|
||||
store.registerCustomWidgets({ MY_WIDGET: vi.fn() })
|
||||
expect(store.inputIsWidget(v2({ type: 'MY_WIDGET' }))).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user