Skip to content

Commit 53dbca9

Browse files
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)
1 parent 18303fa commit 53dbca9

File tree

9 files changed

+456
-29
lines changed

9 files changed

+456
-29
lines changed

browser_tests/fixtures/ComfyPage.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
1313
import { ComfyMouse } from './ComfyMouse'
1414
import { VueNodeHelpers } from './VueNodeHelpers'
1515
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
16+
import { QueueList } from './components/QueueList'
1617
import { SettingDialog } from './components/SettingDialog'
1718
import {
1819
NodeLibrarySidebarTab,
@@ -126,20 +127,6 @@ class ConfirmDialog {
126127
const loc = this[locator]
127128
await expect(loc).toBeVisible()
128129
await loc.click()
129-
130-
// Wait for the dialog mask to disappear after confirming
131-
const mask = this.page.locator('.p-dialog-mask')
132-
const count = await mask.count()
133-
if (count > 0) {
134-
await mask.first().waitFor({ state: 'hidden', timeout: 3000 })
135-
}
136-
137-
// Wait for workflow service to finish if it's busy
138-
await this.page.waitForFunction(
139-
() => window['app']?.extensionManager?.workflow?.isBusy === false,
140-
undefined,
141-
{ timeout: 3000 }
142-
)
143130
}
144131
}
145132

