Files
ComfyUI_frontend/browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
Glary-Bot 3e56bc925f lint: enable oxlint func-style rule and convert function expressions
Enables eslint/func-style in oxlint with declaration mode to enforce
function declarations over function expressions and arrow expressions
assigned to variables. Vendored litegraph is excluded via override.

Converts existing function expressions and variable-initialized arrow
functions to function declarations across src/, browser_tests/, apps/,
packages/, and scripts/. Adjusts a handful of let-reassignable callback
placeholders, narrowed variable patterns, and typed widget constructors
to keep type safety intact.

Pre-existing type-aware oxlint errors (no-console, no-floating-promises,
no-explicit-any) are unchanged from main.
2026-05-20 05:44:45 +00:00

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)
async function assetsRouteHandler(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
})
async function uploadRouteHandler(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()
}
)
}
)