Add tests

This commit is contained in:
Benjamin Lu
2025-12-09 00:42:07 -08:00
parent f0b9d28ff7
commit b42889206c
7 changed files with 229 additions and 1 deletions

View File

@@ -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)

View File

@@ -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}`)
}
}

View File

@@ -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<string, unknown>,
Record<string, unknown>,
string[]
]
type QueueController = {
state: QueueState
sync: (
ws: { trigger(data: any, 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).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<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: any, 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 } }
}
} as StatusWsMessage)
await queueResponse
}
return { state, sync }
}

View File

@@ -31,7 +31,10 @@
>
<i class="icon-[lucide--panel-right] size-4" />
</IconButton>
<QueueInlineProgress :hidden="isQueueOverlayExpanded" />
<QueueInlineProgress
:hidden="isQueueOverlayExpanded"
data-testid="queue-inline-progress"
/>
</div>
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
</div>

View File

@@ -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"
>
<i

View File

@@ -6,6 +6,7 @@
<div
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
:class="containerClass"
data-testid="queue-overlay"
>
<!-- Expanded state -->
<QueueOverlayExpanded

View File

@@ -2,6 +2,9 @@
<div
ref="rowRef"
class="relative"
data-testid="queue-job-item"
:data-job-id="props.jobId"
:data-job-state="props.state"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@contextmenu.stop.prevent="onContextMenu"
@@ -117,6 +120,7 @@
size="sm"
:class="getActionButtonClass()"
:aria-label="action.ariaLabel"
:data-testid="`job-action-${action.key}`"
@click.stop="action.onClick?.($event)"
>
<i :class="cn(action.iconClass, 'size-4')" />
@@ -127,6 +131,7 @@
type="transparent"
:label="action.label"
:aria-label="action.ariaLabel"
:data-testid="`job-action-${action.key}`"
@click.stop="action.onClick?.($event)"
/>
</template>
@@ -141,6 +146,7 @@
size="sm"
:class="getActionButtonClass()"
:aria-label="action.ariaLabel"
:data-testid="`job-action-${action.key}`"
@click.stop="action.onClick?.($event)"
>
<i :class="cn(action.iconClass, 'size-4')" />
@@ -151,6 +157,7 @@
type="transparent"
:label="action.label"
:aria-label="action.ariaLabel"
:data-testid="`job-action-${action.key}`"
@click.stop="action.onClick?.($event)"
/>
</template>