Compare commits

...

1 Commits

Author SHA1 Message Date
huang47
3d3e7feeb5 test: cover critical store branches 2026-06-30 22:37:09 -07:00
5 changed files with 866 additions and 3 deletions

View File

@@ -0,0 +1,135 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AboutPageBadge } from '@/types/comfy'
import { useAboutPanelStore } from '@/stores/aboutPanelStore'
interface SystemInfo {
comfyui_version?: string
installed_templates_version?: string
required_templates_version?: string
}
const { dist, stats, exts } = vi.hoisted(() => ({
dist: { isCloud: false, isDesktop: false },
stats: { system: {} as SystemInfo },
exts: { list: [] as { aboutPageBadges?: AboutPageBadge[] }[] }
}))
vi.mock('@/platform/distribution/types', () => ({
get isCloud() {
return dist.isCloud
},
get isDesktop() {
return dist.isDesktop
}
}))
vi.mock('@/composables/useExternalLink', () => ({
useExternalLink: () => ({
staticUrls: {
github: 'https://github.com/comfyanonymous/ComfyUI',
githubFrontend: 'https://github.com/Comfy-Org/ComfyUI_frontend',
comfyOrg: 'https://comfy.org',
discord: 'https://discord.com'
}
})
}))
vi.mock('@/utils/envUtil', () => ({
electronAPI: () => ({ getComfyUIVersion: () => '9.9.9' })
}))
vi.mock('@/stores/extensionStore', () => ({
useExtensionStore: () => ({ extensions: exts.list })
}))
vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: () => ({ systemStats: stats })
}))
function label(badges: AboutPageBadge[], includes: string) {
return badges.find((b) => b.label.includes(includes))
}
beforeEach(() => {
setActivePinia(createPinia())
dist.isCloud = false
dist.isDesktop = false
stats.system = {}
exts.list = []
})
describe('aboutPanelStore', () => {
it('builds the default desktop-less, non-cloud core badges', () => {
stats.system = { comfyui_version: 'abc1234' }
const store = useAboutPanelStore()
const core = label(store.badges, 'ComfyUI ')!
expect(core.icon).toBe('pi pi-github')
expect(core.url).toContain('github.com/comfyanonymous')
expect(label(store.badges, 'ComfyUI_frontend')).toBeDefined()
expect(label(store.badges, 'Discord')).toBeDefined()
expect(label(store.badges, 'Templates')).toBeUndefined()
})
it('uses cloud url and icon for the core badge when running on cloud', () => {
dist.isCloud = true
const store = useAboutPanelStore()
const core = label(store.badges, 'ComfyUI ')!
expect(core.icon).toBe('pi pi-cloud')
expect(core.url).toBe('https://comfy.org')
})
it('uses the electron-reported version label on desktop', () => {
dist.isDesktop = true
const store = useAboutPanelStore()
expect(label(store.badges, 'ComfyUI v9.9.9')).toBeDefined()
})
it('adds a danger templates badge when the installed version is outdated', () => {
stats.system = {
installed_templates_version: '1.0.0',
required_templates_version: '1.1.0'
}
const store = useAboutPanelStore()
const templates = label(store.badges, 'Templates v1.0.0')!
expect(templates.severity).toBe('danger')
})
it('adds a templates badge without severity when versions match', () => {
stats.system = {
installed_templates_version: '1.1.0',
required_templates_version: '1.1.0'
}
const store = useAboutPanelStore()
const templates = label(store.badges, 'Templates v1.1.0')!
expect(templates.severity).toBeUndefined()
})
it('does not mark templates outdated when the required version is missing', () => {
stats.system = {
installed_templates_version: '1.1.0'
}
const store = useAboutPanelStore()
const templates = label(store.badges, 'Templates v1.1.0')!
expect(templates.severity).toBeUndefined()
})
it('appends extension badges and tolerates extensions without any', () => {
exts.list = [
{
aboutPageBadges: [{ label: 'My Ext', url: 'https://ext', icon: 'pi' }]
},
{} // extension without aboutPageBadges -> ?? [] branch
]
const store = useAboutPanelStore()
expect(label(store.badges, 'My Ext')).toBeDefined()
})
})

