mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
Restore lost work from manager/menu-items-migration feature branch
This merge restores work that was lost during a bad rebase ~2 months ago.
Key restored functionality:
- ComfyUI Manager service with queue/task API endpoint
- Manager queue composable and WebSocket status handling
- Enhanced manager dialog commands and UI labels
- Manager websocket queue status enum types
- Server feature flag support functions
Resolves conflicts between recovery commit 730b278fa0 and
feature branch manager/menu-items-migration to combine all lost work.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
208
tests-ui/tests/api.featureFlags.test.ts
Normal file
208
tests-ui/tests/api.featureFlags.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
describe('API Feature Flags', () => {
|
||||
let mockWebSocket: any
|
||||
const wsEventHandlers: { [key: string]: (event: any) => void } = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Use fake timers
|
||||
vi.useFakeTimers()
|
||||
|
||||
// Mock WebSocket
|
||||
mockWebSocket = {
|
||||
readyState: 1, // WebSocket.OPEN
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
addEventListener: vi.fn(
|
||||
(event: string, handler: (event: any) => void) => {
|
||||
wsEventHandlers[event] = handler
|
||||
}
|
||||
),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
|
||||
// Mock WebSocket constructor
|
||||
global.WebSocket = vi.fn().mockImplementation(() => mockWebSocket) as any
|
||||
|
||||
// Reset API state
|
||||
api.serverFeatureFlags = {}
|
||||
|
||||
// Mock getClientFeatureFlags to return test feature flags
|
||||
vi.spyOn(api, 'getClientFeatureFlags').mockReturnValue({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('Feature flags negotiation', () => {
|
||||
it('should send client feature flags as first message on connection', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Check that feature flags were sent as first message
|
||||
expect(mockWebSocket.send).toHaveBeenCalledTimes(1)
|
||||
const sentMessage = JSON.parse(mockWebSocket.send.mock.calls[0][0])
|
||||
expect(sentMessage).toEqual({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
}
|
||||
})
|
||||
|
||||
// Simulate server response with status message
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate server feature flags response
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'feature_flags',
|
||||
data: {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Check that server features were stored
|
||||
expect(api.serverFeatureFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
async_execution: true,
|
||||
supported_formats: ['webp', 'jpeg', 'png'],
|
||||
api_version: '1.0.0',
|
||||
max_upload_size: 104857600,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle server without feature flags support', async () => {
|
||||
// Initialize API connection
|
||||
const initPromise = api.init()
|
||||
|
||||
// Simulate connection open
|
||||
wsEventHandlers['open'](new Event('open'))
|
||||
|
||||
// Clear the send mock to reset
|
||||
mockWebSocket.send.mockClear()
|
||||
|
||||
// Simulate server response with status but no feature flags
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: { exec_info: { queue_remaining: 0 } },
|
||||
sid: 'test-sid'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Simulate some other message (not feature flags)
|
||||
wsEventHandlers['message']({
|
||||
data: JSON.stringify({
|
||||
type: 'execution_start',
|
||||
data: {}
|
||||
})
|
||||
})
|
||||
|
||||
await initPromise
|
||||
|
||||
// Server features should remain empty
|
||||
expect(api.serverFeatureFlags).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Feature checking methods', () => {
|
||||
beforeEach(() => {
|
||||
// Set up some test features
|
||||
api.serverFeatureFlags = {
|
||||
supports_preview_metadata: true,
|
||||
async_execution: false,
|
||||
capabilities: ['isolated_nodes', 'dynamic_models']
|
||||
}
|
||||
})
|
||||
|
||||
it('should check if server supports a boolean feature', () => {
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.serverSupportsFeature('async_execution')).toBe(false)
|
||||
expect(api.serverSupportsFeature('non_existent_feature')).toBe(false)
|
||||
})
|
||||
|
||||
it('should get server feature value', () => {
|
||||
expect(api.getServerFeature('supports_preview_metadata')).toBe(true)
|
||||
expect(api.getServerFeature('capabilities')).toEqual([
|
||||
'isolated_nodes',
|
||||
'dynamic_models'
|
||||
])
|
||||
expect(api.getServerFeature('non_existent_feature')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Client feature flags configuration', () => {
|
||||
it('should use mocked client feature flags', () => {
|
||||
// Verify mocked flags are returned
|
||||
const clientFlags = api.getClientFeatureFlags()
|
||||
expect(clientFlags).toEqual({
|
||||
supports_preview_metadata: true,
|
||||
api_version: '1.0.0',
|
||||
capabilities: ['bulk_operations', 'async_nodes']
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a copy of client feature flags', () => {
|
||||
// Temporarily restore the real implementation for this test
|
||||
vi.mocked(api.getClientFeatureFlags).mockRestore()
|
||||
|
||||
// Verify that modifications to returned object don't affect original
|
||||
const clientFlags1 = api.getClientFeatureFlags()
|
||||
const clientFlags2 = api.getClientFeatureFlags()
|
||||
|
||||
// Should be different objects
|
||||
expect(clientFlags1).not.toBe(clientFlags2)
|
||||
|
||||
// But with same content
|
||||
expect(clientFlags1).toEqual(clientFlags2)
|
||||
|
||||
// Modifying one should not affect the other
|
||||
clientFlags1.test_flag = true
|
||||
expect(api.getClientFeatureFlags()).not.toHaveProperty('test_flag')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with preview messages', () => {
|
||||
it('should affect preview message handling based on feature support', () => {
|
||||
// Test with metadata support
|
||||
api.serverFeatureFlags = { supports_preview_metadata: true }
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(true)
|
||||
|
||||
// Test without metadata support
|
||||
api.serverFeatureFlags = {}
|
||||
expect(api.serverSupportsFeature('supports_preview_metadata')).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,598 +1,329 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
// Mock VueUse's useEventListener
|
||||
const mockEventListeners = new Map()
|
||||
const mockWheneverCallback = vi.fn()
|
||||
|
||||
vi.mock('@vueuse/core', async () => {
|
||||
const actual = await vi.importActual('@vueuse/core')
|
||||
return {
|
||||
...actual,
|
||||
useEventListener: vi.fn((target, event, handler) => {
|
||||
if (!mockEventListeners.has(event)) {
|
||||
mockEventListeners.set(event, [])
|
||||
}
|
||||
mockEventListeners.get(event).push(handler)
|
||||
|
||||
// Mock the addEventListener behavior
|
||||
if (target && target.addEventListener) {
|
||||
target.addEventListener(event, handler)
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
if (target && target.removeEventListener) {
|
||||
target.removeEventListener(event, handler)
|
||||
}
|
||||
}
|
||||
}),
|
||||
whenever: vi.fn((_source, cb) => {
|
||||
mockWheneverCallback.mockImplementation(cb)
|
||||
})
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
api: {
|
||||
clientId: 'test-client-id',
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/services/comfyManagerService', () => ({
|
||||
useComfyManagerService: vi.fn(() => ({
|
||||
getTaskQueue: vi.fn().mockResolvedValue({
|
||||
queue_running: [],
|
||||
queue_pending: []
|
||||
}),
|
||||
getTaskHistory: vi.fn().mockResolvedValue({}),
|
||||
clearTaskHistory: vi.fn().mockResolvedValue(null),
|
||||
deleteTaskHistoryItems: vi.fn().mockResolvedValue(null)
|
||||
}))
|
||||
}))
|
||||
|
||||
const mockShowManagerProgressDialog = vi.fn()
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: vi.fn(() => ({
|
||||
showManagerProgressDialog: mockShowManagerProgressDialog
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('useManagerQueue', () => {
|
||||
let taskHistory: any
|
||||
let taskQueue: any
|
||||
let installedPacks: any
|
||||
|
||||
// Helper functions
|
||||
const createMockTask = (
|
||||
id: string,
|
||||
clientId = 'test-client-id',
|
||||
additional = {}
|
||||
) => ({
|
||||
id,
|
||||
client_id: clientId,
|
||||
...additional
|
||||
const createMockTask = (result: any = 'result') => ({
|
||||
task: vi.fn().mockResolvedValue(result),
|
||||
onComplete: vi.fn()
|
||||
})
|
||||
|
||||
const createMockHistoryItem = (
|
||||
clientId = 'test-client-id',
|
||||
result = 'success',
|
||||
additional = {}
|
||||
) => ({
|
||||
client_id: clientId,
|
||||
result,
|
||||
...additional
|
||||
})
|
||||
|
||||
const createMockState = (overrides = {}) => ({
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
history: {},
|
||||
installed_packs: {},
|
||||
...overrides
|
||||
})
|
||||
|
||||
const triggerWebSocketEvent = (eventType: string, state: any) => {
|
||||
const mockEventListener = app.api.addEventListener as any
|
||||
const eventCall = mockEventListener.mock.calls.find(
|
||||
(call: any) => call[0] === eventType
|
||||
)
|
||||
|
||||
if (eventCall) {
|
||||
const handler = eventCall[1]
|
||||
handler({
|
||||
type: eventType,
|
||||
detail: { state }
|
||||
})
|
||||
}
|
||||
const createQueueWithMockTask = () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = createMockTask()
|
||||
queue.enqueueTask(mockTask)
|
||||
return { queue, mockTask }
|
||||
}
|
||||
|
||||
const getEventHandler = (eventType: string) => {
|
||||
const mockEventListener = app.api.addEventListener as any
|
||||
const eventCall = mockEventListener.mock.calls.find(
|
||||
(call: any) => call[0] === eventType
|
||||
)
|
||||
return eventCall ? eventCall[1] : null
|
||||
const getEventListenerCallback = () =>
|
||||
vi.mocked(api.addEventListener).mock.calls[0][1]
|
||||
|
||||
const simulateServerStatus = async (status: 'all-done' | 'in_progress') => {
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { status }
|
||||
})
|
||||
getEventListenerCallback()!(event as any)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventListeners.clear()
|
||||
taskHistory = ref({})
|
||||
taskQueue = ref({
|
||||
history: {},
|
||||
running_queue: [],
|
||||
pending_queue: [],
|
||||
installed_packs: {}
|
||||
})
|
||||
installedPacks = ref({})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockEventListeners.clear()
|
||||
})
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with empty queue and DONE status', () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
const queue = useManagerQueue()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should set up event listeners on creation', () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
expect(app.api.addEventListener).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('processing state handling', () => {
|
||||
it('should update processing state based on queue length', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
describe('queue management', () => {
|
||||
it('should add tasks to the queue', () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = createMockTask()
|
||||
|
||||
// Initially empty queue
|
||||
expect(queue.isProcessing.value).toBe(false)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Add tasks to queue
|
||||
taskQueue.value.running_queue = [createMockTask('task1')]
|
||||
taskQueue.value.pending_queue = [createMockTask('task2')]
|
||||
|
||||
// Force reactivity update
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should trigger progress dialog when queue length changes', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Trigger the whenever callback
|
||||
mockWheneverCallback()
|
||||
|
||||
expect(mockShowManagerProgressDialog).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('task state management', () => {
|
||||
it('should reflect task queue state changes', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Add running tasks
|
||||
taskQueue.value.running_queue = [createMockTask('task1')]
|
||||
taskQueue.value.pending_queue = [createMockTask('task2')]
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty queue state', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
taskQueue.value.running_queue = []
|
||||
taskQueue.value.pending_queue = []
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle large queue sizes', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Create large queues
|
||||
taskQueue.value.running_queue = Array(50)
|
||||
.fill(null)
|
||||
.map((_, i) => createMockTask(`running-${i}`))
|
||||
taskQueue.value.pending_queue = Array(100)
|
||||
.fill(null)
|
||||
.map((_, i) => createMockTask(`pending-${i}`))
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(150)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('queue data management', () => {
|
||||
it('should provide access to task queue state', async () => {
|
||||
const runningTasks = [createMockTask('task1')]
|
||||
const pendingTasks = [createMockTask('task2'), createMockTask('task3')]
|
||||
|
||||
taskQueue.value.running_queue = runningTasks
|
||||
taskQueue.value.pending_queue = pendingTasks
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskQueue.value.running_queue).toEqual(runningTasks)
|
||||
expect(queue.taskQueue.value.pending_queue).toEqual(pendingTasks)
|
||||
expect(queue.queueLength.value).toBe(3)
|
||||
})
|
||||
|
||||
it('should provide access to task history', async () => {
|
||||
const mockHistory = {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('test-client-id', 'error')
|
||||
}
|
||||
taskHistory.value = mockHistory
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskHistory.value).toEqual(mockHistory)
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle empty state gracefully', async () => {
|
||||
taskQueue.value.running_queue = []
|
||||
taskQueue.value.pending_queue = []
|
||||
taskHistory.value = {}
|
||||
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.historyCount.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('state management', () => {
|
||||
it('should provide reactive task history', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
taskHistory.value = {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('test-client-id', 'error')
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.taskHistory.value).toEqual(taskHistory.value)
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should provide reactive installed packs', async () => {
|
||||
installedPacks.value = {
|
||||
pack1: { version: '1.0' },
|
||||
pack2: { version: '2.0' }
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// The composable should have access to installedPacks through the parameter
|
||||
expect(installedPacks.value).toEqual({
|
||||
pack1: { version: '1.0' },
|
||||
pack2: { version: '2.0' }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should correctly compute allTasksDone', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Empty queue = all done
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
|
||||
// Add pending tasks
|
||||
taskQueue.value.pending_queue = [createMockTask('task1')]
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
|
||||
// Clear queue
|
||||
taskQueue.value.running_queue = []
|
||||
taskQueue.value.pending_queue = []
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should correctly compute queueLength', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
|
||||
taskQueue.value.running_queue = [createMockTask('task1')]
|
||||
taskQueue.value.pending_queue = [
|
||||
createMockTask('task2'),
|
||||
createMockTask('task3')
|
||||
]
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(queue.queueLength.value).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('client filtering functionality', () => {
|
||||
it('should filter tasks by client ID in WebSocket events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1'),
|
||||
createMockTask('task2', 'other-client-id')
|
||||
],
|
||||
pending_queue: [createMockTask('task3')]
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include tasks from this client
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task3')])
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should filter history by client ID in WebSocket events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
history: {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('other-client-id'),
|
||||
task3: createMockHistoryItem()
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include history items from this client
|
||||
expect(Object.keys(taskHistory.value)).toHaveLength(2)
|
||||
expect(taskHistory.value).toHaveProperty('task1')
|
||||
expect(taskHistory.value).toHaveProperty('task3')
|
||||
expect(taskHistory.value).not.toHaveProperty('task2')
|
||||
expect(queue.historyCount.value).toBe(2)
|
||||
})
|
||||
|
||||
it('should handle all tasks being from other clients', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1', 'other-client-1'),
|
||||
createMockTask('task2', 'other-client-2')
|
||||
],
|
||||
pending_queue: [createMockTask('task3', 'other-client-1')],
|
||||
history: {
|
||||
task4: createMockHistoryItem('other-client-1'),
|
||||
task5: createMockHistoryItem('other-client-2')
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should have no tasks or history
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({})
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.historyCount.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('WebSocket event handling', () => {
|
||||
it('should handle task done events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
history: {
|
||||
task1: createMockHistoryItem()
|
||||
},
|
||||
installed_packs: { pack1: { version: '1.0' } }
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({
|
||||
task1: createMockHistoryItem()
|
||||
})
|
||||
expect(installedPacks.value).toEqual({ pack1: { version: '1.0' } })
|
||||
})
|
||||
|
||||
it('should handle task started events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
pending_queue: [createMockTask('task2')],
|
||||
installed_packs: { pack1: { version: '1.0' } }
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-started', mockState)
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task2')])
|
||||
expect(installedPacks.value).toEqual({ pack1: { version: '1.0' } })
|
||||
})
|
||||
|
||||
it('should filter out tasks from other clients in WebSocket events', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [
|
||||
createMockTask('task1'),
|
||||
createMockTask('task2', 'other-client-id')
|
||||
],
|
||||
pending_queue: [createMockTask('task3', 'other-client-id')],
|
||||
history: {
|
||||
task1: createMockHistoryItem(),
|
||||
task2: createMockHistoryItem('other-client-id')
|
||||
}
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should only include tasks from this client
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task1')])
|
||||
expect(taskQueue.value.pending_queue).toEqual([])
|
||||
expect(taskHistory.value).toEqual({
|
||||
task1: createMockHistoryItem()
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore events with wrong type', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const handler = getEventHandler('cm-task-completed')
|
||||
|
||||
// Send event with wrong type
|
||||
handler({
|
||||
type: 'wrong-event-type',
|
||||
detail: {
|
||||
state: createMockState({ running_queue: [createMockTask('task1')] })
|
||||
}
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
// Should not update state
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanup functionality', () => {
|
||||
it('should clean up event listeners on stopListening', () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
const mockRemoveEventListener = app.api.removeEventListener as any
|
||||
|
||||
queue.stopListening()
|
||||
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Check that both event types were called with the correct event names
|
||||
const calls = mockRemoveEventListener.mock.calls
|
||||
const eventTypes = calls.map((call: any) => call[0])
|
||||
expect(eventTypes).toContain('cm-task-completed')
|
||||
expect(eventTypes).toContain('cm-task-started')
|
||||
|
||||
// Check that functions were passed as second parameter
|
||||
calls.forEach((call: any) => {
|
||||
expect(typeof call[1]).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple stopListening calls gracefully', () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
const mockRemoveEventListener = app.api.removeEventListener as any
|
||||
|
||||
queue.stopListening()
|
||||
queue.stopListening()
|
||||
|
||||
// Should still only be called twice (once per event type)
|
||||
expect(mockRemoveEventListener).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle undefined installed_packs in state update', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
const mockState = createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
installed_packs: undefined
|
||||
})
|
||||
|
||||
triggerWebSocketEvent('cm-task-completed', mockState)
|
||||
await nextTick()
|
||||
|
||||
// Should not update installedPacks when undefined
|
||||
expect(installedPacks.value).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle rapid successive events', async () => {
|
||||
const queue = useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
|
||||
// Send multiple events rapidly
|
||||
for (let i = 0; i < 10; i++) {
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-completed',
|
||||
createMockState({
|
||||
running_queue: [createMockTask(`task${i}`)],
|
||||
history: { [`task${i}`]: createMockHistoryItem() }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Should have the last state
|
||||
expect(taskQueue.value.running_queue).toEqual([createMockTask('task9')])
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should maintain consistency when mixing event types', async () => {
|
||||
useManagerQueue(taskHistory, taskQueue, installedPacks)
|
||||
it('should clear the queue when clearQueue is called', () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Send alternating event types
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-started',
|
||||
createMockState({
|
||||
running_queue: [createMockTask('task1')],
|
||||
pending_queue: [createMockTask('task2')]
|
||||
})
|
||||
)
|
||||
// Add some tasks
|
||||
queue.enqueueTask(createMockTask())
|
||||
queue.enqueueTask(createMockTask())
|
||||
|
||||
triggerWebSocketEvent(
|
||||
'cm-task-completed',
|
||||
createMockState({
|
||||
running_queue: [],
|
||||
pending_queue: [createMockTask('task2')],
|
||||
history: { task1: createMockHistoryItem() }
|
||||
})
|
||||
)
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
// Clear the queue
|
||||
queue.clearQueue()
|
||||
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('server status handling', () => {
|
||||
it('should update server status when receiving websocket events', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
await simulateServerStatus('in_progress')
|
||||
|
||||
expect(queue.statusMessage.value).toBe('in_progress')
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle invalid status values gracefully', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Simulate an invalid status
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: null as any
|
||||
})
|
||||
|
||||
getEventListenerCallback()!(event)
|
||||
await nextTick()
|
||||
|
||||
expect(taskQueue.value.running_queue).toEqual([])
|
||||
expect(taskQueue.value.pending_queue).toEqual([createMockTask('task2')])
|
||||
expect(taskHistory.value).toHaveProperty('task1')
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
|
||||
it('should handle missing status property gracefully', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Simulate a detail object without status property
|
||||
const event = new CustomEvent('cm-queue-status', {
|
||||
detail: { someOtherProperty: 'value' } as any
|
||||
})
|
||||
|
||||
getEventListenerCallback()!(event)
|
||||
await nextTick()
|
||||
|
||||
// Should maintain the default status
|
||||
expect(queue.statusMessage.value).toBe('all-done')
|
||||
})
|
||||
})
|
||||
|
||||
describe('task execution', () => {
|
||||
it('should start the next task when server is idle and queue has items', async () => {
|
||||
const { queue, mockTask } = createQueueWithMockTask()
|
||||
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Task should have been started
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
})
|
||||
|
||||
it('should execute onComplete callback when task completes and server becomes idle', async () => {
|
||||
const { mockTask } = createQueueWithMockTask()
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
await mockTask.task.mock.results[0].value
|
||||
|
||||
// Simulate server cycle (in_progress -> done)
|
||||
await simulateServerStatus('in_progress')
|
||||
expect(mockTask.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle tasks without onComplete callback', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = { task: vi.fn().mockResolvedValue('result') }
|
||||
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Simulate task completion
|
||||
await mockTask.task.mock.results[0].value
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not throw errors even without onComplete
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should process multiple tasks in sequence', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask1 = createMockTask('result1')
|
||||
const mockTask2 = createMockTask('result2')
|
||||
|
||||
// Add tasks to the queue
|
||||
queue.enqueueTask(mockTask1)
|
||||
queue.enqueueTask(mockTask2)
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
|
||||
// Process first task
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Process second task
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
|
||||
// Complete second task
|
||||
await mockTask2.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Queue should be empty and all tasks done
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle task that returns rejected promise', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask = {
|
||||
task: vi.fn().mockRejectedValue(new Error('Task failed')),
|
||||
onComplete: vi.fn()
|
||||
}
|
||||
|
||||
queue.enqueueTask(mockTask)
|
||||
|
||||
// Start the task
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask.task).toHaveBeenCalled()
|
||||
|
||||
// Let the promise rejection happen
|
||||
try {
|
||||
await mockTask.task()
|
||||
} catch (e) {
|
||||
// Ignore the error
|
||||
}
|
||||
|
||||
// Simulate server cycle
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// onComplete should still be called for failed tasks
|
||||
expect(mockTask.onComplete).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle multiple multiple tasks enqueued at once while server busy', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask1 = createMockTask()
|
||||
const mockTask2 = createMockTask()
|
||||
const mockTask3 = createMockTask()
|
||||
|
||||
// Three tasks enqueued at once
|
||||
await simulateServerStatus('in_progress')
|
||||
await Promise.all([
|
||||
queue.enqueueTask(mockTask1),
|
||||
queue.enqueueTask(mockTask2),
|
||||
queue.enqueueTask(mockTask3)
|
||||
])
|
||||
|
||||
// Task 1
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask1.onComplete).toHaveBeenCalled()
|
||||
expect(mockTask2.onComplete).not.toHaveBeenCalled()
|
||||
expect(mockTask3.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
// Verify state of queue
|
||||
expect(queue.queueLength.value).toBe(2)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
|
||||
// Task 2
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask2.onComplete).toHaveBeenCalled()
|
||||
expect(mockTask3.onComplete).not.toHaveBeenCalled()
|
||||
|
||||
// Verify state of queue
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
expect(queue.allTasksDone.value).toBe(false)
|
||||
|
||||
// Task 3
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Verify state of onComplete callbacks
|
||||
expect(mockTask3.task).toHaveBeenCalled()
|
||||
expect(mockTask3.onComplete).toHaveBeenCalled()
|
||||
|
||||
// Verify state of queue
|
||||
expect(queue.queueLength.value).toBe(0)
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle adding tasks while processing is in progress', async () => {
|
||||
const queue = useManagerQueue()
|
||||
const mockTask1 = createMockTask()
|
||||
const mockTask2 = createMockTask()
|
||||
|
||||
// Add first task and start processing
|
||||
queue.enqueueTask(mockTask1)
|
||||
await simulateServerStatus('all-done')
|
||||
expect(mockTask1.task).toHaveBeenCalled()
|
||||
|
||||
// Add second task while first is processing
|
||||
queue.enqueueTask(mockTask2)
|
||||
expect(queue.queueLength.value).toBe(1)
|
||||
|
||||
// Complete first task
|
||||
await mockTask1.task.mock.results[0].value
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Second task should now be processed
|
||||
expect(mockTask2.task).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle server status changes without tasks in queue', async () => {
|
||||
const queue = useManagerQueue()
|
||||
|
||||
// Cycle server status without any tasks
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
await simulateServerStatus('in_progress')
|
||||
await simulateServerStatus('all-done')
|
||||
|
||||
// Should not cause any errors
|
||||
expect(queue.allTasksDone.value).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user