mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
test: add in-memory jobs backend fixture
This commit is contained in:
@@ -108,6 +108,11 @@ Browser tests in this project follow a specific organization pattern:
|
||||
- Organized by functionality (e.g., `widget.spec.ts`, `interaction.spec.ts`)
|
||||
- Snapshot directories (e.g., `widget.spec.ts-snapshots/`) contain reference screenshots
|
||||
|
||||
- **Helper Unit Tests**: Colocated alongside browser-test helpers and fixtures
|
||||
- Use `.test.ts`
|
||||
- Run under Vitest, not Playwright
|
||||
- Do not place them under `browser_tests/tests/`, which is reserved for Playwright `*.spec.ts`
|
||||
|
||||
- **Utilities**: Located in `utils/` - Common utility functions
|
||||
- `litegraphUtils.ts` - Utilities for working with LiteGraph nodes
|
||||
|
||||
|
||||
398
browser_tests/fixtures/helpers/InMemoryJobsBackend.test.ts
Normal file
398
browser_tests/fixtures/helpers/InMemoryJobsBackend.test.ts
Normal file
@@ -0,0 +1,398 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
import type {
|
||||
JobDetailResponse,
|
||||
JobEntry,
|
||||
JobsListResponse
|
||||
} from '@comfyorg/ingest-types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { InMemoryJobsBackend } from '@e2e/fixtures/helpers/InMemoryJobsBackend'
|
||||
import type { SeededJob } from '@e2e/fixtures/helpers/InMemoryJobsBackend'
|
||||
|
||||
type RouteHandler = (route: Route) => Promise<void>
|
||||
|
||||
type RegisteredRoute = {
|
||||
pattern: string | RegExp
|
||||
handler: RouteHandler
|
||||
}
|
||||
|
||||
type PageStub = Pick<Page, 'route' | 'unroute'>
|
||||
|
||||
type FulfillOptions = NonNullable<Parameters<Route['fulfill']>[0]>
|
||||
|
||||
function createPageStub(): {
|
||||
page: PageStub
|
||||
routes: RegisteredRoute[]
|
||||
} {
|
||||
const routes: RegisteredRoute[] = []
|
||||
const page = {
|
||||
route: vi.fn(async (pattern: string | RegExp, handler: RouteHandler) => {
|
||||
routes.push({ pattern, handler })
|
||||
}),
|
||||
unroute: vi.fn(async () => {})
|
||||
} satisfies PageStub
|
||||
|
||||
return { page, routes }
|
||||
}
|
||||
|
||||
function createSeededJob({
|
||||
id,
|
||||
status = 'completed',
|
||||
createTime,
|
||||
executionStartTime = createTime,
|
||||
executionEndTime = createTime + 1_000,
|
||||
workflowId
|
||||
}: {
|
||||
id: string
|
||||
status?: JobEntry['status']
|
||||
createTime: number
|
||||
executionStartTime?: number
|
||||
executionEndTime?: number
|
||||
workflowId?: string
|
||||
}): SeededJob {
|
||||
const previewOutput = { filename: `${id}.png` }
|
||||
const terminalState =
|
||||
status === 'completed' || status === 'failed' || status === 'cancelled'
|
||||
|
||||
const listItem: JobEntry = {
|
||||
id,
|
||||
status,
|
||||
create_time: createTime,
|
||||
...(workflowId ? { workflow_id: workflowId } : {}),
|
||||
...(terminalState
|
||||
? {
|
||||
preview_output: previewOutput,
|
||||
outputs_count: 1,
|
||||
execution_start_time: executionStartTime,
|
||||
execution_end_time: executionEndTime
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
const detail: JobDetailResponse = {
|
||||
id,
|
||||
status,
|
||||
create_time: createTime,
|
||||
update_time: executionEndTime,
|
||||
...(workflowId ? { workflow_id: workflowId } : {}),
|
||||
...(terminalState
|
||||
? {
|
||||
preview_output: previewOutput,
|
||||
outputs_count: 1,
|
||||
outputs: {}
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
return { listItem, detail }
|
||||
}
|
||||
|
||||
function getRouteHandler(routes: RegisteredRoute[], url: string): RouteHandler {
|
||||
const registeredRoute = routes.find(({ pattern }) =>
|
||||
typeof pattern === 'string' ? pattern === url : pattern.test(url)
|
||||
)
|
||||
|
||||
if (!registeredRoute) {
|
||||
throw new Error(`Expected route handler for ${url}`)
|
||||
}
|
||||
|
||||
return registeredRoute.handler
|
||||
}
|
||||
|
||||
function createRouteInvocation({
|
||||
url,
|
||||
method = 'POST',
|
||||
requestBody
|
||||
}: {
|
||||
url: string
|
||||
method?: string
|
||||
requestBody?: unknown
|
||||
}): {
|
||||
route: Route
|
||||
continued: ReturnType<typeof vi.fn>
|
||||
getFulfilled: () => FulfillOptions | undefined
|
||||
} {
|
||||
let fulfilled: FulfillOptions | undefined
|
||||
const continued = vi.fn(async () => {})
|
||||
|
||||
const route = {
|
||||
request: () =>
|
||||
({
|
||||
method: () => method,
|
||||
url: () => url,
|
||||
postDataJSON: () => requestBody
|
||||
}) as ReturnType<Route['request']>,
|
||||
continue: continued,
|
||||
fulfill: vi.fn(async (options?: FulfillOptions) => {
|
||||
if (!options) {
|
||||
throw new Error('Expected route to be fulfilled with options')
|
||||
}
|
||||
|
||||
fulfilled = options
|
||||
})
|
||||
} satisfies Pick<Route, 'request' | 'continue' | 'fulfill'>
|
||||
|
||||
return {
|
||||
route: route as unknown as Route,
|
||||
continued,
|
||||
getFulfilled: () => fulfilled
|
||||
}
|
||||
}
|
||||
|
||||
function bodyToText(body: FulfillOptions['body']): string {
|
||||
if (body instanceof Uint8Array) {
|
||||
return Buffer.from(body).toString('utf-8')
|
||||
}
|
||||
|
||||
return `${body ?? ''}`
|
||||
}
|
||||
|
||||
async function invokeJsonRoute<T>(
|
||||
handler: RouteHandler,
|
||||
args: {
|
||||
url: string
|
||||
requestBody?: unknown
|
||||
}
|
||||
): Promise<{
|
||||
status: number | undefined
|
||||
body: T
|
||||
}> {
|
||||
const invocation = createRouteInvocation(args)
|
||||
|
||||
await handler(invocation.route)
|
||||
|
||||
const fulfilled = invocation.getFulfilled()
|
||||
expect(fulfilled).toBeDefined()
|
||||
|
||||
return {
|
||||
status: fulfilled?.status,
|
||||
body: JSON.parse(bodyToText(fulfilled?.body)) as T
|
||||
}
|
||||
}
|
||||
|
||||
describe('InMemoryJobsBackend', () => {
|
||||
it('lists jobs sorted by create_time descending by default', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([
|
||||
createSeededJob({ id: 'job-oldest', createTime: 1_000 }),
|
||||
createSeededJob({ id: 'job-newest', createTime: 3_000 }),
|
||||
createSeededJob({ id: 'job-middle', createTime: 2_000 })
|
||||
])
|
||||
|
||||
const listRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs'
|
||||
)
|
||||
const response = await invokeJsonRoute<JobsListResponse>(listRouteHandler, {
|
||||
url: 'http://localhost/api/jobs?offset=-1&limit=0'
|
||||
})
|
||||
|
||||
expect(response.body.jobs.map((job) => job.id)).toEqual([
|
||||
'job-newest',
|
||||
'job-middle',
|
||||
'job-oldest'
|
||||
])
|
||||
expect(response.body.pagination).toEqual({
|
||||
offset: 0,
|
||||
limit: 3,
|
||||
total: 3,
|
||||
has_more: false
|
||||
})
|
||||
})
|
||||
|
||||
it('filters by status and workflow_id, then sorts and paginates by execution_duration', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([
|
||||
createSeededJob({
|
||||
id: 'job-fast',
|
||||
status: 'completed',
|
||||
workflowId: 'wf-1',
|
||||
createTime: 1_000,
|
||||
executionEndTime: 1_100
|
||||
}),
|
||||
createSeededJob({
|
||||
id: 'job-slow',
|
||||
status: 'completed',
|
||||
workflowId: 'wf-1',
|
||||
createTime: 2_000,
|
||||
executionEndTime: 4_000
|
||||
}),
|
||||
createSeededJob({
|
||||
id: 'job-other-workflow',
|
||||
status: 'completed',
|
||||
workflowId: 'wf-2',
|
||||
createTime: 3_000,
|
||||
executionEndTime: 8_000
|
||||
}),
|
||||
createSeededJob({
|
||||
id: 'job-pending',
|
||||
status: 'pending',
|
||||
workflowId: 'wf-1',
|
||||
createTime: 4_000
|
||||
})
|
||||
])
|
||||
|
||||
const listRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs'
|
||||
)
|
||||
const response = await invokeJsonRoute<JobsListResponse>(listRouteHandler, {
|
||||
url: 'http://localhost/api/jobs?status=completed&workflow_id=wf-1&sort_by=execution_duration&sort_order=asc&offset=1&limit=1'
|
||||
})
|
||||
|
||||
expect(response.body.jobs.map((job) => job.id)).toEqual(['job-slow'])
|
||||
expect(response.body.pagination).toEqual({
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
total: 2,
|
||||
has_more: false
|
||||
})
|
||||
})
|
||||
|
||||
it('returns job detail responses by id', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
const seededJob = createSeededJob({
|
||||
id: 'job-detail',
|
||||
createTime: 5_000,
|
||||
workflowId: 'wf-detail'
|
||||
})
|
||||
|
||||
await backend.seed([seededJob])
|
||||
|
||||
const detailRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs/job-detail'
|
||||
)
|
||||
const response = await invokeJsonRoute<JobDetailResponse>(
|
||||
detailRouteHandler,
|
||||
{
|
||||
url: 'http://localhost/api/jobs/job-detail'
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toEqual(seededJob.detail)
|
||||
})
|
||||
|
||||
it('returns 404 for unknown job detail requests', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([])
|
||||
|
||||
const detailRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs/missing-job'
|
||||
)
|
||||
const response = await invokeJsonRoute<{ error: string }>(
|
||||
detailRouteHandler,
|
||||
{
|
||||
url: 'http://localhost/api/jobs/missing-job'
|
||||
}
|
||||
)
|
||||
|
||||
expect(response.status).toBe(404)
|
||||
expect(response.body).toEqual({ error: 'Job not found' })
|
||||
})
|
||||
|
||||
it('clears terminal jobs while preserving in-progress jobs for history clear', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([
|
||||
createSeededJob({
|
||||
id: 'job-completed',
|
||||
status: 'completed',
|
||||
createTime: 1_000
|
||||
}),
|
||||
createSeededJob({
|
||||
id: 'job-failed',
|
||||
status: 'failed',
|
||||
createTime: 2_000
|
||||
}),
|
||||
createSeededJob({
|
||||
id: 'job-running',
|
||||
status: 'in_progress',
|
||||
createTime: 3_000
|
||||
})
|
||||
])
|
||||
|
||||
const historyRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/history'
|
||||
)
|
||||
const clearInvocation = createRouteInvocation({
|
||||
url: 'http://localhost/api/history',
|
||||
requestBody: { clear: true }
|
||||
})
|
||||
|
||||
await historyRouteHandler(clearInvocation.route)
|
||||
|
||||
const listRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs'
|
||||
)
|
||||
const response = await invokeJsonRoute<JobsListResponse>(listRouteHandler, {
|
||||
url: 'http://localhost/api/jobs'
|
||||
})
|
||||
|
||||
expect(response.body.jobs.map((job) => job.id)).toEqual(['job-running'])
|
||||
})
|
||||
|
||||
it('deletes specific jobs via the history endpoint', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([
|
||||
createSeededJob({ id: 'job-keep', createTime: 1_000 }),
|
||||
createSeededJob({ id: 'job-delete', createTime: 2_000 })
|
||||
])
|
||||
|
||||
const historyRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/history'
|
||||
)
|
||||
const deleteInvocation = createRouteInvocation({
|
||||
url: 'http://localhost/api/history',
|
||||
requestBody: { delete: ['job-delete'] }
|
||||
})
|
||||
|
||||
await historyRouteHandler(deleteInvocation.route)
|
||||
|
||||
const listRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/jobs'
|
||||
)
|
||||
const response = await invokeJsonRoute<JobsListResponse>(listRouteHandler, {
|
||||
url: 'http://localhost/api/jobs'
|
||||
})
|
||||
|
||||
expect(response.body.jobs.map((job) => job.id)).toEqual(['job-keep'])
|
||||
})
|
||||
|
||||
it('falls through non-POST history requests', async () => {
|
||||
const { page, routes } = createPageStub()
|
||||
const backend = new InMemoryJobsBackend(page as unknown as Page)
|
||||
|
||||
await backend.seed([createSeededJob({ id: 'job-history', createTime: 1 })])
|
||||
|
||||
const historyRouteHandler = getRouteHandler(
|
||||
routes,
|
||||
'http://localhost/api/history'
|
||||
)
|
||||
const invocation = createRouteInvocation({
|
||||
url: 'http://localhost/api/history',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
await historyRouteHandler(invocation.route)
|
||||
|
||||
expect(invocation.continued).toHaveBeenCalledTimes(1)
|
||||
expect(invocation.getFulfilled()).toBeUndefined()
|
||||
})
|
||||
})
|
||||
213
browser_tests/fixtures/helpers/InMemoryJobsBackend.ts
Normal file
213
browser_tests/fixtures/helpers/InMemoryJobsBackend.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type { Page, Route } from '@playwright/test'
|
||||
import type {
|
||||
JobDetailResponse,
|
||||
JobEntry,
|
||||
JobsListResponse
|
||||
} from '@comfyorg/ingest-types'
|
||||
|
||||
const jobsListRoutePattern = /\/api\/jobs(?:\?.*)?$/
|
||||
const jobDetailRoutePattern = /\/api\/jobs\/[^/?#]+(?:\?.*)?$/
|
||||
const historyRoutePattern = /\/api\/history(?:\?.*)?$/
|
||||
|
||||
export type SeededJob = {
|
||||
listItem: JobEntry
|
||||
detail: JobDetailResponse
|
||||
}
|
||||
|
||||
function parseLimit(url: URL, total: number): number {
|
||||
const value = Number(url.searchParams.get('limit'))
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
return total
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function parseOffset(url: URL): number {
|
||||
const value = Number(url.searchParams.get('offset'))
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function getExecutionDuration(job: JobEntry): number {
|
||||
const start = job.execution_start_time ?? 0
|
||||
const end = job.execution_end_time ?? 0
|
||||
|
||||
return end - start
|
||||
}
|
||||
|
||||
function getJobIdFromRequest(route: Route): string | null {
|
||||
const url = new URL(route.request().url())
|
||||
const jobId = url.pathname.split('/').at(-1)
|
||||
|
||||
return jobId ? decodeURIComponent(jobId) : null
|
||||
}
|
||||
|
||||
export class InMemoryJobsBackend {
|
||||
private listRouteHandler: ((route: Route) => Promise<void>) | null = null
|
||||
private detailRouteHandler: ((route: Route) => Promise<void>) | null = null
|
||||
private historyRouteHandler: ((route: Route) => Promise<void>) | null = null
|
||||
private seededJobs = new Map<string, SeededJob>()
|
||||
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async seed(jobs: SeededJob[]): Promise<void> {
|
||||
this.seededJobs = new Map(
|
||||
jobs.map((job) => [job.listItem.id, job] satisfies [string, SeededJob])
|
||||
)
|
||||
await this.ensureRoutesRegistered()
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.seededJobs.clear()
|
||||
|
||||
if (this.listRouteHandler) {
|
||||
await this.page.unroute(jobsListRoutePattern, this.listRouteHandler)
|
||||
this.listRouteHandler = null
|
||||
}
|
||||
|
||||
if (this.detailRouteHandler) {
|
||||
await this.page.unroute(jobDetailRoutePattern, this.detailRouteHandler)
|
||||
this.detailRouteHandler = null
|
||||
}
|
||||
|
||||
if (this.historyRouteHandler) {
|
||||
await this.page.unroute(historyRoutePattern, this.historyRouteHandler)
|
||||
this.historyRouteHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureRoutesRegistered(): Promise<void> {
|
||||
if (!this.listRouteHandler) {
|
||||
this.listRouteHandler = async (route: Route) => {
|
||||
const url = new URL(route.request().url())
|
||||
const statuses = url.searchParams
|
||||
.get('status')
|
||||
?.split(',')
|
||||
.map((status) => status.trim())
|
||||
.filter(Boolean)
|
||||
const workflowId = url.searchParams.get('workflow_id')
|
||||
const sortBy = url.searchParams.get('sort_by')
|
||||
const sortOrder = url.searchParams.get('sort_order') === 'asc' ? 1 : -1
|
||||
|
||||
let filteredJobs = Array.from(
|
||||
this.seededJobs.values(),
|
||||
({ listItem }) => listItem
|
||||
)
|
||||
|
||||
if (statuses?.length) {
|
||||
filteredJobs = filteredJobs.filter((job) =>
|
||||
statuses.includes(job.status)
|
||||
)
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
filteredJobs = filteredJobs.filter(
|
||||
(job) => job.workflow_id === workflowId
|
||||
)
|
||||
}
|
||||
|
||||
filteredJobs.sort((left, right) => {
|
||||
const leftValue =
|
||||
sortBy === 'execution_duration'
|
||||
? getExecutionDuration(left)
|
||||
: left.create_time
|
||||
const rightValue =
|
||||
sortBy === 'execution_duration'
|
||||
? getExecutionDuration(right)
|
||||
: right.create_time
|
||||
|
||||
return (leftValue - rightValue) * sortOrder
|
||||
})
|
||||
|
||||
const offset = parseOffset(url)
|
||||
const total = filteredJobs.length
|
||||
const limit = parseLimit(url, total)
|
||||
const visibleJobs = filteredJobs.slice(offset, offset + limit)
|
||||
|
||||
const response = {
|
||||
jobs: visibleJobs,
|
||||
pagination: {
|
||||
offset,
|
||||
limit,
|
||||
total,
|
||||
has_more: offset + visibleJobs.length < total
|
||||
}
|
||||
} satisfies JobsListResponse
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.route(jobsListRoutePattern, this.listRouteHandler)
|
||||
}
|
||||
|
||||
if (!this.detailRouteHandler) {
|
||||
this.detailRouteHandler = async (route: Route) => {
|
||||
const jobId = getJobIdFromRequest(route)
|
||||
const job = jobId ? this.seededJobs.get(jobId) : undefined
|
||||
|
||||
if (!job) {
|
||||
await route.fulfill({
|
||||
status: 404,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'Job not found' })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(job.detail)
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.route(jobDetailRoutePattern, this.detailRouteHandler)
|
||||
}
|
||||
|
||||
if (!this.historyRouteHandler) {
|
||||
this.historyRouteHandler = async (route: Route) => {
|
||||
const request = route.request()
|
||||
if (request.method() !== 'POST') {
|
||||
await route.continue()
|
||||
return
|
||||
}
|
||||
|
||||
const requestBody = request.postDataJSON() as
|
||||
| { delete?: string[]; clear?: boolean }
|
||||
| undefined
|
||||
|
||||
if (requestBody?.clear) {
|
||||
this.seededJobs = new Map(
|
||||
Array.from(this.seededJobs).filter(([, job]) => {
|
||||
const status = job.listItem.status
|
||||
|
||||
return status === 'pending' || status === 'in_progress'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (requestBody?.delete?.length) {
|
||||
for (const jobId of requestBody.delete) {
|
||||
this.seededJobs.delete(jobId)
|
||||
}
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
}
|
||||
|
||||
await this.page.route(historyRoutePattern, this.historyRouteHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
browser_tests/fixtures/helpers/jobFixtures.ts
Normal file
50
browser_tests/fixtures/helpers/jobFixtures.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { JobDetailResponse, JobEntry } from '@comfyorg/ingest-types'
|
||||
|
||||
import type { SeededJob } from '@e2e/fixtures/helpers/InMemoryJobsBackend'
|
||||
|
||||
export function createMockJob(
|
||||
overrides: Partial<JobEntry> & { id: string }
|
||||
): JobEntry {
|
||||
const now = Date.now()
|
||||
|
||||
return {
|
||||
status: 'completed',
|
||||
create_time: now,
|
||||
execution_start_time: now,
|
||||
execution_end_time: now + 5_000,
|
||||
preview_output: {
|
||||
filename: `output_${overrides.id}.png`,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '1',
|
||||
mediaType: 'images'
|
||||
},
|
||||
outputs_count: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalStatus(status: JobEntry['status']) {
|
||||
return status === 'completed' || status === 'failed' || status === 'cancelled'
|
||||
}
|
||||
|
||||
function createSeededJob(listItem: JobEntry): SeededJob {
|
||||
const updateTime =
|
||||
listItem.execution_end_time ??
|
||||
listItem.execution_start_time ??
|
||||
listItem.create_time
|
||||
const detail: JobDetailResponse = {
|
||||
...listItem,
|
||||
update_time: updateTime,
|
||||
...(isTerminalStatus(listItem.status) ? { outputs: {} } : {})
|
||||
}
|
||||
|
||||
return {
|
||||
listItem,
|
||||
detail
|
||||
}
|
||||
}
|
||||
|
||||
export function createSeededJobs(listItems: readonly JobEntry[]): SeededJob[] {
|
||||
return listItems.map(createSeededJob)
|
||||
}
|
||||
15
browser_tests/fixtures/jobsBackendFixture.ts
Normal file
15
browser_tests/fixtures/jobsBackendFixture.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
import { InMemoryJobsBackend } from '@e2e/fixtures/helpers/InMemoryJobsBackend'
|
||||
|
||||
export const jobsBackendFixture = base.extend<{
|
||||
jobsBackend: InMemoryJobsBackend
|
||||
}>({
|
||||
jobsBackend: async ({ page }, use) => {
|
||||
const jobsBackend = new InMemoryJobsBackend(page)
|
||||
|
||||
await use(jobsBackend)
|
||||
|
||||
await jobsBackend.clear()
|
||||
}
|
||||
})
|
||||
@@ -1,13 +1,19 @@
|
||||
import { expect } from '@playwright/test'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
import type { JobEntry } from '@comfyorg/ingest-types'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
|
||||
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
||||
import { jobsBackendFixture } from '@e2e/fixtures/jobsBackendFixture'
|
||||
import {
|
||||
createMockJob,
|
||||
createSeededJobs
|
||||
} from '@e2e/fixtures/helpers/jobFixtures'
|
||||
import { TestIds } from '@e2e/fixtures/selectors'
|
||||
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, jobsBackendFixture)
|
||||
|
||||
const now = Date.now()
|
||||
|
||||
const MOCK_JOBS: RawJobListItem[] = [
|
||||
const MOCK_JOBS: JobEntry[] = [
|
||||
createMockJob({
|
||||
id: 'job-completed-1',
|
||||
status: 'completed',
|
||||
@@ -35,16 +41,14 @@ const MOCK_JOBS: RawJobListItem[] = [
|
||||
]
|
||||
|
||||
test.describe('Queue overlay', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.mockOutputHistory(MOCK_JOBS)
|
||||
await comfyPage.settings.setSetting('Comfy.Queue.QPOV2', false)
|
||||
test.beforeEach(async ({ comfyPage, jobsBackend }) => {
|
||||
await jobsBackend.seed(createSeededJobs(MOCK_JOBS))
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.Queue.QPOV2': false
|
||||
})
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.assets.clearMocks()
|
||||
})
|
||||
|
||||
test('Toggle button opens expanded queue overlay', async ({ comfyPage }) => {
|
||||
const toggle = comfyPage.page.getByTestId(TestIds.queue.overlayToggle)
|
||||
await toggle.click()
|
||||
|
||||
@@ -17,7 +17,8 @@ The ComfyUI Frontend project uses **colocated tests** - test files are placed al
|
||||
- **Component Tests**: Located directly alongside their components (e.g., `MyComponent.test.ts` next to `MyComponent.vue`)
|
||||
- **Unit Tests**: Located alongside their source files (e.g., `myUtil.test.ts` next to `myUtil.ts`)
|
||||
- **Store Tests**: Located in `src/stores/` alongside their store files
|
||||
- **Browser Tests**: Located in the `browser_tests/` directory (see dedicated README there)
|
||||
- **Browser Tests**: Playwright specs live in `browser_tests/tests/**/*.spec.ts` (see dedicated README there)
|
||||
- **Browser Test Helper Unit Tests**: Vitest tests for browser fixtures/helpers may be colocated in `browser_tests/` as `*.test.ts`, outside `browser_tests/tests/`
|
||||
|
||||
### Test File Naming
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const maybeLocalOptions: PlaywrightTestConfig = process.env.PLAYWRIGHT_LOCAL
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './browser_tests',
|
||||
testMatch: ['tests/**/*.spec.ts'],
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
reporter: 'html',
|
||||
|
||||
@@ -634,6 +634,7 @@ export default defineConfig({
|
||||
'@/utils/formatUtil': '/packages/shared-frontend-utils/src/formatUtil.ts',
|
||||
'@/utils/networkUtil':
|
||||
'/packages/shared-frontend-utils/src/networkUtil.ts',
|
||||
'@e2e': '/browser_tests',
|
||||
'@': '/src'
|
||||
}
|
||||
},
|
||||
@@ -650,6 +651,7 @@ export default defineConfig({
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
retry: process.env.CI ? 2 : 0,
|
||||
include: [
|
||||
'browser_tests/**/*.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
'scripts/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'
|
||||
|
||||
Reference in New Issue
Block a user