From 7649feb47f8165e1a84d8237d8e20720f7d2f0ff Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Mon, 14 Jul 2025 18:27:06 -0700 Subject: [PATCH] [feat] Update history API to v2 array format and add comprehensive tests - Migrate from object-based to array-based history response format - Update /history endpoint to /history_v2 with max_items parameter - Add lazy loading of workflows via /history_v2/:prompt_id endpoint - Implement comprehensive browser tests for history API functionality - Add unit tests for API methods and queue store - Update TaskItemImpl to support history workflow loading - Add proper error handling and edge case coverage - Follow established test patterns for better maintainability This change improves performance by reducing initial payload size and enables on-demand workflow loading for history items. --- browser_tests/fixtures/utils/taskHistory.ts | 43 ++- browser_tests/tests/historyApi.spec.ts | 131 +++++++++ browser_tests/tests/sidebar/workflows.spec.ts | 1 + .../sidebar/tabs/QueueSidebarTab.vue | 15 +- src/schemas/apiSchema.ts | 36 +-- src/scripts/api.ts | 49 +++- src/scripts/ui.ts | 7 +- src/services/workflowService.ts | 29 ++ src/stores/queueStore.ts | 30 +-- tests-ui/tests/scripts/api.test.ts | 248 ++++++++++++++++++ tests-ui/tests/store/queueStore.test.ts | 116 +++++++- 11 files changed, 645 insertions(+), 60 deletions(-) create mode 100644 browser_tests/tests/historyApi.spec.ts create mode 100644 tests-ui/tests/scripts/api.test.ts diff --git a/browser_tests/fixtures/utils/taskHistory.ts b/browser_tests/fixtures/utils/taskHistory.ts index 2c42c8492..9b09df14e 100644 --- a/browser_tests/fixtures/utils/taskHistory.ts +++ b/browser_tests/fixtures/utils/taskHistory.ts @@ -34,17 +34,23 @@ const getContentType = (filename: string, fileType: OutputFileType) => { } const setQueueIndex = (task: TaskItem) => { - task.prompt[0] = TaskHistory.queueIndex++ + task.prompt.priority = TaskHistory.queueIndex++ } const setPromptId = (task: TaskItem) => { - task.prompt[1] = uuidv4() + if (!task.prompt.prompt_id || task.prompt.prompt_id === 'prompt-id') { + task.prompt.prompt_id = uuidv4() + } } export default class TaskHistory { static queueIndex = 0 static readonly defaultTask: Readonly = { - prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []], + prompt: { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: uuidv4() } + }, outputs: {}, status: { status_str: 'success', @@ -66,10 +72,37 @@ export default class TaskHistory { ) private async handleGetHistory(route: Route) { + const url = route.request().url() + + // Handle history_v2/:prompt_id endpoint + const promptIdMatch = url.match(/history_v2\/([^?]+)/) + if (promptIdMatch) { + const promptId = promptIdMatch[1] + const task = this.tasks.find((t) => t.prompt.prompt_id === promptId) + const response: Record = {} + if (task) { + response[promptId] = task + } + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(response) + }) + } + + // Handle history_v2 list endpoint + // Convert HistoryTaskItem to RawHistoryItem format expected by API + const rawHistoryItems = this.tasks.map((task) => ({ + prompt_id: task.prompt.prompt_id, + prompt: task.prompt, + status: task.status, + outputs: task.outputs, + ...(task.meta && { meta: task.meta }) + })) return route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(this.tasks) + body: JSON.stringify({ history: rawHistoryItems }) }) } @@ -93,7 +126,7 @@ export default class TaskHistory { async setupRoutes() { return this.comfyPage.page.route( - /.*\/api\/(view|history)(\?.*)?$/, + /.*\/api\/(view|history_v2)(\/[^?]*)?(\?.*)?$/, async (route) => { const request = route.request() const method = request.method() diff --git a/browser_tests/tests/historyApi.spec.ts b/browser_tests/tests/historyApi.spec.ts new file mode 100644 index 000000000..2317aac6f --- /dev/null +++ b/browser_tests/tests/historyApi.spec.ts @@ -0,0 +1,131 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../fixtures/ComfyPage' + +test.describe('History API v2', () => { + const TEST_PROMPT_ID = 'test-prompt-id' + const TEST_CLIENT_ID = 'test-client' + + test('Can fetch history with new v2 format', async ({ comfyPage }) => { + // Set up mocked history with tasks + await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes() + + // Verify history_v2 API response format + const result = await comfyPage.page.evaluate(async () => { + try { + const response = await window['app'].api.getHistory() + return { success: true, data: response } + } catch (error) { + console.error('Failed to fetch history:', error) + return { success: false, error: error.message } + } + }) + + expect(result.success).toBe(true) + expect(result.data).toHaveProperty('History') + expect(Array.isArray(result.data.History)).toBe(true) + expect(result.data.History.length).toBeGreaterThan(0) + + const historyItem = result.data.History[0] + + // Verify the new prompt structure (object instead of array) + expect(historyItem.prompt).toHaveProperty('priority') + expect(historyItem.prompt).toHaveProperty('prompt_id') + expect(historyItem.prompt).toHaveProperty('extra_data') + expect(typeof historyItem.prompt.priority).toBe('number') + expect(typeof historyItem.prompt.prompt_id).toBe('string') + expect(historyItem.prompt.extra_data).toHaveProperty('client_id') + }) + + test('Can load workflow from history using history_v2 endpoint', async ({ + comfyPage + }) => { + // Simple mock workflow for testing + const mockWorkflow = { + version: 0.4, + nodes: [{ id: 1, type: 'TestNode', pos: [100, 100], size: [200, 100] }], + links: [], + groups: [], + config: {}, + extra: {} + } + + // Set up history with workflow data + await comfyPage + .setupHistory() + .withTask(['example.webp'], 'images', { + prompt: { + priority: 0, + prompt_id: TEST_PROMPT_ID, + extra_data: { + client_id: TEST_CLIENT_ID, + extra_pnginfo: { workflow: mockWorkflow } + } + } + }) + .setupRoutes() + + // Load initial workflow to clear canvas + await comfyPage.loadWorkflow('simple_slider') + await comfyPage.nextFrame() + + // Load workflow from history + const loadResult = await comfyPage.page.evaluate(async (promptId) => { + try { + const workflow = + await window['app'].api.getWorkflowFromHistory(promptId) + if (workflow) { + await window['app'].loadGraphData(workflow) + return { success: true } + } + return { success: false, error: 'No workflow found' } + } catch (error) { + console.error('Failed to load workflow from history:', error) + return { success: false, error: error.message } + } + }, TEST_PROMPT_ID) + + expect(loadResult.success).toBe(true) + + // Verify workflow loaded correctly + await comfyPage.nextFrame() + const nodeInfo = await comfyPage.page.evaluate(() => { + try { + const graph = window['app'].graph + return { + success: true, + nodeCount: graph.nodes?.length || 0, + firstNodeType: graph.nodes?.[0]?.type || null + } + } catch (error) { + return { success: false, error: error.message } + } + }) + + expect(nodeInfo.success).toBe(true) + expect(nodeInfo.nodeCount).toBe(1) + expect(nodeInfo.firstNodeType).toBe('TestNode') + }) + + test('Handles missing workflow data gracefully', async ({ comfyPage }) => { + // Set up empty history routes + await comfyPage.setupHistory().setupRoutes() + + // Test loading from history with invalid prompt_id + const result = await comfyPage.page.evaluate(async () => { + try { + const workflow = + await window['app'].api.getWorkflowFromHistory('invalid-id') + return { success: true, workflow } + } catch (error) { + console.error('Expected error for invalid prompt_id:', error) + return { success: false, error: error.message } + } + }) + + // Should handle gracefully without throwing + expect(result.success).toBe(true) + expect(result.workflow).toBeNull() + }) +}) diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index 1b3f21ff4..78d98a2f9 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -187,6 +187,7 @@ test.describe('Workflows sidebar', () => { test('Can save workflow as with same name', async ({ comfyPage }) => { await comfyPage.menu.topbar.saveWorkflow('workflow5.json') + await comfyPage.nextFrame() expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ 'workflow5.json' ]) diff --git a/src/components/sidebar/tabs/QueueSidebarTab.vue b/src/components/sidebar/tabs/QueueSidebarTab.vue index 6b0fe036c..a7e8b387a 100644 --- a/src/components/sidebar/tabs/QueueSidebarTab.vue +++ b/src/components/sidebar/tabs/QueueSidebarTab.vue @@ -106,8 +106,8 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import VirtualGrid from '@/components/common/VirtualGrid.vue' import { ComfyNode } from '@/schemas/comfyWorkflowSchema' import { api } from '@/scripts/api' -import { app } from '@/scripts/app' import { useLitegraphService } from '@/services/litegraphService' +import { useWorkflowService } from '@/services/workflowService' import { useCommandStore } from '@/stores/commandStore' import { ResultItemImpl, @@ -126,6 +126,7 @@ const toast = useToast() const queueStore = useQueueStore() const settingStore = useSettingStore() const commandStore = useCommandStore() +const workflowService = useWorkflowService() const { t } = useI18n() // Expanded view: show all outputs in a flat list. @@ -208,8 +209,16 @@ const menuItems = computed(() => { { label: t('g.loadWorkflow'), icon: 'pi pi-file-export', - command: () => menuTargetTask.value?.loadWorkflow(app), - disabled: !menuTargetTask.value?.workflow + command: () => { + if (menuTargetTask.value) { + void workflowService.loadTaskWorkflow(menuTargetTask.value) + } + }, + disabled: !( + menuTargetTask.value?.workflow || + (menuTargetTask.value?.isHistory && + menuTargetTask.value?.prompt.prompt_id) + ) }, { label: t('g.goToNode'), diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index ab854dead..b4852d50c 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -161,13 +161,6 @@ export type FeatureFlagsWsMessage = z.infer export type NotificationWsMessage = z.infer // End of ws messages -const zPromptInputItem = z.object({ - inputs: z.record(z.string(), z.any()), - class_type: zNodeType -}) - -const zPromptInputs = z.record(zPromptInputItem) - const zExtraPngInfo = z .object({ workflow: zComfyWorkflow @@ -179,7 +172,6 @@ const zExtraData = z.object({ extra_pnginfo: zExtraPngInfo.optional(), client_id: z.string() }) -const zOutputsToExecute = z.array(zNodeId) const zExecutionStartMessage = z.tuple([ z.literal('execution_start'), @@ -220,13 +212,11 @@ const zStatus = z.object({ messages: z.array(zStatusMessage) }) -const zTaskPrompt = z.tuple([ - zQueueIndex, - zPromptId, - zPromptInputs, - zExtraData, - zOutputsToExecute -]) +const zTaskPrompt = z.object({ + priority: zQueueIndex, + prompt_id: zPromptId, + extra_data: zExtraData +}) const zRunningTaskItem = z.object({ taskType: z.literal('Running'), @@ -262,6 +252,20 @@ const zHistoryTaskItem = z.object({ meta: zTaskMeta.optional() }) +// Raw history item from backend (without taskType) +const zRawHistoryItem = z.object({ + prompt_id: zPromptId, + prompt: zTaskPrompt, + status: zStatus.optional(), + outputs: zTaskOutput, + meta: zTaskMeta.optional() +}) + +// New API response format: { history: [{prompt_id: "...", ...}, ...] } +const zHistoryResponse = z.object({ + history: z.array(zRawHistoryItem) +}) + const zTaskItem = z.union([ zRunningTaskItem, zPendingTaskItem, @@ -284,6 +288,8 @@ export type RunningTaskItem = z.infer export type PendingTaskItem = z.infer // `/history` export type HistoryTaskItem = z.infer +export type RawHistoryItem = z.infer +export type HistoryResponse = z.infer export type TaskItem = z.infer export function validateTaskItem(taskItem: unknown) { diff --git a/src/scripts/api.ts b/src/scripts/api.ts index cc9eeb924..cb4e5694f 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -14,6 +14,7 @@ import type { ExecutionSuccessWsMessage, ExtensionsResponse, FeatureFlagsWsMessage, + HistoryResponse, HistoryTaskItem, LogsRawResponse, LogsWsMessage, @@ -28,6 +29,7 @@ import type { StatusWsMessage, StatusWsMessageStatus, SystemStats, + TaskPrompt, User, UserDataFullInfo } from '@/schemas/apiSchema' @@ -915,13 +917,13 @@ export class ComfyApi extends EventTarget { const data = await res.json() return { // Running action uses a different endpoint for cancelling - Running: data.queue_running.map((prompt: Record) => ({ + Running: data.queue_running.map((prompt: TaskPrompt) => ({ taskType: 'Running', prompt, // prompt[1] is the prompt id remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) } })), - Pending: data.queue_pending.map((prompt: Record) => ({ + Pending: data.queue_pending.map((prompt: TaskPrompt) => ({ taskType: 'Pending', prompt })) @@ -940,13 +942,17 @@ export class ComfyApi extends EventTarget { max_items: number = 200 ): Promise<{ History: HistoryTaskItem[] }> { try { - const res = await this.fetchApi(`/history?max_items=${max_items}`) - const json: Promise = await res.json() + const res = await this.fetchApi(`/history_v2?max_items=${max_items}`) + const json: HistoryResponse = await res.json() + + // Extract history data from new format: { history: [{prompt_id: "...", ...}, ...] } return { - History: Object.values(json).map((item) => ({ - ...item, - taskType: 'History' - })) + History: json.history.map( + (item): HistoryTaskItem => ({ + ...item, + taskType: 'History' + }) + ) } } catch (error) { console.error(error) @@ -954,6 +960,33 @@ export class ComfyApi extends EventTarget { } } + /** + * Gets workflow data for a specific prompt from history + * @param prompt_id The prompt ID to fetch workflow for + * @returns Workflow data for the specific prompt + */ + async getWorkflowFromHistory( + prompt_id: string + ): Promise { + try { + const res = await this.fetchApi(`/history_v2/${prompt_id}`) + const json = await res.json() + + // The /history_v2/{prompt_id} endpoint returns data for a specific prompt + // The response format is: { prompt_id: { prompt: {priority, prompt_id, extra_data}, outputs: {...}, status: {...} } } + const historyItem = json[prompt_id] + if (!historyItem) return null + + // Extract workflow from the prompt object + // prompt.extra_data contains extra_pnginfo.workflow + const workflow = historyItem.prompt?.extra_data?.extra_pnginfo?.workflow + return workflow || null + } catch (error) { + console.error(`Failed to fetch workflow for prompt ${prompt_id}:`, error) + return null + } + } + /** * Gets system & device stats * @returns System stats such as python version, OS, per device info diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts index b2d075665..56251f9d9 100644 --- a/src/scripts/ui.ts +++ b/src/scripts/ui.ts @@ -264,15 +264,16 @@ class ComfyList { ? item.remove : { name: 'Delete', - cb: () => api.deleteItem(this.#type, item.prompt[1]) + cb: () => + api.deleteItem(this.#type, item.prompt.prompt_id) } - return $el('div', { textContent: item.prompt[0] + ': ' }, [ + return $el('div', { textContent: item.prompt.priority + ': ' }, [ $el('button', { textContent: 'Load', onclick: async () => { await app.loadGraphData( // @ts-expect-error fixme ts strict error - item.prompt[3].extra_pnginfo.workflow, + item.prompt.extra_data.extra_pnginfo.workflow, true, false ) diff --git a/src/services/workflowService.ts b/src/services/workflowService.ts index e8ae09a30..6471bdb03 100644 --- a/src/services/workflowService.ts +++ b/src/services/workflowService.ts @@ -5,10 +5,12 @@ import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph' import type { SerialisableGraph, Vector2 } from '@/lib/litegraph/src/litegraph' import { useWorkflowThumbnail } from '@/renderer/thumbnail/composables/useWorkflowThumbnail' import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema' +import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { blankGraph, defaultGraph } from '@/scripts/defaultGraph' import { downloadBlob } from '@/scripts/utils' import { useDomWidgetStore } from '@/stores/domWidgetStore' +import { TaskItemImpl } from '@/stores/queueStore' import { useSettingStore } from '@/stores/settingStore' import { useToastStore } from '@/stores/toastStore' import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore' @@ -154,6 +156,32 @@ export const useWorkflowService = () => { await app.loadGraphData(blankGraph) } + /** + * Load a workflow from a task item (queue/history) + * For history items, fetches workflow data from /history_v2/{prompt_id} + * @param task The task item to load the workflow from + */ + const loadTaskWorkflow = async (task: TaskItemImpl) => { + let workflowData = task.workflow + + // History items don't include workflow data - fetch from API + if (task.isHistory) { + const promptId = task.prompt.prompt_id + if (promptId) { + workflowData = (await api.getWorkflowFromHistory(promptId)) || undefined + } + } + + if (!workflowData) { + return + } + + await app.loadGraphData(toRaw(workflowData)) + if (task.outputs) { + app.nodeOutputs = toRaw(task.outputs) + } + } + /** * Reload the current workflow * This is used to refresh the node definitions update, e.g. when the locale changes. @@ -402,6 +430,7 @@ export const useWorkflowService = () => { saveWorkflow, loadDefaultWorkflow, loadBlankWorkflow, + loadTaskWorkflow, reloadCurrentWorkflow, openWorkflow, closeWorkflow, diff --git a/src/stores/queueStore.ts b/src/stores/queueStore.ts index 7b1f4d60b..39c6f1a4e 100644 --- a/src/stores/queueStore.ts +++ b/src/stores/queueStore.ts @@ -276,23 +276,15 @@ export class TaskItemImpl { } get queueIndex() { - return this.prompt[0] + return this.prompt.priority } get promptId() { - return this.prompt[1] - } - - get promptInputs() { - return this.prompt[2] + return this.prompt.prompt_id } get extraData() { - return this.prompt[3] - } - - get outputsToExecute() { - return this.prompt[4] + return this.prompt.extra_data } get extraPngInfo() { @@ -408,13 +400,11 @@ export class TaskItemImpl { (output: ResultItemImpl, i: number) => new TaskItemImpl( this.taskType, - [ - this.queueIndex, - `${this.promptId}-${i}`, - this.promptInputs, - this.extraData, - this.outputsToExecute - ], + { + priority: this.queueIndex, + prompt_id: `${this.promptId}-${i}`, + extra_data: this.extraData + }, this.status, { [output.nodeId]: { @@ -479,11 +469,11 @@ export const useQueueStore = defineStore('queue', () => { pendingTasks.value = toClassAll(queue.Pending) const allIndex = new Set( - history.History.map((item: TaskItem) => item.prompt[0]) + history.History.map((item: TaskItem) => item.prompt.priority) ) const newHistoryItems = toClassAll( history.History.filter( - (item) => item.prompt[0] > lastHistoryQueueIndex.value + (item) => item.prompt.priority > lastHistoryQueueIndex.value ) ) const existingHistoryItems = historyTasks.value.filter((item) => diff --git a/tests-ui/tests/scripts/api.test.ts b/tests-ui/tests/scripts/api.test.ts new file mode 100644 index 000000000..e868fe51c --- /dev/null +++ b/tests-ui/tests/scripts/api.test.ts @@ -0,0 +1,248 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { + HistoryResponse, + RawHistoryItem +} from '../../../src/schemas/apiSchema' +import type { ComfyWorkflowJSON } from '../../../src/schemas/comfyWorkflowSchema' +import { ComfyApi } from '../../../src/scripts/api' + +describe('ComfyApi getHistory', () => { + let api: ComfyApi + + beforeEach(() => { + api = new ComfyApi() + }) + + const mockHistoryItem: RawHistoryItem = { + prompt_id: 'test_prompt_id', + prompt: { + priority: 0, + prompt_id: 'test_prompt_id', + extra_data: { + extra_pnginfo: { + workflow: { + last_node_id: 1, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0.4 + } + }, + client_id: 'test_client_id' + } + }, + outputs: {}, + status: { + status_str: 'success', + completed: true, + messages: [] + } + } + + describe('history v2 API format', () => { + it('should handle history array format from /history_v2', async () => { + const historyResponse: HistoryResponse = { + history: [ + { ...mockHistoryItem, prompt_id: 'prompt_id_1' }, + { ...mockHistoryItem, prompt_id: 'prompt_id_2' } + ] + } + + // Mock fetchApi to return the v2 format + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(historyResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getHistory(10) + + expect(result.History).toHaveLength(2) + expect(result.History[0]).toEqual({ + ...mockHistoryItem, + prompt_id: 'prompt_id_1', + taskType: 'History' + }) + expect(result.History[1]).toEqual({ + ...mockHistoryItem, + prompt_id: 'prompt_id_2', + taskType: 'History' + }) + }) + + it('should handle empty history array', async () => { + const historyResponse: HistoryResponse = { + history: [] + } + + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(historyResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getHistory(10) + + expect(result.History).toHaveLength(0) + expect(result.History).toEqual([]) + }) + }) + + describe('error handling', () => { + it('should return empty history on error', async () => { + const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error')) + api.fetchApi = mockFetchApi + + const result = await api.getHistory() + + expect(result.History).toEqual([]) + }) + }) + + describe('API call parameters', () => { + it('should call fetchApi with correct v2 endpoint and parameters', async () => { + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue({ history: [] }) + }) + api.fetchApi = mockFetchApi + + await api.getHistory(50) + + expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=50') + }) + + it('should use default max_items parameter with v2 endpoint', async () => { + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue({ history: [] }) + }) + api.fetchApi = mockFetchApi + + await api.getHistory() + + expect(mockFetchApi).toHaveBeenCalledWith('/history_v2?max_items=200') + }) + }) +}) + +describe('ComfyApi getWorkflowFromHistory', () => { + let api: ComfyApi + + beforeEach(() => { + api = new ComfyApi() + }) + + const mockWorkflow: ComfyWorkflowJSON = { + last_node_id: 1, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0.4 + } + + it('should fetch workflow data for a specific prompt', async () => { + const promptId = 'test_prompt_id' + const mockResponse = { + [promptId]: { + prompt: { + priority: 0, + prompt_id: promptId, + extra_data: { + extra_pnginfo: { + workflow: mockWorkflow + } + } + }, + outputs: {}, + status: { + status_str: 'success', + completed: true, + messages: [] + } + } + } + + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(mockResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getWorkflowFromHistory(promptId) + + expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`) + expect(result).toEqual(mockWorkflow) + }) + + it('should return null when prompt_id is not found', async () => { + const promptId = 'non_existent_prompt' + const mockResponse = {} + + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(mockResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getWorkflowFromHistory(promptId) + + expect(mockFetchApi).toHaveBeenCalledWith(`/history_v2/${promptId}`) + expect(result).toBeNull() + }) + + it('should return null when workflow data is missing', async () => { + const promptId = 'test_prompt_id' + const mockResponse = { + [promptId]: { + prompt: { + priority: 0, + prompt_id: promptId, + extra_data: {} + }, + outputs: {}, + status: { + status_str: 'success', + completed: true, + messages: [] + } + } + } + + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(mockResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getWorkflowFromHistory(promptId) + + expect(result).toBeNull() + }) + + it('should handle API errors gracefully', async () => { + const promptId = 'test_prompt_id' + const mockFetchApi = vi.fn().mockRejectedValue(new Error('Network error')) + api.fetchApi = mockFetchApi + + const result = await api.getWorkflowFromHistory(promptId) + + expect(result).toBeNull() + }) + + it('should handle malformed response gracefully', async () => { + const promptId = 'test_prompt_id' + const mockResponse = { + [promptId]: null + } + + const mockFetchApi = vi.fn().mockResolvedValue({ + json: vi.fn().mockResolvedValue(mockResponse) + }) + api.fetchApi = mockFetchApi + + const result = await api.getWorkflowFromHistory(promptId) + + expect(result).toBeNull() + }) +}) diff --git a/tests-ui/tests/store/queueStore.test.ts b/tests-ui/tests/store/queueStore.test.ts index 313673e69..f44a429b0 100644 --- a/tests-ui/tests/store/queueStore.test.ts +++ b/tests-ui/tests/store/queueStore.test.ts @@ -3,10 +3,94 @@ import { describe, expect, it } from 'vitest' import { TaskItemImpl } from '@/stores/queueStore' describe('TaskItemImpl', () => { + describe('prompt property accessors', () => { + it('should correctly access queueIndex from priority', () => { + const taskItem = new TaskItemImpl('Pending', { + priority: 5, + prompt_id: 'test-id', + extra_data: { client_id: 'client-id' } + }) + + expect(taskItem.queueIndex).toBe(5) + }) + + it('should correctly access promptId from prompt_id', () => { + const taskItem = new TaskItemImpl('History', { + priority: 0, + prompt_id: 'unique-prompt-id', + extra_data: { client_id: 'client-id' } + }) + + expect(taskItem.promptId).toBe('unique-prompt-id') + }) + + it('should correctly access extraData', () => { + const extraData = { + client_id: 'client-id', + extra_pnginfo: { + workflow: { + last_node_id: 1, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0.4 + } + } + } + const taskItem = new TaskItemImpl('Running', { + priority: 1, + prompt_id: 'test-id', + extra_data: extraData + }) + + expect(taskItem.extraData).toEqual(extraData) + }) + + it('should correctly access workflow from extraPngInfo', () => { + const workflow = { + last_node_id: 1, + last_link_id: 0, + nodes: [], + links: [], + groups: [], + config: {}, + extra: {}, + version: 0.4 + } + const taskItem = new TaskItemImpl('History', { + priority: 0, + prompt_id: 'test-id', + extra_data: { + client_id: 'client-id', + extra_pnginfo: { workflow } + } + }) + + expect(taskItem.workflow).toEqual(workflow) + }) + + it('should return undefined workflow when extraPngInfo is missing', () => { + const taskItem = new TaskItemImpl('History', { + priority: 0, + prompt_id: 'test-id', + extra_data: { client_id: 'client-id' } + }) + + expect(taskItem.workflow).toBeUndefined() + }) + }) + it('should remove animated property from outputs during construction', () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': { @@ -26,7 +110,11 @@ describe('TaskItemImpl', () => { it('should handle outputs without animated property', () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': { @@ -42,7 +130,11 @@ describe('TaskItemImpl', () => { it('should recognize webm video from core', () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': { @@ -64,7 +156,11 @@ describe('TaskItemImpl', () => { it('should recognize webm video from VHS', () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': { @@ -93,7 +189,11 @@ describe('TaskItemImpl', () => { it('should recognize mp4 video from core', () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': { @@ -128,7 +228,11 @@ describe('TaskItemImpl', () => { it(`should recognize ${extension} audio`, () => { const taskItem = new TaskItemImpl( 'History', - [0, 'prompt-id', {}, { client_id: 'client-id' }, []], + { + priority: 0, + prompt_id: 'prompt-id', + extra_data: { client_id: 'client-id' } + }, { status_str: 'success', messages: [], completed: true }, { 'node-1': {