mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
## 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.
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
import { expect, mergeTests } from '@playwright/test'
|
|
import type { Page, Route } from '@playwright/test'
|
|
import type { Asset, ListAssetsResponse } from '@comfyorg/ingest-types'
|
|
|
|
import {
|
|
assetRequestIncludesTag,
|
|
createCloudAssetsFixture
|
|
} from '@e2e/fixtures/assetApiFixture'
|
|
import { comfyPageFixture } from '@e2e/fixtures/ComfyPage'
|
|
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
|
import {
|
|
createRouteMockJob,
|
|
jobsRouteFixture
|
|
} from '@e2e/fixtures/jobsRouteFixture'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
|
import type { RawJobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
|
|
|
const ossTest = mergeTests(comfyPageFixture, jobsRouteFixture)
|
|
const outputHash =
|
|
'147257c95a3e957e0deee73a077cfec89da2d906dd086ca70a2b0c897a9591d6e.png'
|
|
const plainVideoFileName = 'plain_video.mp4'
|
|
const graphDropPosition = { x: 500, y: 300 }
|
|
const missingMediaUploadObservationMs = 1_000
|
|
const missingMediaUploadPollMs = 100
|
|
|
|
const cloudOutputAsset: Asset = {
|
|
id: 'test-output-hash-001',
|
|
name: 'ComfyUI_00001_.png',
|
|
asset_hash: outputHash,
|
|
size: 4_194_304,
|
|
mime_type: 'image/png',
|
|
tags: ['output'],
|
|
created_at: '2026-05-01T00:00:00Z',
|
|
updated_at: '2026-05-01T00:00:00Z',
|
|
last_access_time: '2026-05-01T00:00:00Z'
|
|
}
|
|
|
|
const cloudUploadedVideoAsset: Asset = {
|
|
id: 'test-uploaded-video-001',
|
|
name: plainVideoFileName,
|
|
asset_hash: plainVideoFileName,
|
|
size: 1_024,
|
|
mime_type: 'video/mp4',
|
|
tags: ['input'],
|
|
created_at: '2026-05-01T00:00:00Z',
|
|
updated_at: '2026-05-01T00:00:00Z',
|
|
last_access_time: '2026-05-01T00:00:00Z'
|
|
}
|
|
|
|
// The Cloud test app starts with a default LoadImage node. Keep that baseline
|
|
// input resolvable so this spec only observes the media it creates.
|
|
const cloudDefaultGraphInputAsset: Asset = {
|
|
id: 'test-default-input-001',
|
|
name: '00000000000000000000000Aexample.png',
|
|
asset_hash: '00000000000000000000000Aexample.png',
|
|
size: 1_024,
|
|
mime_type: 'image/png',
|
|
tags: ['input'],
|
|
created_at: '2026-05-01T00:00:00Z',
|
|
updated_at: '2026-05-01T00:00:00Z',
|
|
last_access_time: '2026-05-01T00:00:00Z'
|
|
}
|
|
|
|
interface CloudUploadAssetState {
|
|
isUploadedAssetAvailable: boolean
|
|
}
|
|
|
|
const cloudOutputTest = createCloudAssetsFixture([cloudOutputAsset])
|
|
const cloudUploadAssetStateByPage = new WeakMap<Page, CloudUploadAssetState>()
|
|
const cloudUploadRaceTest = comfyPageFixture.extend<{
|
|
markUploadedCloudAssetAvailable: () => void
|
|
}>({
|
|
page: async ({ page }, use) => {
|
|
const state: CloudUploadAssetState = {
|
|
isUploadedAssetAvailable: false
|
|
}
|
|
cloudUploadAssetStateByPage.set(page, state)
|
|
|
|
const assetsRouteHandler = async (route: Route) => {
|
|
const allAssets = [
|
|
cloudDefaultGraphInputAsset,
|
|
...(state.isUploadedAssetAvailable ? [cloudUploadedVideoAsset] : [])
|
|
]
|
|
const includeTags =
|
|
new URL(route.request().url()).searchParams
|
|
.get('include_tags')
|
|
?.split(',')
|
|
.filter(Boolean) ?? []
|
|
const assets = includeTags.length
|
|
? allAssets.filter((asset) =>
|
|
asset.tags?.some((tag) => includeTags.includes(tag))
|
|
)
|
|
: allAssets
|
|
const response: ListAssetsResponse = {
|
|
assets,
|
|
total: assets.length,
|
|
has_more: false
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(response)
|
|
})
|
|
}
|
|
|
|
await page.route(/\/api\/assets(?:\?.*)?$/, assetsRouteHandler)
|
|
await use(page)
|
|
await page.unroute(/\/api\/assets(?:\?.*)?$/, assetsRouteHandler)
|
|
cloudUploadAssetStateByPage.delete(page)
|
|
},
|
|
markUploadedCloudAssetAvailable: async ({ page }, use) => {
|
|
await use(() => {
|
|
const state = cloudUploadAssetStateByPage.get(page)
|
|
if (state) state.isUploadedAssetAvailable = true
|
|
})
|
|
}
|
|
})
|
|
|
|
async function enableErrorsTab(comfyPage: ComfyPage) {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.RightSidePanel.ShowErrorsTab',
|
|
true
|
|
)
|
|
}
|
|
|
|
function getErrorOverlay(comfyPage: ComfyPage) {
|
|
return comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
|
}
|
|
|
|
async function expectNoErrorsTab(comfyPage: ComfyPage) {
|
|
await expect(getErrorOverlay(comfyPage)).toBeHidden()
|
|
|
|
const panel = new PropertiesPanelHelper(comfyPage.page)
|
|
await panel.open(comfyPage.actionbar.propertiesButton)
|
|
await expect(
|
|
panel.root.getByTestId(TestIds.propertiesPanel.errorsTab)
|
|
).toBeHidden()
|
|
}
|
|
|
|
async function delayNextUpload(comfyPage: ComfyPage) {
|
|
let releaseUpload!: () => void
|
|
let resolveUploadStarted!: () => void
|
|
const uploadStarted = new Promise<void>((resolve) => {
|
|
resolveUploadStarted = resolve
|
|
})
|
|
const release = new Promise<void>((resolve) => {
|
|
releaseUpload = resolve
|
|
})
|
|
|
|
const uploadRouteHandler = async (route: Route) => {
|
|
resolveUploadStarted()
|
|
await release
|
|
await route.continue()
|
|
}
|
|
|
|
await comfyPage.page.route('**/upload/image', uploadRouteHandler)
|
|
|
|
return {
|
|
waitForUploadStarted: () => uploadStarted,
|
|
finishUpload: async () => {
|
|
const uploadResponse = comfyPage.page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes('/upload/image') && response.status() === 200,
|
|
{ timeout: 10_000 }
|
|
)
|
|
releaseUpload()
|
|
try {
|
|
await uploadResponse
|
|
} finally {
|
|
await comfyPage.page.unroute('**/upload/image', uploadRouteHandler)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function expectLoadVideoUploading(comfyPage: ComfyPage) {
|
|
await expect
|
|
.poll(
|
|
() =>
|
|
comfyPage.page.evaluate(() =>
|
|
window.app!.graph.nodes.some(
|
|
(node) => node.type === 'LoadVideo' && node.isUploading
|
|
)
|
|
),
|
|
{ timeout: 5_000 }
|
|
)
|
|
.toBe(true)
|
|
}
|
|
|
|
async function expectNoMissingMediaDuringUpload(comfyPage: ComfyPage) {
|
|
await comfyPage.nextFrame()
|
|
await comfyPage.nextFrame()
|
|
|
|
let sawErrorOverlay = false
|
|
const startedAt = Date.now()
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
sawErrorOverlay =
|
|
sawErrorOverlay || (await getErrorOverlay(comfyPage).isVisible())
|
|
return (
|
|
!sawErrorOverlay &&
|
|
Date.now() - startedAt >= missingMediaUploadObservationMs
|
|
)
|
|
},
|
|
{
|
|
timeout: missingMediaUploadObservationMs + missingMediaUploadPollMs * 5,
|
|
intervals: [missingMediaUploadPollMs]
|
|
}
|
|
)
|
|
.toBe(true)
|
|
}
|
|
|
|
function outputHistoryJobs(): RawJobListItem[] {
|
|
return [
|
|
createRouteMockJob({
|
|
id: 'history-output-image',
|
|
preview_output: {
|
|
filename: 'ComfyUI_00001_.png',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '1',
|
|
mediaType: 'images'
|
|
}
|
|
}),
|
|
createRouteMockJob({
|
|
id: 'history-output-video',
|
|
preview_output: {
|
|
filename: 'clip.mp4',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '2',
|
|
mediaType: 'video'
|
|
}
|
|
}),
|
|
createRouteMockJob({
|
|
id: 'history-output-audio',
|
|
preview_output: {
|
|
filename: 'sound.wav',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '3',
|
|
mediaType: 'audio'
|
|
}
|
|
})
|
|
]
|
|
}
|
|
|
|
ossTest.describe(
|
|
'Errors tab - OSS missing media runtime sources',
|
|
{ tag: '@ui' },
|
|
() => {
|
|
ossTest.beforeEach(async ({ comfyPage }) => {
|
|
await enableErrorsTab(comfyPage)
|
|
})
|
|
|
|
ossTest(
|
|
'resolves annotated output media from job history',
|
|
async ({ comfyPage, jobsRoutes }) => {
|
|
await jobsRoutes.mockJobsHistory(outputHistoryJobs())
|
|
await jobsRoutes.mockJobsQueue([])
|
|
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'missing/missing_media_output_annotations'
|
|
)
|
|
|
|
await expectNoErrorsTab(comfyPage)
|
|
}
|
|
)
|
|
|
|
ossTest(
|
|
'does not surface missing media while dropped video upload is in progress',
|
|
async ({ comfyFiles, comfyPage }) => {
|
|
await comfyPage.nodeOps.clearGraph()
|
|
const delayedUpload = await delayNextUpload(comfyPage)
|
|
|
|
await comfyPage.dragDrop.dragAndDropFile(plainVideoFileName, {
|
|
dropPosition: graphDropPosition
|
|
})
|
|
await delayedUpload.waitForUploadStarted()
|
|
comfyFiles.deleteAfterTest({
|
|
filename: plainVideoFileName,
|
|
type: 'input'
|
|
})
|
|
|
|
await expectLoadVideoUploading(comfyPage)
|
|
await expectNoMissingMediaDuringUpload(comfyPage)
|
|
|
|
await delayedUpload.finishUpload()
|
|
await expect(getErrorOverlay(comfyPage)).toBeHidden()
|
|
}
|
|
)
|
|
}
|
|
)
|
|
|
|
cloudOutputTest.describe(
|
|
'Errors tab - Cloud missing media runtime sources',
|
|
{ tag: '@cloud' },
|
|
() => {
|
|
cloudOutputTest.beforeEach(async ({ comfyPage }) => {
|
|
await enableErrorsTab(comfyPage)
|
|
})
|
|
|
|
cloudOutputTest(
|
|
'resolves compact annotated output media from output assets',
|
|
async ({ cloudAssetRequests, comfyPage }) => {
|
|
await comfyPage.workflow.loadWorkflow(
|
|
'missing/missing_media_cloud_output_annotation'
|
|
)
|
|
|
|
await expect
|
|
.poll(() =>
|
|
cloudAssetRequests.some((url) =>
|
|
assetRequestIncludesTag(url, 'output')
|
|
)
|
|
)
|
|
.toBe(true)
|
|
await expectNoErrorsTab(comfyPage)
|
|
}
|
|
)
|
|
}
|
|
)
|
|
|
|
cloudUploadRaceTest.describe(
|
|
'Errors tab - Cloud missing media upload race',
|
|
{ tag: '@cloud' },
|
|
() => {
|
|
cloudUploadRaceTest.beforeEach(async ({ comfyPage }) => {
|
|
await enableErrorsTab(comfyPage)
|
|
})
|
|
|
|
cloudUploadRaceTest(
|
|
'does not surface missing media while dropped video upload is in progress',
|
|
async ({ comfyFiles, comfyPage, markUploadedCloudAssetAvailable }) => {
|
|
await comfyPage.nodeOps.clearGraph()
|
|
const delayedUpload = await delayNextUpload(comfyPage)
|
|
|
|
await comfyPage.dragDrop.dragAndDropFile(plainVideoFileName, {
|
|
dropPosition: graphDropPosition
|
|
})
|
|
await delayedUpload.waitForUploadStarted()
|
|
comfyFiles.deleteAfterTest({
|
|
filename: plainVideoFileName,
|
|
type: 'input'
|
|
})
|
|
|
|
await expectLoadVideoUploading(comfyPage)
|
|
await expectNoMissingMediaDuringUpload(comfyPage)
|
|
|
|
markUploadedCloudAssetAvailable()
|
|
await delayedUpload.finishUpload()
|
|
await expect(getErrorOverlay(comfyPage)).toBeHidden()
|
|
}
|
|
)
|
|
}
|
|
)
|