mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-17 21:21:06 +00:00
Compare commits
2 Commits
test/asset
...
sort-histo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b3485237f | ||
|
|
21a988a342 |
@@ -712,11 +712,26 @@ export class ComfyApi extends EventTarget {
|
||||
try {
|
||||
const res = await this.fetchApi(`/history?max_items=${max_items}`)
|
||||
const json: Promise<HistoryTaskItem[]> = await res.json()
|
||||
const historyItems = Object.values(json).map((item) => ({
|
||||
...item,
|
||||
taskType: 'History'
|
||||
}))
|
||||
|
||||
// Sort by execution_start timestamp in descending order (newest first)
|
||||
historyItems.sort((a, b) => {
|
||||
const getExecutionStartTimestamp = (item: HistoryTaskItem) => {
|
||||
if (!item.status?.messages) return 0
|
||||
const executionStartMessage = item.status.messages.find(
|
||||
(msg) => msg[0] === 'execution_start'
|
||||
)
|
||||
return executionStartMessage ? executionStartMessage[1].timestamp : 0
|
||||
}
|
||||
|
||||
return getExecutionStartTimestamp(b) - getExecutionStartTimestamp(a)
|
||||
})
|
||||
|
||||
return {
|
||||
History: Object.values(json).map((item) => ({
|
||||
...item,
|
||||
taskType: 'History'
|
||||
}))
|
||||
History: historyItems
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
350
tests-ui/tests/scripts/api.test.ts
Normal file
350
tests-ui/tests/scripts/api.test.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ComfyApi } from '@/scripts/api'
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch
|
||||
|
||||
describe('ComfyApi', () => {
|
||||
let api: ComfyApi
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
api = new ComfyApi()
|
||||
api.api_base = '/test'
|
||||
})
|
||||
|
||||
describe('getHistory', () => {
|
||||
it('should return empty history when API call fails', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('API Error'))
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result).toEqual({ History: [] })
|
||||
})
|
||||
|
||||
it('should return unsorted history when no execution_start messages exist', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_success', { prompt_id: 'prompt-1', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
prompt: [2, 'prompt-2', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_success', { prompt_id: 'prompt-2', timestamp: 2000 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(2)
|
||||
expect(result.History[0]).toEqual({
|
||||
...mockHistoryData['1'],
|
||||
taskType: 'History'
|
||||
})
|
||||
expect(result.History[1]).toEqual({
|
||||
...mockHistoryData['2'],
|
||||
taskType: 'History'
|
||||
})
|
||||
})
|
||||
|
||||
it('should sort history by execution_start timestamp in descending order', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-1', timestamp: 1000 }],
|
||||
['execution_success', { prompt_id: 'prompt-1', timestamp: 1100 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
prompt: [2, 'prompt-2', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-2', timestamp: 3000 }],
|
||||
['execution_success', { prompt_id: 'prompt-2', timestamp: 3100 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'3': {
|
||||
prompt: [3, 'prompt-3', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-3', timestamp: 2000 }],
|
||||
['execution_success', { prompt_id: 'prompt-3', timestamp: 2100 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(3)
|
||||
// Should be sorted by execution_start timestamp: 3000, 2000, 1000
|
||||
expect(result.History[0].prompt[1]).toBe('prompt-2') // timestamp 3000
|
||||
expect(result.History[1].prompt[1]).toBe('prompt-3') // timestamp 2000
|
||||
expect(result.History[2].prompt[1]).toBe('prompt-1') // timestamp 1000
|
||||
})
|
||||
|
||||
it('should handle items without status or messages', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-1', timestamp: 2000 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
prompt: [2, 'prompt-2', {}],
|
||||
outputs: {}
|
||||
// No status field
|
||||
},
|
||||
'3': {
|
||||
prompt: [3, 'prompt-3', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [] // Empty messages array
|
||||
}
|
||||
},
|
||||
'4': {
|
||||
prompt: [4, 'prompt-4', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-4', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(4)
|
||||
// Items with execution_start should be sorted by timestamp (descending)
|
||||
// Items without execution_start should be sorted with timestamp 0 (appear last)
|
||||
expect(result.History[0].prompt[1]).toBe('prompt-1') // timestamp 2000
|
||||
expect(result.History[1].prompt[1]).toBe('prompt-4') // timestamp 1000
|
||||
// Items without execution_start timestamps should appear at the end
|
||||
expect(result.History[2].prompt[1]).toBe('prompt-2') // no status
|
||||
expect(result.History[3].prompt[1]).toBe('prompt-3') // empty messages
|
||||
})
|
||||
|
||||
it('should handle mixed message types and find execution_start', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_cached', { prompt_id: 'prompt-1', timestamp: 900 }],
|
||||
['execution_start', { prompt_id: 'prompt-1', timestamp: 1000 }],
|
||||
['execution_success', { prompt_id: 'prompt-1', timestamp: 1100 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
prompt: [2, 'prompt-2', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'error',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-2', timestamp: 2000 }],
|
||||
[
|
||||
'execution_error',
|
||||
{
|
||||
prompt_id: 'prompt-2',
|
||||
timestamp: 2100,
|
||||
node_id: 'node-1',
|
||||
node_type: 'TestNode',
|
||||
executed: [],
|
||||
exception_message: 'Test error',
|
||||
exception_type: 'ValueError',
|
||||
traceback: [],
|
||||
current_inputs: {},
|
||||
current_outputs: {}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(2)
|
||||
// Should be sorted by execution_start timestamp: 2000, 1000
|
||||
expect(result.History[0].prompt[1]).toBe('prompt-2') // timestamp 2000
|
||||
expect(result.History[1].prompt[1]).toBe('prompt-1') // timestamp 1000
|
||||
})
|
||||
|
||||
it('should respect max_items parameter', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': { prompt: [1, 'prompt-1', {}], outputs: {} },
|
||||
'2': { prompt: [2, 'prompt-2', {}], outputs: {} }
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
await api.getHistory(50)
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/test/api/history?max_items=50',
|
||||
expect.objectContaining({
|
||||
cache: 'no-cache',
|
||||
headers: expect.objectContaining({
|
||||
'Comfy-User': ''
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should use default max_items of 200', async () => {
|
||||
const mockHistoryData = {}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
await api.getHistory()
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'/test/api/history?max_items=200',
|
||||
expect.objectContaining({
|
||||
cache: 'no-cache'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should add taskType to all history items', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-1', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(1)
|
||||
expect(result.History[0].taskType).toBe('History')
|
||||
})
|
||||
|
||||
it('should handle empty history response', async () => {
|
||||
const mockHistoryData = {}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle identical timestamps consistently', async () => {
|
||||
const mockHistoryData = {
|
||||
'1': {
|
||||
prompt: [1, 'prompt-1', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-1', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
prompt: [2, 'prompt-2', {}],
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
completed: true,
|
||||
messages: [
|
||||
['execution_start', { prompt_id: 'prompt-2', timestamp: 1000 }]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve(mockHistoryData)
|
||||
})
|
||||
|
||||
const result = await api.getHistory()
|
||||
|
||||
expect(result.History).toHaveLength(2)
|
||||
// Both items should be present, order may vary for identical timestamps
|
||||
const promptIds = result.History.map((item) => item.prompt[1])
|
||||
expect(promptIds).toContain('prompt-1')
|
||||
expect(promptIds).toContain('prompt-2')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user