mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Add tests
This commit is contained in:
@@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
|
|||||||
import { ComfyMouse } from './ComfyMouse'
|
import { ComfyMouse } from './ComfyMouse'
|
||||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||||
|
import { QueueList } from './components/QueueList'
|
||||||
import { SettingDialog } from './components/SettingDialog'
|
import { SettingDialog } from './components/SettingDialog'
|
||||||
import {
|
import {
|
||||||
NodeLibrarySidebarTab,
|
NodeLibrarySidebarTab,
|
||||||
@@ -151,6 +152,7 @@ export class ComfyPage {
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
public readonly searchBox: ComfyNodeSearchBox
|
public readonly searchBox: ComfyNodeSearchBox
|
||||||
|
public readonly queueList: QueueList
|
||||||
public readonly menu: ComfyMenu
|
public readonly menu: ComfyMenu
|
||||||
public readonly actionbar: ComfyActionbar
|
public readonly actionbar: ComfyActionbar
|
||||||
public readonly templates: ComfyTemplates
|
public readonly templates: ComfyTemplates
|
||||||
@@ -183,6 +185,7 @@ export class ComfyPage {
|
|||||||
this.visibleToasts = page.locator('.p-toast-message:visible')
|
this.visibleToasts = page.locator('.p-toast-message:visible')
|
||||||
|
|
||||||
this.searchBox = new ComfyNodeSearchBox(page)
|
this.searchBox = new ComfyNodeSearchBox(page)
|
||||||
|
this.queueList = new QueueList(page)
|
||||||
this.menu = new ComfyMenu(page)
|
this.menu = new ComfyMenu(page)
|
||||||
this.actionbar = new ComfyActionbar(page)
|
this.actionbar = new ComfyActionbar(page)
|
||||||
this.templates = new ComfyTemplates(page)
|
this.templates = new ComfyTemplates(page)
|
||||||
|
|||||||
57
browser_tests/fixtures/components/QueueList.ts
Normal file
57
browser_tests/fixtures/components/QueueList.ts
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
browser_tests/tests/queue/queueList.spec.ts
Normal file
156
browser_tests/tests/queue/queueList.spec.ts
Normal 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 }
|
||||||
|
}
|
||||||
@@ -31,7 +31,10 @@
|
|||||||
>
|
>
|
||||||
<i class="icon-[lucide--panel-right] size-4" />
|
<i class="icon-[lucide--panel-right] size-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<QueueInlineProgress :hidden="isQueueOverlayExpanded" />
|
<QueueInlineProgress
|
||||||
|
:hidden="isQueueOverlayExpanded"
|
||||||
|
data-testid="queue-inline-progress"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
|
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
|
||||||
:aria-label="t('g.close')"
|
:aria-label="t('g.close')"
|
||||||
|
data-testid="queue-overlay-close-button"
|
||||||
@click="onCloseClick"
|
@click="onCloseClick"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<div
|
<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="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"
|
:class="containerClass"
|
||||||
|
data-testid="queue-overlay"
|
||||||
>
|
>
|
||||||
<!-- Expanded state -->
|
<!-- Expanded state -->
|
||||||
<QueueOverlayExpanded
|
<QueueOverlayExpanded
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<div
|
<div
|
||||||
ref="rowRef"
|
ref="rowRef"
|
||||||
class="relative"
|
class="relative"
|
||||||
|
data-testid="queue-job-item"
|
||||||
|
:data-job-id="props.jobId"
|
||||||
|
:data-job-state="props.state"
|
||||||
@mouseenter="handleMouseEnter"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
@contextmenu.stop.prevent="onContextMenu"
|
@contextmenu.stop.prevent="onContextMenu"
|
||||||
@@ -117,6 +120,7 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
:class="getActionButtonClass()"
|
:class="getActionButtonClass()"
|
||||||
:aria-label="action.ariaLabel"
|
:aria-label="action.ariaLabel"
|
||||||
|
:data-testid="`job-action-${action.key}`"
|
||||||
@click.stop="action.onClick?.($event)"
|
@click.stop="action.onClick?.($event)"
|
||||||
>
|
>
|
||||||
<i :class="cn(action.iconClass, 'size-4')" />
|
<i :class="cn(action.iconClass, 'size-4')" />
|
||||||
@@ -127,6 +131,7 @@
|
|||||||
type="transparent"
|
type="transparent"
|
||||||
:label="action.label"
|
:label="action.label"
|
||||||
:aria-label="action.ariaLabel"
|
:aria-label="action.ariaLabel"
|
||||||
|
:data-testid="`job-action-${action.key}`"
|
||||||
@click.stop="action.onClick?.($event)"
|
@click.stop="action.onClick?.($event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -141,6 +146,7 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
:class="getActionButtonClass()"
|
:class="getActionButtonClass()"
|
||||||
:aria-label="action.ariaLabel"
|
:aria-label="action.ariaLabel"
|
||||||
|
:data-testid="`job-action-${action.key}`"
|
||||||
@click.stop="action.onClick?.($event)"
|
@click.stop="action.onClick?.($event)"
|
||||||
>
|
>
|
||||||
<i :class="cn(action.iconClass, 'size-4')" />
|
<i :class="cn(action.iconClass, 'size-4')" />
|
||||||
@@ -151,6 +157,7 @@
|
|||||||
type="transparent"
|
type="transparent"
|
||||||
:label="action.label"
|
:label="action.label"
|
||||||
:aria-label="action.ariaLabel"
|
:aria-label="action.ariaLabel"
|
||||||
|
:data-testid="`job-action-${action.key}`"
|
||||||
@click.stop="action.onClick?.($event)"
|
@click.stop="action.onClick?.($event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user