From 5b3485237f6d80f77e6c5a2f81aef5eee2f2ad60 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Wed, 9 Jul 2025 16:50:06 -0700 Subject: [PATCH] [test] Add comprehensive unit tests for getHistory sorting - Created tests for ComfyApi.getHistory method to verify execution_start timestamp sorting - Added tests for edge cases: missing status, empty messages, mixed message types - Verified proper error handling and parameter passing - Ensured backward compatibility with existing API interface - All 10 test cases pass successfully --- tests-ui/tests/scripts/api.test.ts | 350 +++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 tests-ui/tests/scripts/api.test.ts diff --git a/tests-ui/tests/scripts/api.test.ts b/tests-ui/tests/scripts/api.test.ts new file mode 100644 index 000000000..9f7e73e13 --- /dev/null +++ b/tests-ui/tests/scripts/api.test.ts @@ -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') + }) + }) +})