@@ -165,6 +152,7 @@ export class ComfyPage {
165152

166153
// Components
167154
public readonly searchBox: ComfyNodeSearchBox
155+
public readonly queueList: QueueList
168156
public readonly menu: ComfyMenu
169157
public readonly actionbar: ComfyActionbar
170158
public readonly templates: ComfyTemplates
@@ -197,6 +185,7 @@ export class ComfyPage {
197185
this.visibleToasts = page.locator('.p-toast-message:visible')
198186

199187
this.searchBox = new ComfyNodeSearchBox(page)
188+
this.queueList = new QueueList(page)
200189
this.menu = new ComfyMenu(page)
201190
this.actionbar = new ComfyActionbar(page)
202191
this.templates = new ComfyTemplates(page)
@@ -256,9 +245,6 @@ export class ComfyPage {
256245
await this.page.evaluate(async () => {
257246
await window['app'].extensionManager.workflow.syncWorkflows()
258247
})
259-
260-
// Wait for Vue to re-render the workflow list
261-
await this.nextFrame()
262248
}
263249

264250
async setupUser(username: string) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
export class QueueList {
5+
constructor(public readonly page: Page) {}
6+
7+
get toggleButton() {
8+
return this.page.getByTestId('queue-toggle-button')
9+
}
10+
11+
get inlineProgress() {
12+
return this.page.getByTestId('queue-inline-progress')
13+
}
14+
15+
get overlay() {
16+
return this.page.getByTestId('queue-overlay')
17+
}
18+
19+
get closeButton() {
20+
return this.page.getByTestId('queue-overlay-close-button')
21+
}
22+
23+
get jobItems() {
24+
return this.page.getByTestId('queue-job-item')
25+
}
26+
27+
get clearHistoryButton() {
28+
return this.page.getByRole('button', { name: /Clear History/i })
29+
}
30+
31+
async open() {
32+
if (!(await this.overlay.isVisible())) {
33+
await this.toggleButton.click()
34+
await expect(this.overlay).toBeVisible()
35+
}
36+
}
37+
38+
async close() {
39+
if (await this.overlay.isVisible()) {
40+
await this.closeButton.click()
41+
await expect(this.overlay).not.toBeVisible()
42+
}
43+
}
44+
45+
async getJobCount(state?: string) {
46+
if (state) {
47+
return await this.page
48+
.locator(`[data-testid="queue-job-item"][data-job-state="${state}"]`)
49+
.count()
50+
}
51+
return await this.jobItems.count()
52+
}
53+
54+
getJobAction(actionKey: string) {
55+
return this.page.getByTestId(`job-action-${actionKey}`)
56+
}
57+
}

browser_tests/fixtures/ws.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { test as base } from '@playwright/test'
22

3+
import type { StatusWsMessage } from '../../src/schemas/apiSchema'
4+
5+
export type WsMessage = { type: 'status'; data: StatusWsMessage }
6+
37
export const webSocketFixture = base.extend<{
4-
ws: { trigger(data: any, url?: string): Promise<void> }
8+
ws: { trigger(data: WsMessage, url?: string): Promise<void> }
59
}>({
610
ws: [
711
async ({ page }, use) => {

browser_tests/tests/actionbar.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Response } from '@playwright/test'
22
import { expect, mergeTests } from '@playwright/test'
33

4-
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
5-
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
6-
import { webSocketFixture } from '../fixtures/ws.ts'
4+
import { comfyPageFixture } from '../fixtures/ComfyPage'
5+
import { webSocketFixture } from '../fixtures/ws'
6+
import type { WsMessage } from '../fixtures/ws'
77

88
const test = mergeTests(comfyPageFixture, webSocketFixture)
99

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

6262
// Trigger a status websocket message
6363
const triggerStatus = async (queueSize: number) => {
64-
await ws.trigger({
64+
const message = {
6565
type: 'status',
6666
data: {
6767
status: {
@@ -70,7 +70,9 @@ test.describe('Actionbar', () => {
7070
}
7171
}
7272
}
73-
} as StatusWsMessage)
73+
} satisfies WsMessage
74+
75+
await ws.trigger(message)
7476
}
7577

7678
// Extract the width from the queue response
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { expect, mergeTests } from '@playwright/test'
2+
3+
import type { ComfyPage } from '../../fixtures/ComfyPage'
4+
import { comfyPageFixture } from '../../fixtures/ComfyPage'
5+
import { webSocketFixture } from '../../fixtures/ws'
6+
import type { WsMessage } from '../../fixtures/ws'
7+
8+
const test = mergeTests(comfyPageFixture, webSocketFixture)
9+
10+
type QueueState = {
11+
running: QueueJob[]
12+
pending: QueueJob[]
13+
}
14+
15+
type QueueJob = [
16+
string,
17+
string,
18+
Record<string, unknown>,
19+
Record<string, unknown>,
20+
string[]
21+
]
22+
23+
type QueueController = {
24+
state: QueueState
25+
sync: (
26+
ws: { trigger(data: WsMessage, url?: string): Promise<void> },
27+
nextState: Partial<QueueState>
28+
) => Promise<void>
29+
}
30+
31+
test.describe('Queue UI', () => {
32+
let queue: QueueController
33+
34+
test.beforeEach(async ({ comfyPage }) => {
35+
await comfyPage.page.route('**/api/prompt', async (route) => {
36+
await route.fulfill({
37+
status: 200,
38+
contentType: 'application/json',
39+
body: JSON.stringify({
40+
prompt_id: 'mock-prompt-id',
41+
number: 1,
42+
node_errors: {}
43+
})
44+
})
45+
})
46+
47+
// Mock history to avoid pulling real data
48+
await comfyPage.page.route('**/api/history**', async (route) => {
49+
await route.fulfill({
50+
status: 200,
51+
contentType: 'application/json',
52+
body: JSON.stringify({ History: [] })
53+
})
54+
})
55+
56+
queue = await createQueueController(comfyPage)
57+
})
58+
59+
test('toggles overlay and updates count from status events', async ({
60+
comfyPage,
61+
ws
62+
}) => {
63+
await queue.sync(ws, { running: [], pending: [] })
64+
65+
await expect(comfyPage.queueList.toggleButton).toContainText('0')
66+
await expect(comfyPage.queueList.toggleButton).toContainText(/queued/i)
67+
await expect(comfyPage.queueList.overlay).toBeHidden()
68+
69+
await queue.sync(ws, {
70+
pending: [queueJob('1', 'mock-pending', 'client-a')]
71+
})
72+
73+
await expect(comfyPage.queueList.toggleButton).toContainText('1')
74+
await expect(comfyPage.queueList.toggleButton).toContainText(/queued/i)
75+
76+
await comfyPage.queueList.open()
77+
await expect(comfyPage.queueList.overlay).toBeVisible()
78+
await expect(comfyPage.queueList.jobItems).toHaveCount(1)
79+
80+
await comfyPage.queueList.close()
81+
await expect(comfyPage.queueList.overlay).toBeHidden()
82+
})
83+
84+
test('displays running and pending jobs via status updates', async ({
85+
comfyPage,
86+
ws
87+
}) => {
88+
await queue.sync(ws, {
89+
running: [queueJob('2', 'mock-running', 'client-b')],
90+
pending: [queueJob('3', 'mock-pending', 'client-c')]
91+
})
92+
93+
await comfyPage.queueList.open()
94+
await expect(comfyPage.queueList.jobItems).toHaveCount(2)
95+
96+
const firstJob = comfyPage.queueList.jobItems.first()
97+
await firstJob.hover()
98+
99+
const cancelAction = firstJob
100+
.getByTestId('job-action-cancel-running')
101+
.or(firstJob.getByTestId('job-action-cancel-hover'))
102+
103+
await expect(cancelAction).toBeVisible()
104+
})
105+
})
106+
107+
const queueJob = (
108+
queueIndex: string,
109+
promptId: string,
110+
clientId: string
111+
): QueueJob => [
112+
queueIndex,
113+
promptId,
114+
{ client_id: clientId },
115+
{ class_type: 'Note' },
116+
['output']
117+
]
118+
119+
const createQueueController = async (
120+
comfyPage: ComfyPage
121+
): Promise<QueueController> => {
122+
const state: QueueState = { running: [], pending: [] }
123+
124+
// Single queue handler reads the latest in-memory state
125+
await comfyPage.page.route('**/api/queue', async (route) => {
126+
await route.fulfill({
127+
status: 200,
128+
contentType: 'application/json',
129+
body: JSON.stringify({
130+
queue_running: state.running,
131+
queue_pending: state.pending
132+
})
133+
})
134+
})
135+
136+
const sync = async (
137+
ws: { trigger(data: WsMessage, url?: string): Promise<void> },
138+
nextState: Partial<QueueState>
139+
) => {
140+
if (nextState.running) state.running = nextState.running
141+
if (nextState.pending) state.pending = nextState.pending
142+
143+
const total = state.running.length + state.pending.length
144+
const queueResponse = comfyPage.page.waitForResponse('**/api/queue')
145+
146+
await ws.trigger({
147+
type: 'status',
148+
data: {
149+
status: { exec_info: { queue_remaining: total } }
150+
}
151+
})
152+
153+
await queueResponse
154+
}
155+
156+
return { state, sync }
157+
}

0 commit comments

Comments
 (0)