Files
ComfyUI_frontend/tests-ui/tests/composables/useCompletionSummary.test.ts
Benjamin Lu e42715086e Implement workflow progress panel (#6092)
Adds a workflow progress panel component underneath the
`actionbar-container`.

I suggest starting a review at the extraneous changes that were needed.
Including but not limited to:

- `get createTime()` in queueStore
- `promptIdToWorkflowId`, `initializingPromptIds`, and
`nodeProgressStatesByPrompt` in executionStore
- `create_time` handling in v2ToV1Adapter
- `pointer-events-auto` on ComfyActionbar.vue

The rest of the changes should be contained under
`QueueProgressOverlay.vue`, and has less of a blast radius in case
something goes wrong.

---------

Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-18 22:43:49 -08:00

290 lines
6.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, reactive } from 'vue'
import { useCompletionSummary } from '@/composables/queue/useCompletionSummary'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
type MockTask = {
displayStatus: 'Completed' | 'Failed' | 'Cancelled' | 'Running' | 'Pending'
executionEndTimestamp?: number
previewOutput?: {
isImage: boolean
urlWithTimestamp: string
}
}
vi.mock('@/stores/queueStore', () => {
const state = reactive({
runningTasks: [] as MockTask[],
historyTasks: [] as MockTask[]
})
return {
useQueueStore: () => state
}
})
vi.mock('@/stores/executionStore', () => {
const state = reactive({
isIdle: true
})
return {
useExecutionStore: () => state
}
})
describe('useCompletionSummary', () => {
const queueStore = () =>
useQueueStore() as {
runningTasks: MockTask[]
historyTasks: MockTask[]
}
const executionStore = () => useExecutionStore() as { isIdle: boolean }
const resetState = () => {
queueStore().runningTasks = []
queueStore().historyTasks = []
executionStore().isIdle = true
}
const createTask = (
options: {
state?: MockTask['displayStatus']
ts?: number
previewUrl?: string
isImage?: boolean
} = {}
): MockTask => {
const {
state = 'Completed',
ts = Date.now(),
previewUrl,
isImage = true
} = options
const task: MockTask = {
displayStatus: state,
executionEndTimestamp: ts
}
if (previewUrl) {
task.previewOutput = {
isImage,
urlWithTimestamp: previewUrl
}
}
return task
}
const runBatch = async (options: {
start: number
finish: number
tasks: MockTask[]
}) => {
const { start, finish, tasks } = options
vi.setSystemTime(start)
executionStore().isIdle = false
await nextTick()
vi.setSystemTime(finish)
queueStore().historyTasks = tasks
executionStore().isIdle = true
await nextTick()
}
beforeEach(() => {
resetState()
vi.useFakeTimers()
vi.setSystemTime(0)
})
afterEach(() => {
vi.runOnlyPendingTimers()
vi.useRealTimers()
resetState()
})
it('summarizes the most recent batch and auto clears after the dismiss delay', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 1_000
const finish = 2_000
const tasks = [
createTask({ ts: start - 100, previewUrl: 'ignored-old' }),
createTask({ ts: start + 10, previewUrl: 'img-1' }),
createTask({ ts: start + 20, previewUrl: 'img-2' }),
createTask({ ts: start + 30, previewUrl: 'img-3' }),
createTask({ ts: start + 40, previewUrl: 'img-4' }),
createTask({ state: 'Failed', ts: start + 50 })
]
await runBatch({ start, finish, tasks })
expect(summary.value).toEqual({
mode: 'mixed',
completedCount: 4,
failedCount: 1,
thumbnailUrls: ['img-1', 'img-2', 'img-3']
})
vi.advanceTimersByTime(6000)
await nextTick()
expect(summary.value).toBeNull()
})
it('reports allFailed when every task in the batch failed', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 10_000
const finish = 10_200
await runBatch({
start,
finish,
tasks: [
createTask({ state: 'Failed', ts: start + 25 }),
createTask({ state: 'Failed', ts: start + 50 })
]
})
expect(summary.value).toEqual({
mode: 'allFailed',
completedCount: 0,
failedCount: 2,
thumbnailUrls: []
})
})
it('treats cancelled tasks as failures and skips non-image previews', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 15_000
const finish = 15_200
await runBatch({
start,
finish,
tasks: [
createTask({ ts: start + 25, previewUrl: 'img-1' }),
createTask({
state: 'Cancelled',
ts: start + 50,
previewUrl: 'thumb-ignore',
isImage: false
})
]
})
expect(summary.value).toEqual({
mode: 'mixed',
completedCount: 1,
failedCount: 1,
thumbnailUrls: ['img-1']
})
})
it('clearSummary dismisses the banner immediately and still tracks future batches', async () => {
const { summary, clearSummary } = useCompletionSummary()
await nextTick()
await runBatch({
start: 5_000,
finish: 5_100,
tasks: [createTask({ ts: 5_050, previewUrl: 'img-1' })]
})
expect(summary.value).toEqual({
mode: 'allSuccess',
completedCount: 1,
failedCount: 0,
thumbnailUrls: ['img-1']
})
clearSummary()
expect(summary.value).toBeNull()
await runBatch({
start: 6_000,
finish: 6_150,
tasks: [createTask({ ts: 6_075, previewUrl: 'img-2' })]
})
expect(summary.value).toEqual({
mode: 'allSuccess',
completedCount: 1,
failedCount: 0,
thumbnailUrls: ['img-2']
})
})
it('ignores batches that have no finished tasks after the active period started', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 20_000
const finish = 20_500
await runBatch({
start,
finish,
tasks: [createTask({ ts: start - 1, previewUrl: 'too-early' })]
})
expect(summary.value).toBeNull()
})
it('derives the active period from running tasks when execution is already idle', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 25_000
vi.setSystemTime(start)
queueStore().runningTasks = [
createTask({ state: 'Running', ts: start + 1 })
]
await nextTick()
const finish = start + 150
vi.setSystemTime(finish)
queueStore().historyTasks = [
createTask({ ts: finish - 10, previewUrl: 'img-running-trigger' })
]
queueStore().runningTasks = []
await nextTick()
expect(summary.value).toEqual({
mode: 'allSuccess',
completedCount: 1,
failedCount: 0,
thumbnailUrls: ['img-running-trigger']
})
})
it('does not emit a summary when every finished task is still running or pending', async () => {
const { summary } = useCompletionSummary()
await nextTick()
const start = 30_000
const finish = 30_300
await runBatch({
start,
finish,
tasks: [
createTask({ state: 'Running', ts: start + 20 }),
createTask({ state: 'Pending', ts: start + 40 })
]
})
expect(summary.value).toBeNull()
})
})