Files
ComfyUI_frontend/src/composables/queue/useResultGallery.test.ts
ric-yu c0a649ef43 refactor: encapsulate error extraction in TaskItemImpl getters (#7650)
## Summary
- Add `errorMessage` and `executionError` getters to `TaskItemImpl` that
extract error info from status messages
- Update `useJobErrorReporting` composable to use these getters instead
of standalone function
- Remove the standalone `extractExecutionError` function

This encapsulates error extraction within `TaskItemImpl`, preparing for
the Jobs API migration where the underlying data format will change but
the getter interface will remain stable.

## Test plan
- [x] All existing tests pass
- [x] New tests added for `TaskItemImpl.errorMessage` and
`TaskItemImpl.executionError` getters
- [x] TypeScript, lint, and knip checks pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7650-refactor-encapsulate-error-extraction-in-TaskItemImpl-getters-2ce6d73d365081caae33dcc7e1e07720)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-01-15 21:11:22 -07:00

172 lines
5.1 KiB
TypeScript

import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { useResultGallery } from '@/composables/queue/useResultGallery'
import type { JobListItem as JobListViewItem } from '@/composables/queue/useJobList'
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
const createResultItem = (
url: string,
supportsPreview = true
): ResultItemImpl => {
const item = new ResultItemImpl({
filename: url,
subfolder: '',
type: 'output',
nodeId: 'node-1',
mediaType: supportsPreview ? 'images' : 'unknown'
})
// Override url getter for test matching
Object.defineProperty(item, 'url', { get: () => url })
Object.defineProperty(item, 'supportsPreview', { get: () => supportsPreview })
return item
}
const createMockJob = (id: string, outputsCount = 1): JobListItem => ({
id,
status: 'completed',
create_time: Date.now(),
preview_output: null,
outputs_count: outputsCount,
priority: 0
})
const createTask = (
preview?: ResultItemImpl,
allOutputs?: ResultItemImpl[],
outputsCount = 1
): TaskItemImpl => {
const job = createMockJob(
`task-${Math.random().toString(36).slice(2)}`,
outputsCount
)
const flatOutputs = allOutputs ?? (preview ? [preview] : [])
return new TaskItemImpl(job, {}, flatOutputs)
}
const createJobViewItem = (
id: string,
taskRef?: TaskItemImpl
): JobListViewItem =>
({
id,
title: `Job ${id}`,
meta: '',
state: 'completed',
showClear: false,
taskRef
}) as JobListViewItem
describe('useResultGallery', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('collects only previewable outputs and preserves their order', async () => {
const previewable = [createResultItem('p-1'), createResultItem('p-2')]
const nonPreviewable = createResultItem('skip-me', false)
const tasks = [
createTask(previewable[0]),
createTask(nonPreviewable),
createTask(previewable[1]),
createTask()
]
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => tasks
)
await onViewItem(createJobViewItem('job-1', tasks[0]))
expect(galleryItems.value).toEqual([previewable[0]])
expect(galleryActiveIndex.value).toBe(0)
})
it('does not change state when there are no previewable tasks', async () => {
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => []
)
await onViewItem(createJobViewItem('job-missing'))
expect(galleryItems.value).toEqual([])
expect(galleryActiveIndex.value).toBe(-1)
})
it('activates the index that matches the viewed preview URL', async () => {
const previewable = [
createResultItem('p-1'),
createResultItem('p-2'),
createResultItem('p-3')
]
const tasks = previewable.map((preview) => createTask(preview))
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => tasks
)
await onViewItem(createJobViewItem('job-2', tasks[1]))
expect(galleryItems.value).toEqual([previewable[1]])
expect(galleryActiveIndex.value).toBe(0)
})
it('defaults to the first entry when the clicked job lacks a preview', async () => {
const previewable = [createResultItem('p-1'), createResultItem('p-2')]
const tasks = previewable.map((preview) => createTask(preview))
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => tasks
)
await onViewItem(createJobViewItem('job-no-preview'))
expect(galleryItems.value).toEqual(previewable)
expect(galleryActiveIndex.value).toBe(0)
})
it('defaults to the first entry when no gallery item matches the preview URL', async () => {
const previewable = [createResultItem('p-1'), createResultItem('p-2')]
const tasks = previewable.map((preview) => createTask(preview))
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => tasks
)
const taskWithMismatchedPreview = createTask(createResultItem('missing'))
await onViewItem(
createJobViewItem('job-mismatch', taskWithMismatchedPreview)
)
expect(galleryItems.value).toEqual([createResultItem('missing')])
expect(galleryActiveIndex.value).toBe(0)
})
it('loads full outputs when task has only preview outputs', async () => {
const previewOutput = createResultItem('preview-1')
const fullOutputs = [
createResultItem('full-1'),
createResultItem('full-2'),
createResultItem('full-3')
]
// Create a task with outputsCount > 1 to trigger lazy loading
const job = createMockJob('task-1', 3)
const task = new TaskItemImpl(job, {}, [previewOutput])
// Mock loadFullOutputs to return full outputs
const loadedTask = new TaskItemImpl(job, {}, fullOutputs)
task.loadFullOutputs = async () => loadedTask
const { galleryItems, galleryActiveIndex, onViewItem } = useResultGallery(
() => [task]
)
await onViewItem(createJobViewItem('job-1', task))
expect(galleryItems.value).toEqual(fullOutputs)
expect(galleryActiveIndex.value).toBe(0)
})
})