Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add queue overlay tests and stories (#7342)
## Summary
- add Playwright queue list fixture and coverage for toggle/count
display
- update queue overlay unit tests plus storybook stories for inline
progress and job item
- adjust useJobList expectations for ordered tasks

main <-- #7336 <--
#7338 <--
#7342

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7342-Add-queue-overlay-tests-and-stories-2c66d73d365081ae8e32d6e34f87e1d9)
by [Unito](https://www.unito.io)
  • Loading branch information
benceruleanlu authored Dec 14, 2025
commit 53dbca9fea29c5a446fa2725a0a195bfdbee797c
20 changes: 3 additions & 17 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -126,20 +127,6 @@ class ConfirmDialog {
const loc = this[locator]
await expect(loc).toBeVisible()
await loc.click()

// Wait for the dialog mask to disappear after confirming
const mask = this.page.locator('.p-dialog-mask')
const count = await mask.count()
if (count > 0) {
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
}

// Wait for workflow service to finish if it's busy
await this.page.waitForFunction(
() => window['app']?.extensionManager?.workflow?.isBusy === false,
undefined,
{ timeout: 3000 }
)
}
}

Expand All @@ -165,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
Expand Down Expand Up @@ -197,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)
Expand Down Expand Up @@ -256,9 +245,6 @@ export class ComfyPage {
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})

// Wait for Vue to re-render the workflow list
await this.nextFrame()
}

async setupUser(username: string) {
Expand Down
57 changes: 57 additions & 0 deletions browser_tests/fixtures/components/QueueList.ts
Original file line number Diff line number Diff line change
@@ -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}`)
}
}
6 changes: 5 additions & 1 deletion browser_tests/fixtures/ws.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { test as base } from '@playwright/test'

import type { StatusWsMessage } from '../../src/schemas/apiSchema'

export type WsMessage = { type: 'status'; data: StatusWsMessage }

export const webSocketFixture = base.extend<{
ws: { trigger(data: any, url?: string): Promise<void> }
ws: { trigger(data: WsMessage, url?: string): Promise<void> }
}>({
ws: [
async ({ page }, use) => {
Expand Down
12 changes: 7 additions & 5 deletions browser_tests/tests/actionbar.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'

import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
import { webSocketFixture } from '../fixtures/ws.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage'
import { webSocketFixture } from '../fixtures/ws'
import type { WsMessage } from '../fixtures/ws'

const test = mergeTests(comfyPageFixture, webSocketFixture)

Expand Down Expand Up @@ -61,7 +61,7 @@ test.describe('Actionbar', () => {

// Trigger a status websocket message
const triggerStatus = async (queueSize: number) => {
await ws.trigger({
const message = {
type: 'status',
data: {
status: {
Expand All @@ -70,7 +70,9 @@ test.describe('Actionbar', () => {
}
}
}
} as StatusWsMessage)
} satisfies WsMessage

await ws.trigger(message)
}

// Extract the width from the queue response
Expand Down
157 changes: 157 additions & 0 deletions browser_tests/tests/queue/queueList.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { expect, mergeTests } from '@playwright/test'

import type { ComfyPage } from '../../fixtures/ComfyPage'
import { comfyPageFixture } from '../../fixtures/ComfyPage'
import { webSocketFixture } from '../../fixtures/ws'
import type { WsMessage } from '../../fixtures/ws'

const test = mergeTests(comfyPageFixture, webSocketFixture)

type QueueState = {
running: QueueJob[]
pending: QueueJob[]
}

type QueueJob = [
string,
string,
Record<string, unknown>,
Record<string, unknown>,
string[]
]

type QueueController = {
state: QueueState
sync: (
ws: { trigger(data: WsMessage, url?: string): Promise<void> },
nextState: Partial<QueueState>
) => Promise<void>
}

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).toContainText('0')
await expect(comfyPage.queueList.toggleButton).toContainText(/queued/i)
await expect(comfyPage.queueList.overlay).toBeHidden()

await queue.sync(ws, {
pending: [queueJob('1', 'mock-pending', 'client-a')]
})

await expect(comfyPage.queueList.toggleButton).toContainText('1')
await expect(comfyPage.queueList.toggleButton).toContainText(/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<QueueController> => {
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: WsMessage, url?: string): Promise<void> },
nextState: Partial<QueueState>
) => {
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 } }
}
})

await queueResponse
}

return { state, sync }
}
Loading