mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 17:52:16 +00:00
Compare commits
3 Commits
pr1-job-ou
...
update-his
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3019e484a | ||
|
|
75faab2a28 | ||
|
|
6dc75e2dcc |
@@ -34,17 +34,21 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setQueueIndex = (task: TaskItem) => {
|
const setQueueIndex = (task: TaskItem) => {
|
||||||
task.prompt[0] = TaskHistory.queueIndex++
|
task.prompt.priority = TaskHistory.queueIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPromptId = (task: TaskItem) => {
|
const setPromptId = (task: TaskItem) => {
|
||||||
task.prompt[1] = uuidv4()
|
task.prompt.prompt_id = uuidv4()
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TaskHistory {
|
export default class TaskHistory {
|
||||||
static queueIndex = 0
|
static queueIndex = 0
|
||||||
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
||||||
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
|
prompt: {
|
||||||
|
priority: 0,
|
||||||
|
prompt_id: 'prompt-id',
|
||||||
|
extra_data: { client_id: uuidv4() }
|
||||||
|
},
|
||||||
outputs: {},
|
outputs: {},
|
||||||
status: {
|
status: {
|
||||||
status_str: 'success',
|
status_str: 'success',
|
||||||
@@ -69,13 +73,13 @@ export default class TaskHistory {
|
|||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify(this.tasks)
|
body: JSON.stringify({ history: this.tasks })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleGetView(route: Route) {
|
private async handleGetView(route: Route) {
|
||||||
const fileName = getFilenameParam(route.request())
|
const fileName = getFilenameParam(route.request())
|
||||||
if (!this.outputContentTypes.has(fileName)) route.continue()
|
if (!this.outputContentTypes.has(fileName)) return route.continue()
|
||||||
|
|
||||||
const asset = this.loadAsset(fileName)
|
const asset = this.loadAsset(fileName)
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
@@ -91,7 +95,7 @@ export default class TaskHistory {
|
|||||||
|
|
||||||
async setupRoutes() {
|
async setupRoutes() {
|
||||||
return this.comfyPage.page.route(
|
return this.comfyPage.page.route(
|
||||||
/.*\/api\/(view|history)(\?.*)?$/,
|
/.*\/api\/(view|history|history_v2)(\?.*)?$/,
|
||||||
async (route) => {
|
async (route) => {
|
||||||
const request = route.request()
|
const request = route.request()
|
||||||
const method = request.method()
|
const method = request.method()
|
||||||
|
|||||||
@@ -187,12 +187,14 @@ test.describe('Workflows sidebar', () => {
|
|||||||
|
|
||||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow5.json'
|
'workflow5.json'
|
||||||
])
|
])
|
||||||
|
|
||||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
|
||||||
await comfyPage.confirmDialog.click('overwrite')
|
await comfyPage.confirmDialog.click('overwrite')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||||
'workflow5.json'
|
'workflow5.json'
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
|||||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||||
import { ComfyNode } from '@/schemas/comfyWorkflowSchema'
|
import { ComfyNode } from '@/schemas/comfyWorkflowSchema'
|
||||||
import { api } from '@/scripts/api'
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
|
||||||
import { useLitegraphService } from '@/services/litegraphService'
|
import { useLitegraphService } from '@/services/litegraphService'
|
||||||
|
import { useWorkflowService } from '@/services/workflowService'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import {
|
import {
|
||||||
ResultItemImpl,
|
ResultItemImpl,
|
||||||
@@ -126,6 +126,7 @@ const toast = useToast()
|
|||||||
const queueStore = useQueueStore()
|
const queueStore = useQueueStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
|
const workflowService = useWorkflowService()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// Expanded view: show all outputs in a flat list.
|
// Expanded view: show all outputs in a flat list.
|
||||||
@@ -208,8 +209,16 @@ const menuItems = computed<MenuItem[]>(() => {
|
|||||||
{
|
{
|
||||||
label: t('g.loadWorkflow'),
|
label: t('g.loadWorkflow'),
|
||||||
icon: 'pi pi-file-export',
|
icon: 'pi pi-file-export',
|
||||||
command: () => menuTargetTask.value?.loadWorkflow(app),
|
command: () => {
|
||||||
disabled: !menuTargetTask.value?.workflow
|
if (menuTargetTask.value) {
|
||||||
|
void workflowService.loadTaskWorkflow(menuTargetTask.value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
disabled: !(
|
||||||
|
menuTargetTask.value?.workflow ||
|
||||||
|
(menuTargetTask.value?.isHistory &&
|
||||||
|
menuTargetTask.value?.prompt.prompt_id)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('g.goToNode'),
|
label: t('g.goToNode'),
|
||||||
|
|||||||
@@ -134,13 +134,6 @@ export type DisplayComponentWsMessage = z.infer<
|
|||||||
>
|
>
|
||||||
// End of ws messages
|
// 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
|
const zExtraPngInfo = z
|
||||||
.object({
|
.object({
|
||||||
workflow: zComfyWorkflow
|
workflow: zComfyWorkflow
|
||||||
@@ -152,7 +145,6 @@ const zExtraData = z.object({
|
|||||||
extra_pnginfo: zExtraPngInfo.optional(),
|
extra_pnginfo: zExtraPngInfo.optional(),
|
||||||
client_id: z.string()
|
client_id: z.string()
|
||||||
})
|
})
|
||||||
const zOutputsToExecute = z.array(zNodeId)
|
|
||||||
|
|
||||||
const zExecutionStartMessage = z.tuple([
|
const zExecutionStartMessage = z.tuple([
|
||||||
z.literal('execution_start'),
|
z.literal('execution_start'),
|
||||||
@@ -193,13 +185,11 @@ const zStatus = z.object({
|
|||||||
messages: z.array(zStatusMessage)
|
messages: z.array(zStatusMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
const zTaskPrompt = z.tuple([
|
const zTaskPrompt = z.object({
|
||||||
zQueueIndex,
|
priority: zQueueIndex,
|
||||||
zPromptId,
|
prompt_id: zPromptId,
|
||||||
zPromptInputs,
|
extra_data: zExtraData
|
||||||
zExtraData,
|
})
|
||||||
zOutputsToExecute
|
|
||||||
])
|
|
||||||
|
|
||||||
const zRunningTaskItem = z.object({
|
const zRunningTaskItem = z.object({
|
||||||
taskType: z.literal('Running'),
|
taskType: z.literal('Running'),
|
||||||
@@ -235,6 +225,20 @@ const zHistoryTaskItem = z.object({
|
|||||||
meta: zTaskMeta.optional()
|
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([
|
const zTaskItem = z.union([
|
||||||
zRunningTaskItem,
|
zRunningTaskItem,
|
||||||
zPendingTaskItem,
|
zPendingTaskItem,
|
||||||
@@ -257,6 +261,8 @@ export type RunningTaskItem = z.infer<typeof zRunningTaskItem>
|
|||||||
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
|
export type PendingTaskItem = z.infer<typeof zPendingTaskItem>
|
||||||
// `/history`
|
// `/history`
|
||||||
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
|
export type HistoryTaskItem = z.infer<typeof zHistoryTaskItem>
|
||||||
|
export type RawHistoryItem = z.infer<typeof zRawHistoryItem>
|
||||||
|
export type HistoryResponse = z.infer<typeof zHistoryResponse>
|
||||||
export type TaskItem = z.infer<typeof zTaskItem>
|
export type TaskItem = z.infer<typeof zTaskItem>
|
||||||
|
|
||||||
export function validateTaskItem(taskItem: unknown) {
|
export function validateTaskItem(taskItem: unknown) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
ExecutionStartWsMessage,
|
ExecutionStartWsMessage,
|
||||||
ExecutionSuccessWsMessage,
|
ExecutionSuccessWsMessage,
|
||||||
ExtensionsResponse,
|
ExtensionsResponse,
|
||||||
|
HistoryResponse,
|
||||||
HistoryTaskItem,
|
HistoryTaskItem,
|
||||||
LogsRawResponse,
|
LogsRawResponse,
|
||||||
LogsWsMessage,
|
LogsWsMessage,
|
||||||
@@ -23,6 +24,7 @@ import type {
|
|||||||
StatusWsMessage,
|
StatusWsMessage,
|
||||||
StatusWsMessageStatus,
|
StatusWsMessageStatus,
|
||||||
SystemStats,
|
SystemStats,
|
||||||
|
TaskPrompt,
|
||||||
User,
|
User,
|
||||||
UserDataFullInfo
|
UserDataFullInfo
|
||||||
} from '@/schemas/apiSchema'
|
} from '@/schemas/apiSchema'
|
||||||
@@ -686,13 +688,12 @@ export class ComfyApi extends EventTarget {
|
|||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return {
|
return {
|
||||||
// Running action uses a different endpoint for cancelling
|
// Running action uses a different endpoint for cancelling
|
||||||
Running: data.queue_running.map((prompt: Record<number, any>) => ({
|
Running: data.queue_running.map((prompt: TaskPrompt) => ({
|
||||||
taskType: 'Running',
|
taskType: 'Running',
|
||||||
prompt,
|
prompt,
|
||||||
// prompt[1] is the prompt id
|
remove: { name: 'Cancel', cb: () => api.interrupt(prompt.prompt_id) }
|
||||||
remove: { name: 'Cancel', cb: () => api.interrupt(prompt[1]) }
|
|
||||||
})),
|
})),
|
||||||
Pending: data.queue_pending.map((prompt: Record<number, any>) => ({
|
Pending: data.queue_pending.map((prompt: TaskPrompt) => ({
|
||||||
taskType: 'Pending',
|
taskType: 'Pending',
|
||||||
prompt
|
prompt
|
||||||
}))
|
}))
|
||||||
@@ -711,13 +712,17 @@ export class ComfyApi extends EventTarget {
|
|||||||
max_items: number = 200
|
max_items: number = 200
|
||||||
): Promise<{ History: HistoryTaskItem[] }> {
|
): Promise<{ History: HistoryTaskItem[] }> {
|
||||||
try {
|
try {
|
||||||
const res = await this.fetchApi(`/history?max_items=${max_items}`)
|
const res = await this.fetchApi(`/history_v2?max_items=${max_items}`)
|
||||||
const json: Promise<HistoryTaskItem[]> = await res.json()
|
const json: HistoryResponse = await res.json()
|
||||||
|
|
||||||
|
// Extract history data from new format: { history: [{prompt_id: "...", ...}, ...] }
|
||||||
return {
|
return {
|
||||||
History: Object.values(json).map((item) => ({
|
History: json.history.map(
|
||||||
...item,
|
(item): HistoryTaskItem => ({
|
||||||
taskType: 'History'
|
...item,
|
||||||
}))
|
taskType: 'History'
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -725,6 +730,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<ComfyWorkflowJSON | null> {
|
||||||
|
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
|
* Gets system & device stats
|
||||||
* @returns System stats such as python version, OS, per device info
|
* @returns System stats such as python version, OS, per device info
|
||||||
|
|||||||
@@ -264,15 +264,16 @@ class ComfyList {
|
|||||||
? item.remove
|
? item.remove
|
||||||
: {
|
: {
|
||||||
name: 'Delete',
|
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', {
|
$el('button', {
|
||||||
textContent: 'Load',
|
textContent: 'Load',
|
||||||
onclick: async () => {
|
onclick: async () => {
|
||||||
await app.loadGraphData(
|
await app.loadGraphData(
|
||||||
// @ts-expect-error fixme ts strict error
|
// @ts-expect-error fixme ts strict error
|
||||||
item.prompt[3].extra_pnginfo.workflow,
|
item.prompt.extra_data.extra_pnginfo.workflow,
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { toRaw } from 'vue'
|
|||||||
|
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
import { ComfyWorkflowJSON } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
import { blankGraph, defaultGraph } from '@/scripts/defaultGraph'
|
||||||
import { downloadBlob } from '@/scripts/utils'
|
import { downloadBlob } from '@/scripts/utils'
|
||||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||||
|
import { TaskItemImpl } from '@/stores/queueStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
import { useToastStore } from '@/stores/toastStore'
|
import { useToastStore } from '@/stores/toastStore'
|
||||||
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
import { ComfyWorkflow, useWorkflowStore } from '@/stores/workflowStore'
|
||||||
@@ -152,6 +154,32 @@ export const useWorkflowService = () => {
|
|||||||
await app.loadGraphData(blankGraph)
|
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
|
* Reload the current workflow
|
||||||
* This is used to refresh the node definitions update, e.g. when the locale changes.
|
* This is used to refresh the node definitions update, e.g. when the locale changes.
|
||||||
@@ -394,6 +422,7 @@ export const useWorkflowService = () => {
|
|||||||
saveWorkflow,
|
saveWorkflow,
|
||||||
loadDefaultWorkflow,
|
loadDefaultWorkflow,
|
||||||
loadBlankWorkflow,
|
loadBlankWorkflow,
|
||||||
|
loadTaskWorkflow,
|
||||||
reloadCurrentWorkflow,
|
reloadCurrentWorkflow,
|
||||||
openWorkflow,
|
openWorkflow,
|
||||||
closeWorkflow,
|
closeWorkflow,
|
||||||
|
|||||||
@@ -269,23 +269,15 @@ export class TaskItemImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get queueIndex() {
|
get queueIndex() {
|
||||||
return this.prompt[0]
|
return this.prompt.priority
|
||||||
}
|
}
|
||||||
|
|
||||||
get promptId() {
|
get promptId() {
|
||||||
return this.prompt[1]
|
return this.prompt.prompt_id
|
||||||
}
|
|
||||||
|
|
||||||
get promptInputs() {
|
|
||||||
return this.prompt[2]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get extraData() {
|
get extraData() {
|
||||||
return this.prompt[3]
|
return this.prompt.extra_data
|
||||||
}
|
|
||||||
|
|
||||||
get outputsToExecute() {
|
|
||||||
return this.prompt[4]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get extraPngInfo() {
|
get extraPngInfo() {
|
||||||
@@ -390,13 +382,11 @@ export class TaskItemImpl {
|
|||||||
(output: ResultItemImpl, i: number) =>
|
(output: ResultItemImpl, i: number) =>
|
||||||
new TaskItemImpl(
|
new TaskItemImpl(
|
||||||
this.taskType,
|
this.taskType,
|
||||||
[
|
{
|
||||||
this.queueIndex,
|
priority: this.queueIndex,
|
||||||
`${this.promptId}-${i}`,
|
prompt_id: `${this.promptId}-${i}`,
|
||||||
this.promptInputs,
|
extra_data: this.extraData
|
||||||
this.extraData,
|
},
|
||||||
this.outputsToExecute
|
|
||||||
],
|
|
||||||
this.status,
|
this.status,
|
||||||
{
|
{
|
||||||
[output.nodeId]: {
|
[output.nodeId]: {
|
||||||
@@ -461,11 +451,11 @@ export const useQueueStore = defineStore('queue', () => {
|
|||||||
pendingTasks.value = toClassAll(queue.Pending)
|
pendingTasks.value = toClassAll(queue.Pending)
|
||||||
|
|
||||||
const allIndex = new Set<number>(
|
const allIndex = new Set<number>(
|
||||||
history.History.map((item: TaskItem) => item.prompt[0])
|
history.History.map((item: TaskItem) => item.prompt.priority)
|
||||||
)
|
)
|
||||||
const newHistoryItems = toClassAll(
|
const newHistoryItems = toClassAll(
|
||||||
history.History.filter(
|
history.History.filter(
|
||||||
(item) => item.prompt[0] > lastHistoryQueueIndex.value
|
(item) => item.prompt.priority > lastHistoryQueueIndex.value
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
const existingHistoryItems = historyTasks.value.filter((item) =>
|
const existingHistoryItems = historyTasks.value.filter((item) =>
|
||||||
|
|||||||
248
tests-ui/tests/scripts/api.test.ts
Normal file
248
tests-ui/tests/scripts/api.test.ts
Normal file
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,10 +3,94 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { TaskItemImpl } from '@/stores/queueStore'
|
import { TaskItemImpl } from '@/stores/queueStore'
|
||||||
|
|
||||||
describe('TaskItemImpl', () => {
|
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', () => {
|
it('should remove animated property from outputs during construction', () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
@@ -26,7 +110,11 @@ describe('TaskItemImpl', () => {
|
|||||||
it('should handle outputs without animated property', () => {
|
it('should handle outputs without animated property', () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
@@ -42,7 +130,11 @@ describe('TaskItemImpl', () => {
|
|||||||
it('should recognize webm video from core', () => {
|
it('should recognize webm video from core', () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
@@ -64,7 +156,11 @@ describe('TaskItemImpl', () => {
|
|||||||
it('should recognize webm video from VHS', () => {
|
it('should recognize webm video from VHS', () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
@@ -93,7 +189,11 @@ describe('TaskItemImpl', () => {
|
|||||||
it('should recognize mp4 video from core', () => {
|
it('should recognize mp4 video from core', () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
@@ -128,7 +228,11 @@ describe('TaskItemImpl', () => {
|
|||||||
it(`should recognize ${extension} audio`, () => {
|
it(`should recognize ${extension} audio`, () => {
|
||||||
const taskItem = new TaskItemImpl(
|
const taskItem = new TaskItemImpl(
|
||||||
'History',
|
'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 },
|
{ status_str: 'success', messages: [], completed: true },
|
||||||
{
|
{
|
||||||
'node-1': {
|
'node-1': {
|
||||||
|
|||||||
Reference in New Issue
Block a user