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
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
/**
|
|
* Centralized test selectors for browser tests.
|
|
* Use data-testid attributes for stable selectors.
|
|
*/
|
|
|
|
export const TestIds = {
|
|
sidebar: {
|
|
toolbar: 'side-toolbar',
|
|
nodeLibrary: 'node-library-tree',
|
|
nodeLibrarySearch: 'node-library-search',
|
|
workflows: 'workflows-sidebar',
|
|
modeToggle: 'mode-toggle'
|
|
},
|
|
tree: {
|
|
folder: 'tree-folder',
|
|
leaf: 'tree-leaf',
|
|
node: 'tree-node'
|
|
},
|
|
canvas: {
|
|
main: 'graph-canvas',
|
|
contextMenu: 'canvas-context-menu',
|
|
toggleMinimapButton: 'toggle-minimap-button',
|
|
closeMinimapButton: 'close-minimap-button',
|
|
toggleLinkVisibilityButton: 'toggle-link-visibility-button',
|
|
zoomControlsButton: 'zoom-controls-button',
|
|
zoomInAction: 'zoom-in-action',
|
|
zoomOutAction: 'zoom-out-action',
|
|
zoomToFitAction: 'zoom-to-fit-action',
|
|
zoomPercentageInput: 'zoom-percentage-input'
|
|
},
|
|
dialogs: {
|
|
settings: 'settings-dialog',
|
|
settingsContainer: 'settings-container',
|
|
settingsTabAbout: 'settings-tab-about',
|
|
confirm: 'confirm-dialog',
|
|
errorOverlay: 'error-overlay',
|
|
errorOverlaySeeErrors: 'error-overlay-see-errors',
|
|
errorOverlayDismiss: 'error-overlay-dismiss',
|
|
errorOverlayMessages: 'error-overlay-messages',
|
|
runtimeErrorPanel: 'runtime-error-panel',
|
|
missingNodeCard: 'missing-node-card',
|
|
errorCardFindOnGithub: 'error-card-find-on-github',
|
|
errorCardCopy: 'error-card-copy',
|
|
about: 'about-panel',
|
|
whatsNewSection: 'whats-new-section',
|
|
missingNodePacksGroup: 'error-group-missing-node',
|
|
missingModelsGroup: 'error-group-missing-model',
|
|
missingMediaGroup: 'error-group-missing-media',
|
|
missingMediaRow: 'missing-media-row',
|
|
missingMediaUploadDropzone: 'missing-media-upload-dropzone',
|
|
missingMediaLibrarySelect: 'missing-media-library-select',
|
|
missingMediaStatusCard: 'missing-media-status-card',
|
|
missingMediaConfirmButton: 'missing-media-confirm-button',
|
|
missingMediaCancelButton: 'missing-media-cancel-button',
|
|
missingMediaLocateButton: 'missing-media-locate-button'
|
|
},
|
|
keybindings: {
|
|
presetMenu: 'keybinding-preset-menu'
|
|
},
|
|
topbar: {
|
|
queueButton: 'queue-button',
|
|
queueModeMenuTrigger: 'queue-mode-menu-trigger',
|
|
saveButton: 'save-workflow-button',
|
|
subscribeButton: 'topbar-subscribe-button'
|
|
},
|
|
nodeLibrary: {
|
|
bookmarksSection: 'node-library-bookmarks-section'
|
|
},
|
|
propertiesPanel: {
|
|
root: 'properties-panel'
|
|
},
|
|
subgraphEditor: {
|
|
toggle: 'subgraph-editor-toggle',
|
|
shownSection: 'subgraph-editor-shown-section',
|
|
hiddenSection: 'subgraph-editor-hidden-section',
|
|
widgetToggle: 'subgraph-widget-toggle',
|
|
widgetLabel: 'subgraph-widget-label',
|
|
iconLink: 'icon-link',
|
|
iconEye: 'icon-eye',
|
|
widgetActionsMenuButton: 'widget-actions-menu-button'
|
|
},
|
|
node: {
|
|
titleInput: 'node-title-input'
|
|
},
|
|
selectionToolbox: {
|
|
colorPickerButton: 'color-picker-button',
|
|
colorPickerCurrentColor: 'color-picker-current-color',
|
|
colorBlue: 'blue',
|
|
colorRed: 'red'
|
|
},
|
|
menu: {
|
|
moreMenuContent: 'more-menu-content'
|
|
},
|
|
widgets: {
|
|
container: 'node-widgets',
|
|
widget: 'node-widget',
|
|
decrement: 'decrement',
|
|
increment: 'increment',
|
|
domWidgetTextarea: 'dom-widget-textarea',
|
|
subgraphEnterButton: 'subgraph-enter-button'
|
|
},
|
|
builder: {
|
|
footerNav: 'builder-footer-nav',
|
|
saveButton: 'builder-save-button',
|
|
saveAsButton: 'builder-save-as-button',
|
|
saveGroup: 'builder-save-group',
|
|
saveAsChevron: 'builder-save-as-chevron',
|
|
ioItem: 'builder-io-item',
|
|
ioItemTitle: 'builder-io-item-title',
|
|
widgetActionsMenu: 'widget-actions-menu',
|
|
opensAs: 'builder-opens-as'
|
|
},
|
|
breadcrumb: {
|
|
subgraph: 'subgraph-breadcrumb'
|
|
},
|
|
templates: {
|
|
content: 'template-workflows-content',
|
|
workflowCard: (id: string) => `template-workflow-${id}`
|
|
},
|
|
user: {
|
|
currentUserIndicator: 'current-user-indicator'
|
|
},
|
|
queue: {
|
|
overlayToggle: 'queue-overlay-toggle',
|
|
clearHistoryAction: 'clear-history-action'
|
|
},
|
|
errors: {
|
|
imageLoadError: 'error-loading-image',
|
|
videoLoadError: 'error-loading-video'
|
|
}
|
|
} as const
|
|
|
|
/**
|
|
* Helper type for accessing nested TestIds (excludes function values)
|
|
*/
|
|
export type TestIdValue =
|
|
| (typeof TestIds.sidebar)[keyof typeof TestIds.sidebar]
|
|
| (typeof TestIds.tree)[keyof typeof TestIds.tree]
|
|
| (typeof TestIds.canvas)[keyof typeof TestIds.canvas]
|
|
| (typeof TestIds.dialogs)[keyof typeof TestIds.dialogs]
|
|
| (typeof TestIds.keybindings)[keyof typeof TestIds.keybindings]
|
|
| (typeof TestIds.topbar)[keyof typeof TestIds.topbar]
|
|
| (typeof TestIds.nodeLibrary)[keyof typeof TestIds.nodeLibrary]
|
|
| (typeof TestIds.propertiesPanel)[keyof typeof TestIds.propertiesPanel]
|
|
| (typeof TestIds.node)[keyof typeof TestIds.node]
|
|
| (typeof TestIds.selectionToolbox)[keyof typeof TestIds.selectionToolbox]
|
|
| (typeof TestIds.widgets)[keyof typeof TestIds.widgets]
|
|
| (typeof TestIds.builder)[keyof typeof TestIds.builder]
|
|
| (typeof TestIds.breadcrumb)[keyof typeof TestIds.breadcrumb]
|
|
| Exclude<
|
|
(typeof TestIds.templates)[keyof typeof TestIds.templates],
|
|
(id: string) => string
|
|
>
|
|
| (typeof TestIds.user)[keyof typeof TestIds.user]
|
|
| (typeof TestIds.menu)[keyof typeof TestIds.menu]
|
|
| (typeof TestIds.subgraphEditor)[keyof typeof TestIds.subgraphEditor]
|
|
| (typeof TestIds.queue)[keyof typeof TestIds.queue]
|
|
| (typeof TestIds.errors)[keyof typeof TestIds.errors]
|