View File

@@ -0,0 +1,197 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import type { ComfyApiWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
import { useExecutionStore } from '@/stores/executionStore'
const {
handlers,
openSet,
errorStore,
dist,
resolvePrecondition,
classifyCloud
} = vi.hoisted(() => ({
handlers: {} as Record<string, (e: { detail: unknown }) => void>,
openSet: new Set<unknown>(),
errorStore: {
clearExecutionStartErrors: () => {},
clearPromptError: () => {}
} as Record<string, unknown>,
dist: { isCloud: false },
resolvePrecondition: vi.fn(),
classifyCloud: vi.fn()
}))
vi.mock('@/scripts/app', () => ({ app: { rootGraph: {} } }))
vi.mock('@/scripts/api', () => ({
api: {
addEventListener: (name: string, fn: (e: { detail: unknown }) => void) => {
handlers[name] = fn
},
removeEventListener: () => {}
}
}))
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
useWorkflowStore: () => ({
isOpen: (workflow: unknown) => openSet.has(workflow),
openWorkflows: [],
nodeLocatorIdToNodeExecutionId: () => null
})
}))
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({ canvas: undefined })
}))
vi.mock('@/stores/executionErrorStore', () => ({
useExecutionErrorStore: () => errorStore
}))
vi.mock('@/composables/useAppMode', () => ({
useAppMode: () => ({ mode: ref('default'), isAppMode: ref(false) })
}))
vi.mock('@/platform/telemetry', () => ({ useTelemetry: () => undefined }))
vi.mock('@/utils/appMode', () => ({
getWorkflowMode: () => 'workflow',
isAppModeValue: () => false
}))
vi.mock('@/platform/distribution/types', () => ({
get isCloud() {
return dist.isCloud
}
}))
vi.mock('@/platform/errorCatalog/accountPreconditionRouting', () => ({
resolveAccountPrecondition: resolvePrecondition
}))
vi.mock('@/utils/executionErrorUtil', () => ({
classifyCloudValidationError: classifyCloud
}))
function workflow(path: string): ComfyWorkflow {
return { path } as unknown as ComfyWorkflow
}
function promptOutput(): ComfyApiWorkflow {
return {}
}
function setup() {
const store = useExecutionStore()
store.bindExecutionEvents()
return store
}
function fireError(detail: Record<string, unknown>) {
handlers['execution_error']?.({ detail })
}
beforeEach(() => {
setActivePinia(createPinia())
for (const key of Object.keys(handlers)) delete handlers[key]
openSet.clear()
dist.isCloud = false
resolvePrecondition.mockReturnValue(null)
classifyCloud.mockReturnValue(null)
for (const key of ['lastExecutionError', 'lastPromptError', 'lastNodeErrors'])
delete errorStore[key]
})
describe('executionStore error handling', () => {
it('marks an open workflow failed and records the raw execution error', () => {
const store = setup()
const wf = workflow('a.json')
openSet.add(wf)
store.storeJob({
nodes: [],
id: 'job-1',
promptOutput: promptOutput(),
workflow: wf
})
const detail = {
prompt_id: 'job-1',
node_id: '5',
exception_message: 'boom'
}
fireError(detail)
expect(store.getWorkflowStatus(wf)).toBe('failed')
expect(errorStore.lastExecutionError).toBe(detail)
})
it('routes account-precondition errors away from the failed badge', () => {
resolvePrecondition.mockReturnValue({ type: 'credits' })
const store = setup()
const wf = workflow('b.json')
openSet.add(wf)
store.storeJob({
nodes: [],
id: 'job-2',
promptOutput: promptOutput(),
workflow: wf
})
fireError({
prompt_id: 'job-2',
node_id: '5',
exception_type: 'AccountError'
})
expect(resolvePrecondition).toHaveBeenCalledWith({
exceptionType: 'AccountError',
exceptionMessage: ''
})
expect(store.getWorkflowStatus(wf)).toBeUndefined()
expect(errorStore.lastExecutionError).toBeUndefined()
expect(errorStore.lastPromptError).toBeUndefined()
})
it('records a node-less service-level error as a prompt error', () => {
setup()
fireError({
prompt_id: 'job-3',
exception_type: 'StagnationError',
exception_message: 'stuck',
traceback: ['line1', 'line2']
})
expect(errorStore.lastPromptError).toEqual({
type: 'StagnationError',
message: 'StagnationError: stuck',
details: 'line1\nline2'
})
})
it('records classified cloud validation node errors without a failed badge', () => {
dist.isCloud = true
classifyCloud.mockReturnValue({
kind: 'nodeErrors',
nodeErrors: { '5': { errors: [] } }
})
const store = setup()
const wf = workflow('c.json')
openSet.add(wf)
store.storeJob({
nodes: [],
id: 'job-4',
promptOutput: promptOutput(),
workflow: wf
})
fireError({ prompt_id: 'job-4', exception_message: '{"nodeErrors":{}}' })
expect(store.getWorkflowStatus(wf)).toBeUndefined()
expect(errorStore.lastNodeErrors).toEqual({ '5': { errors: [] } })
})
})

