From b42889206c3e766c088dd7d9d90e7a888a9ecbc7 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 9 Dec 2025 00:42:07 -0800 Subject: [PATCH] Add tests --- browser_tests/fixtures/ComfyPage.ts | 3 + .../fixtures/components/QueueList.ts | 57 +++++++ browser_tests/tests/queue/queueList.spec.ts | 156 ++++++++++++++++++ src/components/TopMenuSection.vue | 5 +- src/components/queue/QueueOverlayHeader.vue | 1 + src/components/queue/QueueProgressOverlay.vue | 1 + src/components/queue/job/QueueJobItem.vue | 7 + 7 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 browser_tests/fixtures/components/QueueList.ts create mode 100644 browser_tests/tests/queue/queueList.spec.ts diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index a9ae09dfe..df5b1e26a 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates' import { ComfyMouse } from './ComfyMouse' import { VueNodeHelpers } from './VueNodeHelpers' import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox' +import { QueueList } from './components/QueueList' import { SettingDialog } from './components/SettingDialog' import { NodeLibrarySidebarTab, @@ -151,6 +152,7 @@ export class ComfyPage { // Components public readonly searchBox: ComfyNodeSearchBox + public readonly queueList: QueueList public readonly menu: ComfyMenu public readonly actionbar: ComfyActionbar public readonly templates: ComfyTemplates @@ -183,6 +185,7 @@ export class ComfyPage { this.visibleToasts = page.locator('.p-toast-message:visible') this.searchBox = new ComfyNodeSearchBox(page) + this.queueList = new QueueList(page) this.menu = new ComfyMenu(page) this.actionbar = new ComfyActionbar(page) this.templates = new ComfyTemplates(page) diff --git a/browser_tests/fixtures/components/QueueList.ts b/browser_tests/fixtures/components/QueueList.ts new file mode 100644 index 000000000..453ef0765 --- /dev/null +++ b/browser_tests/fixtures/components/QueueList.ts @@ -0,0 +1,57 @@ +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +export class QueueList { + constructor(public readonly page: Page) {} + + get toggleButton() { + return this.page.getByTestId('queue-toggle-button') + } + + get inlineProgress() { + return this.page.getByTestId('queue-inline-progress') + } + + get overlay() { + return this.page.getByTestId('queue-overlay') + } + + get closeButton() { + return this.page.getByTestId('queue-overlay-close-button') + } + + get jobItems() { + return this.page.getByTestId('queue-job-item') + } + + get clearHistoryButton() { + return this.page.getByRole('button', { name: /Clear History/i }) + } + + async open() { + if (!(await this.overlay.isVisible())) { + await this.toggleButton.click() + await expect(this.overlay).toBeVisible() + } + } + + async close() { + if (await this.overlay.isVisible()) { + await this.closeButton.click() + await expect(this.overlay).not.toBeVisible() + } + } + + async getJobCount(state?: string) { + if (state) { + return await this.page + .locator(`[data-testid="queue-job-item"][data-job-state="${state}"]`) + .count() + } + return await this.jobItems.count() + } + + getJobAction(actionKey: string) { + return this.page.getByTestId(`job-action-${actionKey}`) + } +} diff --git a/browser_tests/tests/queue/queueList.spec.ts b/browser_tests/tests/queue/queueList.spec.ts new file mode 100644 index 000000000..979fbcabd --- /dev/null +++ b/browser_tests/tests/queue/queueList.spec.ts @@ -0,0 +1,156 @@ +import { expect, mergeTests } from '@playwright/test' + +import type { ComfyPage } from '../../fixtures/ComfyPage' +import type { StatusWsMessage } from '../../../src/schemas/apiSchema.ts' +import { comfyPageFixture } from '../../fixtures/ComfyPage' +import { webSocketFixture } from '../../fixtures/ws' + +const test = mergeTests(comfyPageFixture, webSocketFixture) + +type QueueState = { + running: QueueJob[] + pending: QueueJob[] +} + +type QueueJob = [ + string, + string, + Record, + Record, + string[] +] + +type QueueController = { + state: QueueState + sync: ( + ws: { trigger(data: any, url?: string): Promise }, + nextState: Partial + ) => Promise +} + +test.describe('Queue UI', () => { + let queue: QueueController + + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.page.route('**/api/prompt', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + prompt_id: 'mock-prompt-id', + number: 1, + node_errors: {} + }) + }) + }) + + // Mock history to avoid pulling real data + await comfyPage.page.route('**/api/history**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ History: [] }) + }) + }) + + queue = await createQueueController(comfyPage) + }) + + test('toggles overlay and updates count from status events', async ({ + comfyPage, + ws + }) => { + await queue.sync(ws, { running: [], pending: [] }) + + await expect(comfyPage.queueList.toggleButton).toBeVisible() + await expect(comfyPage.queueList.toggleButton).toHaveText(/0 queued/i) + await expect(comfyPage.queueList.overlay).toBeHidden() + + await queue.sync(ws, { + pending: [queueJob('1', 'mock-pending', 'client-a')] + }) + + await expect(comfyPage.queueList.toggleButton).toHaveText(/1 queued/i) + + await comfyPage.queueList.open() + await expect(comfyPage.queueList.overlay).toBeVisible() + await expect(comfyPage.queueList.jobItems).toHaveCount(1) + + await comfyPage.queueList.close() + await expect(comfyPage.queueList.overlay).toBeHidden() + }) + + test('displays running and pending jobs via status updates', async ({ + comfyPage, + ws + }) => { + await queue.sync(ws, { + running: [queueJob('2', 'mock-running', 'client-b')], + pending: [queueJob('3', 'mock-pending', 'client-c')] + }) + + await comfyPage.queueList.open() + await expect(comfyPage.queueList.jobItems).toHaveCount(2) + + const firstJob = comfyPage.queueList.jobItems.first() + await firstJob.hover() + + const cancelAction = firstJob + .getByTestId('job-action-cancel-running') + .or(firstJob.getByTestId('job-action-cancel-hover')) + + await expect(cancelAction).toBeVisible() + }) +}) + +const queueJob = ( + queueIndex: string, + promptId: string, + clientId: string +): QueueJob => [ + queueIndex, + promptId, + { client_id: clientId }, + { class_type: 'Note' }, + ['output'] +] + +const createQueueController = async ( + comfyPage: ComfyPage +): Promise => { + const state: QueueState = { running: [], pending: [] } + + // Single queue handler reads the latest in-memory state + await comfyPage.page.route('**/api/queue', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + queue_running: state.running, + queue_pending: state.pending + }) + }) + }) + + const sync = async ( + ws: { trigger(data: any, url?: string): Promise }, + nextState: Partial + ) => { + if (nextState.running) state.running = nextState.running + if (nextState.pending) state.pending = nextState.pending + + const total = state.running.length + state.pending.length + const queueResponse = comfyPage.page.waitForResponse('**/api/queue') + + await ws.trigger({ + type: 'status', + data: { + status: { exec_info: { queue_remaining: total } } + } + } as StatusWsMessage) + + await queueResponse + } + + return { state, sync } +} diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 38ffe6c3b..ec298082c 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -31,7 +31,10 @@ > - + diff --git a/src/components/queue/QueueOverlayHeader.vue b/src/components/queue/QueueOverlayHeader.vue index 365f4a3d9..eb6fff8c0 100644 --- a/src/components/queue/QueueOverlayHeader.vue +++ b/src/components/queue/QueueOverlayHeader.vue @@ -68,6 +68,7 @@ size="sm" class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100" :aria-label="t('g.close')" + data-testid="queue-overlay-close-button" @click="onCloseClick" > @@ -127,6 +131,7 @@ type="transparent" :label="action.label" :aria-label="action.ariaLabel" + :data-testid="`job-action-${action.key}`" @click.stop="action.onClick?.($event)" /> @@ -141,6 +146,7 @@ size="sm" :class="getActionButtonClass()" :aria-label="action.ariaLabel" + :data-testid="`job-action-${action.key}`" @click.stop="action.onClick?.($event)" > @@ -151,6 +157,7 @@ type="transparent" :label="action.label" :aria-label="action.ariaLabel" + :data-testid="`job-action-${action.key}`" @click.stop="action.onClick?.($event)" />