mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
## Summary Add detection and resolution UI for missing image/video/audio inputs (LoadImage, LoadVideo, LoadAudio nodes) in the Errors tab, mirroring the existing missing model pipeline. ## Changes - **What**: New `src/platform/missingMedia/` module — scan pipeline detects missing media files on workflow load (sync for OSS, async for cloud), surfaces them in the error tab with upload dropzone, thumbnail library select, and 2-step confirm flow - **Detection**: `scanAllMediaCandidates()` checks combo widget values against options; cloud path defers to `verifyCloudMediaCandidates()` via `assetsStore.updateInputs()` - **UI**: `MissingMediaCard` groups by media type; `MissingMediaRow` shows node name (single) or filename+count (multiple), upload dropzone with drag & drop, `MissingMediaLibrarySelect` with image/video thumbnails - **Resolution**: Upload via `/upload/image` API or select from library → status card → checkmark confirm → widget value applied, item removed from error list - **Integration**: `executionErrorStore` aggregates into `hasAnyError`/`totalErrorCount`; `useNodeErrorFlagSync` flags nodes on canvas; `useErrorGroups` renders in error tab - **Shared**: Extract `ACCEPTED_IMAGE_TYPES`/`ACCEPTED_VIDEO_TYPES` to `src/utils/mediaUploadUtil.ts`; extract `resolveComboValues` to `src/utils/litegraphUtil.ts` (shared across missingMedia + missingModel scan) - **Reverse clearing**: Widget value changes on nodes auto-remove corresponding missing media errors (via `clearWidgetRelatedErrors`) ## Testing ### Unit tests (22 tests) - `missingMediaScan.test.ts` (12): groupCandidatesByName, groupCandidatesByMediaType (ordering, multi-name), verifyCloudMediaCandidates (missing/present, abort before/after updateInputs, already resolved true/false, no-pending skip, updateInputs spy) - `missingMediaStore.test.ts` (10): setMissingMedia, clearMissingMedia (full lifecycle with interaction state), missingMediaNodeIds, hasMissingMediaOnNode, removeMissingMediaByWidget (match/no-match/last-entry), createVerificationAbortController ### E2E tests (10 scenarios in `missingMedia.spec.ts`) - Detection: error overlay shown, Missing Inputs group in errors tab, correct row count, dropzone + library select visibility, no false positive for valid media - Upload flow: file picker → uploading status card → confirm → row removed - Library select: dropdown → selected status card → confirm → row removed - Cancel: pending selection → returns to upload/library UI - All resolved: Missing Inputs group disappears - Locate node: canvas pans to missing media node ## Review Focus - Cloud verification path: `verifyCloudMediaCandidates` compares widget value against `asset_hash` — implicit contract - 2-step confirm mirrors missing model pattern (`pendingSelection` → confirm/cancel) - Event propagation guard on dropzone (`@drop.prevent.stop`) to prevent canvas LoadImage node creation - `clearAllErrors()` intentionally does NOT clear missing media (same as missing models — preserves pending repairs) - `runMissingMediaPipeline` is now `async` and `await`-ed, matching model pipeline ## Test plan - [x] OSS: load workflow with LoadImage referencing non-existent file → error tab shows it - [x] Upload file via dropzone → status card shows "Uploaded" → confirm → widget updated, error removed - [x] Select from library with thumbnail preview → confirm → widget updated, error removed - [x] Cancel pending selection → returns to upload/library UI - [x] Load workflow with valid images → no false positives - [x] Click locate-node → canvas navigates to the node - [x] Multiple nodes referencing different missing files → correct row count - [x] Widget value change on node → missing media error auto-removed ## Screenshots https://github.com/user-attachments/assets/631c0cb0-9706-4db2-8615-f24a4c3fe27d
226 lines
6.8 KiB
TypeScript
226 lines
6.8 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import type { ComfyPage } from '../fixtures/ComfyPage'
|
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
|
import { TestIds } from '../fixtures/selectors'
|
|
|
|
async function loadMissingMediaAndOpenErrorsTab(
|
|
comfyPage: ComfyPage,
|
|
workflow = 'missing/missing_media_single'
|
|
) {
|
|
await comfyPage.workflow.loadWorkflow(workflow)
|
|
|
|
const errorOverlay = comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
|
await expect(errorOverlay).toBeVisible()
|
|
|
|
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlaySeeErrors).click()
|
|
await expect(errorOverlay).not.toBeVisible()
|
|
}
|
|
|
|
async function uploadFileViaDropzone(comfyPage: ComfyPage) {
|
|
const dropzone = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingMediaUploadDropzone
|
|
)
|
|
const [fileChooser] = await Promise.all([
|
|
comfyPage.page.waitForEvent('filechooser'),
|
|
dropzone.click()
|
|
])
|
|
await fileChooser.setFiles(comfyPage.assetPath('test_upload_image.png'))
|
|
}
|
|
|
|
async function confirmPendingSelection(comfyPage: ComfyPage) {
|
|
const confirmButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingMediaConfirmButton
|
|
)
|
|
await expect(confirmButton).toBeEnabled()
|
|
await confirmButton.click()
|
|
}
|
|
|
|
function getMediaRow(comfyPage: ComfyPage) {
|
|
return comfyPage.page.getByTestId(TestIds.dialogs.missingMediaRow)
|
|
}
|
|
|
|
function getStatusCard(comfyPage: ComfyPage) {
|
|
return comfyPage.page.getByTestId(TestIds.dialogs.missingMediaStatusCard)
|
|
}
|
|
|
|
function getDropzone(comfyPage: ComfyPage) {
|
|
return comfyPage.page.getByTestId(TestIds.dialogs.missingMediaUploadDropzone)
|
|
}
|
|
|
|
test.describe('Missing media inputs in Error Tab', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.RightSidePanel.ShowErrorsTab',
|
|
true
|
|
)
|
|
})
|
|
|
|
test.describe('Detection', () => {
|
|
test('Shows error overlay when workflow has missing media inputs', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('missing/missing_media_single')
|
|
|
|
const errorOverlay = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.errorOverlay
|
|
)
|
|
await expect(errorOverlay).toBeVisible()
|
|
|
|
const messages = errorOverlay.getByTestId(
|
|
TestIds.dialogs.errorOverlayMessages
|
|
)
|
|
await expect(messages).toBeVisible()
|
|
await expect(messages).toHaveText(/missing required inputs/i)
|
|
})
|
|
|
|
test('Shows missing media group in errors tab after clicking See Errors', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Shows correct number of missing media rows', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/missing_media_multiple'
|
|
)
|
|
|
|
await expect(getMediaRow(comfyPage)).toHaveCount(2)
|
|
})
|
|
|
|
test('Shows upload dropzone and library select for each missing item', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
|
|
await expect(getDropzone(comfyPage)).toBeVisible()
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaLibrarySelect)
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Does not show error overlay when all media inputs exist', async ({
|
|
comfyPage
|
|
}) => {
|
|
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay)
|
|
).not.toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Upload flow (2-step confirm)', () => {
|
|
test('Upload via file picker shows status card then allows confirm', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
await uploadFileViaDropzone(comfyPage)
|
|
|
|
await expect(getStatusCard(comfyPage)).toBeVisible()
|
|
|
|
await confirmPendingSelection(comfyPage)
|
|
await expect(getMediaRow(comfyPage)).toHaveCount(0)
|
|
})
|
|
})
|
|
|
|
test.describe('Library select flow (2-step confirm)', () => {
|
|
test('Selecting from library shows status card then allows confirm', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
|
|
const librarySelect = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingMediaLibrarySelect
|
|
)
|
|
await librarySelect.getByRole('combobox').click()
|
|
|
|
const optionCount = await comfyPage.page.getByRole('option').count()
|
|
if (optionCount === 0) {
|
|
test.skip()
|
|
return
|
|
}
|
|
|
|
await comfyPage.page.getByRole('option').first().click()
|
|
|
|
await expect(getStatusCard(comfyPage)).toBeVisible()
|
|
|
|
await confirmPendingSelection(comfyPage)
|
|
await expect(getMediaRow(comfyPage)).toHaveCount(0)
|
|
})
|
|
})
|
|
|
|
test.describe('Cancel selection', () => {
|
|
test('Cancelling pending selection returns to upload/library UI', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
await uploadFileViaDropzone(comfyPage)
|
|
|
|
await expect(getStatusCard(comfyPage)).toBeVisible()
|
|
await expect(getDropzone(comfyPage)).not.toBeVisible()
|
|
|
|
await comfyPage.page
|
|
.getByTestId(TestIds.dialogs.missingMediaCancelButton)
|
|
.click()
|
|
|
|
await expect(getStatusCard(comfyPage)).not.toBeVisible()
|
|
await expect(getDropzone(comfyPage)).toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('All resolved', () => {
|
|
test('Missing Inputs group disappears when all items are resolved', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
await uploadFileViaDropzone(comfyPage)
|
|
await confirmPendingSelection(comfyPage)
|
|
|
|
await expect(
|
|
comfyPage.page.getByTestId(TestIds.dialogs.missingMediaGroup)
|
|
).not.toBeVisible()
|
|
})
|
|
})
|
|
|
|
test.describe('Locate node', () => {
|
|
test('Locate button navigates canvas to the missing media node', async ({
|
|
comfyPage
|
|
}) => {
|
|
await loadMissingMediaAndOpenErrorsTab(comfyPage)
|
|
|
|
const offsetBefore = await comfyPage.page.evaluate(() => {
|
|
const canvas = window['app']?.canvas
|
|
return canvas?.ds?.offset
|
|
? [canvas.ds.offset[0], canvas.ds.offset[1]]
|
|
: null
|
|
})
|
|
|
|
const locateButton = comfyPage.page.getByTestId(
|
|
TestIds.dialogs.missingMediaLocateButton
|
|
)
|
|
await expect(locateButton).toBeVisible()
|
|
await locateButton.click()
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
return await comfyPage.page.evaluate(() => {
|
|
const canvas = window['app']?.canvas
|
|
return canvas?.ds?.offset
|
|
? [canvas.ds.offset[0], canvas.ds.offset[1]]
|
|
: null
|
|
})
|
|
})
|
|
.not.toEqual(offsetBefore)
|
|
})
|
|
})
|
|
})
|