mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 13:32:11 +00:00
## Summary Adds browser coverage for the missing-media runtime paths introduced by #12069 and #12111: - OSS: annotated `[output]` media is resolved from job history. - Cloud: compact `[output]` media is resolved from output assets. - OSS and Cloud: dropped video uploads do not surface a missing-media error while the upload is still in progress. This PR is now rebased directly onto `main`; the parent fix PRs have been squash-merged, so this branch only contains the E2E coverage commit. ## Test Fixtures - Adds workflow fixtures for OSS spaced output annotations and Cloud compact output annotations. - Adds a small plain MP4 fixture for video drag/drop upload coverage. ## Validation - `pnpm exec oxfmt --check browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts browser_tests/assets/missing/missing_media_cloud_output_annotation.json browser_tests/assets/missing/missing_media_output_annotations.json` - `pnpm typecheck:browser` - `pnpm exec oxlint browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts --type-aware` - `git diff --check origin/main..HEAD` Note: before the rebase, the Cloud project target for this spec passed locally. Local OSS project execution against the currently running Cloud dist did not reach the test body because `ComfyPage.waitForAppReady` timed out in `beforeEach`. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-12126-test-cover-missing-media-runtime-sources-35d6d73d365081f0a981c02f33c0ff84) by [Unito](https://www.unito.io)
358 lines
10 KiB
TypeScript
358 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 { jobsApiMockFixture } from '@e2e/fixtures/jobsApiMockFixture'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
import {
|
|
createMockJob,
|
|
createMockJobRecords
|
|
} from '@e2e/fixtures/utils/jobFixtures'
|
|
import { PropertiesPanelHelper } from '@e2e/tests/propertiesPanel/PropertiesPanelHelper'
|
|
|
|
const ossTest = mergeTests(comfyPageFixture, jobsApiMockFixture)
|
|
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() {
|
|
return createMockJobRecords([
|
|
createMockJob({
|
|
id: 'history-output-image',
|
|
preview_output: {
|
|
filename: 'ComfyUI_00001_.png',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '1',
|
|
mediaType: 'images'
|
|
}
|
|
}),
|
|
createMockJob({
|
|
id: 'history-output-video',
|
|
preview_output: {
|
|
filename: 'clip.mp4',
|
|
subfolder: '',
|
|
type: 'output',
|
|
nodeId: '2',
|
|
mediaType: 'video'
|
|
}
|
|
}),
|
|
createMockJob({
|
|
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, jobsApi }) => {
|
|
await jobsApi.mockJobs(outputHistoryJobs())
|
|
|
|
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()
|
|
}
|
|
)
|
|
}
|
|
)
|