Files
ComfyUI_frontend/browser_tests/tests/queue/queueOverlay.spec.ts
Benjamin Lu 06e09df673 test: replace jobs mock fixture with typed route mocks (#12267)
## Summary

Replace the merged stateful jobs API browser mock fixture with a small
declarative typed route-mock foundation.

## Changes

- **What**: Removes `JobsApiMock`, `jobsApiMockFixture`, and the old
shared `jobFixtures` helper.
- **What**: Adds a generic `RouteMocker` primitive for explicit typed
JSON route responses.
- **What**: Adds `jobsRouteFixture`, which registers explicit
`/api/jobs` list/detail responses without filtering, mutation handling,
or hidden in-memory backend behavior.
- **What**: Migrates the current queue overlay and missing-media runtime
specs onto the new jobs route fixture.
- **What**: Keeps `./browser_tests/tsconfig.json` in the ESLint
TypeScript resolver config.
- **Dependencies**: None.

## Review Focus

This is intended to be the foundation PR for the test-strategy reset:
old stateful helper out, typed declarative route mocks in. It
intentionally does not add the full asset sidebar, job history sidebar,
or floating QPO coverage suite; those should stack on top after this
fixture shape is accepted.

The boundary this PR is trying to preserve: route mocks may describe
frontend-visible API responses, but should not implement Core
queue/history mutation semantics.

Context:
https://www.notion.so/comfy-org/E2E-Test-Strategy-for-Assets-Job-History-and-Queue-Progress-Overlay-35f6d73d365081209bc5f10e6c7eb8de

## Screenshots (if applicable)

Not applicable.
2026-05-14 20:09:48 +00:00

179 lines
5.6 KiB
TypeScript

import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
import {
createRouteMockJob,
jobsRouteFixture
} from '@e2e/fixtures/jobsRouteFixture'
import { TestIds } from '@e2e/fixtures/selectors'
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
const test = mergeTests(comfyPageFixture, jobsRouteFixture)
const mockJobTimestamp = Date.UTC(2026, 0, 1, 12)
const MOCK_JOBS: RawJobListItem[] = [
createRouteMockJob({
id: 'job-completed-1',
status: 'completed',
create_time: mockJobTimestamp - 60_000,
execution_start_time: mockJobTimestamp - 60_000,
execution_end_time: mockJobTimestamp - 50_000,
outputs_count: 2
}),
createRouteMockJob({
id: 'job-completed-2',
status: 'completed',
create_time: mockJobTimestamp - 120_000,
execution_start_time: mockJobTimestamp - 120_000,
execution_end_time: mockJobTimestamp - 115_000,
outputs_count: 1
}),
createRouteMockJob({
id: 'job-failed-1',
status: 'failed',
create_time: mockJobTimestamp - 30_000,
execution_start_time: mockJobTimestamp - 30_000,
execution_end_time: mockJobTimestamp - 28_000,
outputs_count: 0
}),
createRouteMockJob({
id: 'job-failed-bottom',
status: 'failed',
create_time: mockJobTimestamp - 180_000,
execution_start_time: mockJobTimestamp - 180_000,
execution_end_time: mockJobTimestamp - 178_000,
outputs_count: 0
})
]
test.describe('Queue overlay', () => {
test.beforeEach(async ({ comfyPage, jobsRoutes }) => {
await jobsRoutes.mockJobsScenario({ history: MOCK_JOBS, queue: [] })
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false)
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', false)
await comfyPage.setup()
})
test('Toggle button opens expanded queue overlay', async ({ comfyPage }) => {
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
// Expanded overlay should show job items
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
})
test('Overlay shows filter tabs (All, Completed)', async ({ comfyPage }) => {
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
await expect(
comfyPage.page.getByRole('button', { name: 'All', exact: true })
).toBeVisible()
await expect(
comfyPage.page.getByRole('button', { name: 'Completed', exact: true })
).toBeVisible()
})
test('Overlay shows Failed tab when failed jobs exist', async ({
comfyPage
}) => {
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
await expect(
comfyPage.page.getByRole('button', { name: 'Failed', exact: true })
).toBeVisible()
})
test('Completed filter shows only completed jobs', async ({ comfyPage }) => {
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
await comfyPage.page
.getByRole('button', { name: 'Completed', exact: true })
.click()
await expect(
comfyPage.page.locator('[data-job-id="job-completed-1"]')
).toBeVisible()
await expect(
comfyPage.page.locator('[data-job-id="job-failed-1"]')
).toBeHidden()
})
test('Toggling overlay again closes it', async ({ comfyPage }) => {
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeVisible()
await toggle.click()
await expect(comfyPage.page.locator('[data-job-id]').first()).toBeHidden()
})
test('Job details popover stays inside the viewport for bottom rows', async ({
comfyPage
}) => {
await comfyPage.page.setViewportSize({ width: 1280, height: 420 })
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
await toggle.click()
const bottomJob = comfyPage.page.locator(
'[data-job-id="job-failed-bottom"]'
)
await expect(bottomJob).toBeVisible()
await bottomJob.scrollIntoViewIfNeeded()
await expect(bottomJob).toBeVisible()
const viewportSize = comfyPage.page.viewportSize()
if (!viewportSize) throw new Error('Viewport must be available')
const rowBox = await bottomJob.boundingBox()
if (!rowBox) throw new Error('Bottom job row should be measurable')
expect(
rowBox.y + rowBox.height,
'Test row should be low enough to exercise bottom-edge collision handling'
).toBeGreaterThan(viewportSize.height * 0.55)
await expect
.poll(async () =>
bottomJob.evaluate((element) => {
const rect = element.getBoundingClientRect()
const hitTarget = document.elementFromPoint(
rect.x + rect.width / 2,
rect.y + rect.height / 2
)
return hitTarget ? element.contains(hitTarget) : false
})
)
.toBe(true)
await comfyPage.page.mouse.move(0, 0)
await comfyPage.page.mouse.move(
rowBox.x + rowBox.width / 2,
rowBox.y + rowBox.height / 2,
{ steps: 5 }
)
const popover = comfyPage.page.getByTestId(TestIds.queue.jobDetailsPopover)
await expect(popover).toBeVisible()
await expect
.poll(async () => {
const popoverBox = await popover.boundingBox()
if (!popoverBox) return false
return (
popoverBox.y >= 0 &&
popoverBox.y + popoverBox.height <= viewportSize.height
)
})
.toBe(true)
})
})