diff --git a/browser_tests/fixtures/utils/taskHistory.ts b/browser_tests/fixtures/utils/taskHistory.ts index 29f63849e..db24139d5 100644 --- a/browser_tests/fixtures/utils/taskHistory.ts +++ b/browser_tests/fixtures/utils/taskHistory.ts @@ -73,7 +73,7 @@ export default class TaskHistory { return route.fulfill({ status: 200, contentType: 'application/json', - body: JSON.stringify(this.tasks) + body: JSON.stringify({ history: this.tasks }) }) } @@ -95,7 +95,7 @@ export default class TaskHistory { async setupRoutes() { return this.comfyPage.page.route( - /.*\/api\/(view|history)(\?.*)?$/, + /.*\/api\/(view|history|history_v2)(\?.*)?$/, async (route) => { const request = route.request() const method = request.method() diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index 9b853168f..30da01151 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -187,12 +187,14 @@ 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' ]) await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json') await comfyPage.confirmDialog.click('overwrite') + 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 a89f79440..a7e8b387a 100644 --- a/src/components/sidebar/tabs/QueueSidebarTab.vue +++ b/src/components/sidebar/tabs/QueueSidebarTab.vue @@ -214,12 +214,11 @@ const menuItems = computed(() => { void workflowService.loadTaskWorkflow(menuTargetTask.value) } }, - disabled: - !menuTargetTask.value?.workflow && - !( - menuTargetTask.value?.isHistory && - menuTargetTask.value?.prompt.prompt_id - ) + disabled: !( + menuTargetTask.value?.workflow || + (menuTargetTask.value?.isHistory && + menuTargetTask.value?.prompt.prompt_id) + ) }, { label: t('g.goToNode'), diff --git a/src/scripts/api.ts b/src/scripts/api.ts index bf00ee639..ece42968c 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -743,13 +743,13 @@ export class ComfyApi extends EventTarget { const json = await res.json() // The /history_v2/{prompt_id} endpoint returns data for a specific prompt - // The response format is: { prompt_id: { prompt: [...], outputs: {...}, status: {...} } } + // 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 array - // prompt[3] contains extra_data which has extra_pnginfo.workflow - const workflow = historyItem.prompt?.[3]?.extra_pnginfo?.workflow + // 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) diff --git a/tests-ui/tests/scripts/api.test.ts b/tests-ui/tests/scripts/api.test.ts index a4c11c208..e868fe51c 100644 --- a/tests-ui/tests/scripts/api.test.ts +++ b/tests-ui/tests/scripts/api.test.ts @@ -4,6 +4,7 @@ import type { HistoryResponse, RawHistoryItem } from '../../../src/schemas/apiSchema' +import type { ComfyWorkflowJSON } from '../../../src/schemas/comfyWorkflowSchema' import { ComfyApi } from '../../../src/scripts/api' describe('ComfyApi getHistory', () => { @@ -124,3 +125,124 @@ describe('ComfyApi getHistory', () => { }) }) }) + +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 2bfcc851c..f44a429b0 100644 --- a/tests-ui/tests/store/queueStore.test.ts +++ b/tests-ui/tests/store/queueStore.test.ts @@ -3,6 +3,86 @@ 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',