View File

@@ -0,0 +1,243 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
const BOOKMARK_ID = 'Comfy.NodeLibrary.Bookmarks.V2'
const CUSTOMIZATION_ID = 'Comfy.NodeLibrary.BookmarksCustomization'
const { settings, setSpy, nodeDefs } = vi.hoisted(() => ({
settings: {} as Record<string, unknown>,
setSpy: vi.fn(),
nodeDefs: {} as Record<string, unknown>
}))
vi.mock('@/platform/settings/settingStore', async () => {
const { reactive } = await import('vue')
const reactiveSettings = reactive(settings)
setSpy.mockImplementation(async (id: string, value: unknown) => {
reactiveSettings[id] = value
})
return {
useSettingStore: () => ({
get: (id: string) => reactiveSettings[id],
set: setSpy
})
}
})
vi.mock('@/stores/nodeDefStore', () => ({
useNodeDefStore: () => ({ allNodeDefsByName: nodeDefs }),
buildNodeDefTree: (defs: unknown[]) => ({ key: 'root', children: defs }),
createDummyFolderNodeDef: (path: string) => ({
isDummyFolder: true,
nodePath: path,
name: path
})
}))
type BookmarkNodeFixture = Pick<
ComfyNodeDefImpl,
'isDummyFolder' | 'nodePath' | 'category' | 'name'
>
function folderNode(nodePath: string) {
const node = {
isDummyFolder: true,
nodePath,
category: nodePath.replace(/\/$/, ''),
name: nodePath
} satisfies BookmarkNodeFixture
return node as ComfyNodeDefImpl
}
function leafNode(name: string, nodePath = name) {
const node = {
isDummyFolder: false,
name,
nodePath,
category: ''
} satisfies BookmarkNodeFixture
return node as ComfyNodeDefImpl
}
beforeEach(() => {
setActivePinia(createPinia())
for (const key of Object.keys(settings)) delete settings[key]
for (const key of Object.keys(nodeDefs)) delete nodeDefs[key]
settings[BOOKMARK_ID] = []
settings[CUSTOMIZATION_ID] = {}
setSpy.mockClear()
})
describe('nodeBookmarkStore', () => {
it('reports isBookmarked by either nodePath or top-level name', () => {
settings[BOOKMARK_ID] = ['sampling/KSampler', 'LoadImage']
const store = useNodeBookmarkStore()
expect(store.isBookmarked(leafNode('KSampler', 'sampling/KSampler'))).toBe(
true
)
expect(store.isBookmarked(leafNode('LoadImage'))).toBe(true)
expect(store.isBookmarked(leafNode('VAEDecode'))).toBe(false)
})
it('adds a bookmark by appending to the current list', async () => {
settings[BOOKMARK_ID] = ['A']
const store = useNodeBookmarkStore()
await store.addBookmark('B')
expect(setSpy).toHaveBeenCalledWith(BOOKMARK_ID, ['A', 'B'])
})
it('toggles an un-bookmarked node by adding its name', async () => {
const store = useNodeBookmarkStore()
await store.toggleBookmark(leafNode('KSampler'))
expect(setSpy).toHaveBeenCalledWith(BOOKMARK_ID, ['KSampler'])
})
it('toggles a bookmarked node by deleting both nodePath and name', async () => {
settings[BOOKMARK_ID] = ['sampling/KSampler', 'KSampler']
const store = useNodeBookmarkStore()
await store.toggleBookmark(leafNode('KSampler', 'sampling/KSampler'))
expect(setSpy).toHaveBeenLastCalledWith(BOOKMARK_ID, [])
expect(store.bookmarks).toEqual([])
})
it('creates a folder under a parent and at the root', async () => {
const store = useNodeBookmarkStore()
const rootPath = await store.addNewBookmarkFolder(undefined, 'Favorites')
expect(rootPath).toBe('Favorites/')
const childPath = await store.addNewBookmarkFolder(
folderNode('Favorites/'),
'Nested'
)
expect(childPath).toBe('Favorites/Nested/')
})
it('builds the bookmark tree, dropping unknown node defs', () => {
nodeDefs['KSampler'] = leafNode('KSampler')
settings[BOOKMARK_ID] = ['sampling/KSampler', 'sampling/Unknown', 'Folder/']
const store = useNodeBookmarkStore()
const children = (store.bookmarkedRoot as { children: unknown[] }).children
expect(children).toHaveLength(2)
})
describe('renameBookmarkFolder', () => {
it('rejects renaming a non-folder node', async () => {
const store = useNodeBookmarkStore()
await expect(
store.renameBookmarkFolder(leafNode('KSampler'), 'New')
).rejects.toThrow('Cannot rename non-folder node')
})
it('rejects a name containing a slash', async () => {
const store = useNodeBookmarkStore()
await expect(
store.renameBookmarkFolder(folderNode('Old/'), 'a/b')
).rejects.toThrow('cannot contain')
})
it('rejects a rename that collides with an existing folder', async () => {
settings[BOOKMARK_ID] = ['Taken/']
const store = useNodeBookmarkStore()
await expect(
store.renameBookmarkFolder(folderNode('Old/'), 'Taken')
).rejects.toThrow('already exists')
})
it('rewrites matching bookmark paths on a valid rename', async () => {
settings[BOOKMARK_ID] = ['Old/', 'Old/KSampler', 'Other/Node']
settings[CUSTOMIZATION_ID] = { 'Old/': { color: '#abc' } }
const store = useNodeBookmarkStore()
await store.renameBookmarkFolder(folderNode('Old/'), 'New')
expect(setSpy).toHaveBeenCalledWith(BOOKMARK_ID, [
'New/',
'New/KSampler',
'Other/Node'
])
expect(setSpy).toHaveBeenCalledWith(CUSTOMIZATION_ID, {
'New/': { color: '#abc' }
})
})
it('does nothing when the folder keeps the same path', async () => {
const store = useNodeBookmarkStore()
await store.renameBookmarkFolder(folderNode('Old/'), 'Old')
expect(setSpy).not.toHaveBeenCalled()
})
})
it('deletes a folder and all its descendants', async () => {
settings[BOOKMARK_ID] = ['Old/', 'Old/KSampler', 'Keep/Node']
settings[CUSTOMIZATION_ID] = { 'Old/': { color: '#abc' } }
const store = useNodeBookmarkStore()
await store.deleteBookmarkFolder(folderNode('Old/'))
expect(settings[BOOKMARK_ID]).toEqual(['Keep/Node'])
expect(
(settings[CUSTOMIZATION_ID] as Record<string, unknown>)['Old/']
).toBeUndefined()
})
it('rejects deleting a non-folder node', async () => {
const store = useNodeBookmarkStore()
await expect(
store.deleteBookmarkFolder(leafNode('KSampler'))
).rejects.toThrow('Cannot delete non-folder node')
})
describe('updateBookmarkCustomization', () => {
it('persists a non-default customization', async () => {
const store = useNodeBookmarkStore()
await store.updateBookmarkCustomization('Folder/', {
color: '#ff0000',
icon: 'pi-star'
})
expect(setSpy).toHaveBeenCalledWith(CUSTOMIZATION_ID, {
'Folder/': { color: '#ff0000', icon: 'pi-star' }
})
})
it('drops attributes set to their default values', async () => {
const store = useNodeBookmarkStore()
await store.updateBookmarkCustomization('Folder/', {
color: store.defaultBookmarkColor,
icon: store.defaultBookmarkIcon
})
expect(setSpy).toHaveBeenCalledWith(CUSTOMIZATION_ID, {
'Folder/': undefined
})
})
})
it('renames a customization entry, moving the old key to the new one', async () => {
settings[CUSTOMIZATION_ID] = { 'Old/': { color: '#abc' } }
const store = useNodeBookmarkStore()
await store.renameBookmarkCustomization('Old/', 'New/')
expect(setSpy).toHaveBeenCalledWith(CUSTOMIZATION_ID, {
'New/': { color: '#abc' }
})
})
})

View File

@@ -1,5 +1,5 @@
import { createTestingPinia } from '@pinia/testing'
import { fromPartial } from '@total-typescript/shoehorn'
import { fromAny, fromPartial } from '@total-typescript/shoehorn'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -10,7 +10,11 @@ import type {
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyApp } from '@/scripts/app'
import * as jobOutputCache from '@/services/jobOutputCache'
import type { TaskOutput } from '@/schemas/apiSchema'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { TaskItemImpl } from '@/stores/queueStore'
import { createNodeExecutionId } from '@/types/nodeIdentification'
import { toNodeId } from '@/types/nodeId'
vi.mock('@/services/extensionService', () => ({
useExtensionService: vi.fn(() => ({
@@ -44,7 +48,9 @@ const mockJobDetail = {
}
},
outputs: {
'1': { images: [{ filename: 'test.png', subfolder: '', type: 'output' }] }
'1': {
images: [{ filename: 'test.png', subfolder: '', type: 'output' as const }]
}
}
}
@@ -137,4 +143,110 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => {
expect(jobOutputCache.getJobDetail).toHaveBeenCalled()
expect(mockApp.loadGraphData).not.toHaveBeenCalled()
})
it('should load full outputs for history tasks', async () => {
const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
mockJobDetail as JobDetail
)
const loaded = await task.loadFullOutputs()
expect(loaded).not.toBe(task)
expect(loaded.flatOutputs[0].filename).toBe('test.png')
})
it('should not load full outputs for running tasks', async () => {
const job = createRunningJob('test-job-id')
const task = new TaskItemImpl(job)
const detailSpy = vi.spyOn(jobOutputCache, 'getJobDetail')
const loaded = await task.loadFullOutputs()
expect(loaded).toBe(task)
expect(detailSpy).not.toHaveBeenCalled()
})
it('should keep history tasks when full outputs are unavailable', async () => {
const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
fromPartial<JobDetail>({ id: 'test-job-id', status: 'completed' })
)
const loaded = await task.loadFullOutputs()
expect(loaded).toBe(task)
})
it('should load workflow outputs from the task when job detail has none', async () => {
const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job, mockJobDetail.outputs)
const nodeOutputStore = useNodeOutputStore()
const setOutputsSpy = vi.spyOn(
nodeOutputStore,
'setNodeOutputsByExecutionId'
)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
fromPartial<JobDetail>({ ...mockJobDetail, outputs: undefined })
)
await task.loadWorkflow(mockApp)
expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow)
expect(setOutputsSpy).toHaveBeenCalledOnce()
expect(
nodeOutputStore.getNodeOutputByExecutionId(
createNodeExecutionId([toNodeId(1)])
)
).toEqual(mockJobDetail.outputs['1'])
})
it('should skip workflow output loading when no outputs exist', async () => {
const job = createHistoryJob('test-job-id')
const task = new TaskItemImpl(job, fromAny<TaskOutput, unknown>(null))
const nodeOutputStore = useNodeOutputStore()
const setOutputsSpy = vi.spyOn(
nodeOutputStore,
'setNodeOutputsByExecutionId'
)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
fromPartial<JobDetail>({ ...mockJobDetail, outputs: undefined })
)
await task.loadWorkflow(mockApp)
expect(mockApp.loadGraphData).toHaveBeenCalledWith(mockWorkflow)
expect(setOutputsSpy).not.toHaveBeenCalled()
expect(nodeOutputStore.nodeOutputs).toEqual({})
})
it('should skip invalid node execution ids while loading outputs', async () => {
const job = createHistoryJob('test-job-id')
const outputs = fromAny<TaskOutput, unknown>({
'': { images: [{ filename: 'skip.png', subfolder: '', type: 'output' }] },
'1': { images: [{ filename: 'keep.png', subfolder: '', type: 'output' }] }
})
const task = new TaskItemImpl(job, outputs)
const nodeOutputStore = useNodeOutputStore()
const setOutputsSpy = vi.spyOn(
nodeOutputStore,
'setNodeOutputsByExecutionId'
)
vi.spyOn(jobOutputCache, 'getJobDetail').mockResolvedValue(
fromPartial<JobDetail>({ ...mockJobDetail, outputs: undefined })
)
await task.loadWorkflow(mockApp)
expect(setOutputsSpy).toHaveBeenCalledOnce()
expect(setOutputsSpy).toHaveBeenCalledWith('1', outputs['1'])
expect(
nodeOutputStore.getNodeOutputByExecutionId(
createNodeExecutionId([toNodeId(1)])
)
).toEqual(outputs['1'])
expect(Object.keys(nodeOutputStore.nodeOutputs)).toEqual(['1'])
})
})

View File

@@ -1,4 +1,5 @@
import { createTestingPinia } from '@pinia/testing'
import { fromAny } from '@total-typescript/shoehorn'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -6,7 +7,14 @@ import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import type { TaskOutput } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { useExecutionStore } from '@/stores/executionStore'
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
import {
isInstantMode,
isInstantRunningMode,
ResultItemImpl,
TaskItemImpl,
useQueuePendingTaskCountStore,
useQueueStore
} from '@/stores/queueStore'
// Fixture factory for JobListItem
function createJob(
@@ -67,6 +75,86 @@ vi.mock('@/scripts/api', () => ({
}))
describe('TaskItemImpl', () => {
it('should default missing result URL fields', () => {
const output = new ResultItemImpl(
fromAny<ConstructorParameters<typeof ResultItemImpl>[0], unknown>({
nodeId: 'node-1',
mediaType: 'images'
})
)
expect(output.filename).toBe('')
expect(output.subfolder).toBe('')
expect(output.type).toBe('')
expect(output.url).toBe('')
})
it('should use the raw URL as preview URL for non-images', () => {
const output = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'video',
filename: 'clip.webm',
type: 'output',
subfolder: ''
})
expect(output.previewUrl).toBe(output.url)
})
it('should recognize VHS mp4 and unsupported video formats', () => {
const webm = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'gifs',
filename: 'clip',
type: 'output',
subfolder: '',
format: 'video/webm',
frame_rate: 24
})
const mp4 = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'gifs',
filename: 'clip',
type: 'output',
subfolder: '',
format: 'video/mp4',
frame_rate: 24
})
const avi = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'gifs',
filename: 'clip',
type: 'output',
subfolder: '',
format: 'video/avi',
frame_rate: 24
})
expect(webm.htmlVideoType).toBe('video/webm')
expect(mp4.htmlVideoType).toBe('video/mp4')
expect(avi.htmlVideoType).toBeUndefined()
})
it('should detect image media type without an image suffix', () => {
const image = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'images',
filename: 'generated',
type: 'output',
subfolder: ''
})
const audioFile = new ResultItemImpl({
nodeId: 'node-1',
mediaType: 'images',
filename: 'generated.wav',
type: 'output',
subfolder: ''
})
expect(image.isImage).toBe(true)
expect(audioFile.isImage).toBe(false)
})
it('should exclude animated from flatOutputs', () => {
const job = createHistoryJob(0, 'job-id')
const taskItem = new TaskItemImpl(job, {
@@ -259,6 +347,41 @@ describe('TaskItemImpl', () => {
expect(taskItem.executionError).toEqual(errorDetail)
})
})
it('should expose queue API task type for running tasks', () => {
const task = new TaskItemImpl(createRunningJob(1, 'run-1'))
expect(task.apiTaskType).toBe('queue')
})
it('should return empty flat outputs when outputs are missing', () => {
const task = new TaskItemImpl(
createHistoryJob(0, 'job-id'),
fromAny<TaskOutput, unknown>(null)
)
expect(task.calculateFlatOutputs()).toEqual([])
})
it('should calculate execution time in seconds', () => {
const task = new TaskItemImpl({
...createHistoryJob(0, 'job-id'),
execution_start_time: 1000,
execution_end_time: 3500
})
expect(task.executionStartTimestamp).toBe(1000)
expect(task.executionEndTimestamp).toBe(3500)
expect(task.executionTime).toBe(2500)
expect(task.executionTimeInSeconds).toBe(2.5)
})
it('should return undefined execution seconds without both timestamps', () => {
const task = new TaskItemImpl(createHistoryJob(0, 'job-id'))
expect(task.executionTime).toBeUndefined()
expect(task.executionTimeInSeconds).toBeUndefined()
})
})
describe('useQueueStore', () => {
@@ -314,6 +437,19 @@ describe('useQueueStore', () => {
expect(store.pendingTasks[1].jobId).toBe('pend-1')
})
it('should register workflow ids for active jobs', async () => {
const executionStore = useExecutionStore()
mockGetQueue.mockResolvedValue({
Running: [{ ...createRunningJob(1, 'run-1'), workflow_id: 'wf-1' }],
Pending: []
})
mockGetHistory.mockResolvedValue([])
await store.update()
expect(executionStore.jobIdToWorkflowId.get('run-1')).toBe('wf-1')
})
it('should load history tasks from API', async () => {
const historyJob1 = createHistoryJob(5, 'hist-1')
const historyJob2 = createHistoryJob(4, 'hist-2')
@@ -1115,3 +1251,43 @@ describe('useQueueStore', () => {
})
})
})
describe('useQueuePendingTaskCountStore', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('updates from status websocket messages', () => {
const store = useQueuePendingTaskCountStore()
store.update(
fromAny<CustomEvent, unknown>({
detail: { exec_info: { queue_remaining: 3 } }
})
)
expect(store.count).toBe(3)
})
it('falls back to zero when status details are missing', () => {
const store = useQueuePendingTaskCountStore()
store.count = 3
store.update(fromAny<CustomEvent, unknown>({}))
expect(store.count).toBe(0)
})
})
describe('queue mode helpers', () => {
it('detect instant queue modes', () => {
expect(isInstantMode('instant-idle')).toBe(true)
expect(isInstantMode('instant-running')).toBe(true)
expect(isInstantMode('change')).toBe(false)
})
it('detect instant running mode', () => {
expect(isInstantRunningMode('instant-running')).toBe(true)
expect(isInstantRunningMode('instant-idle')).toBe(false)
})
})