Files
ComfyUI_frontend/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
jaeone94 6d43320b93 Simplify missing model error presentation (#12793)
## Summary

Simplifies the Missing Models error card as the fifth slice of the
catalog-driven error-tab redesign. This PR is intentionally larger than
the previous slices because Missing Models is the only remaining error
type where the card UI, OSS download flow, Cloud import flow, and shared
model-import dialog all have to move together to preserve the resolution
path.

The high-level goal is to make Missing Models behave like the other
simplified error cards: show the exact missing item, show the affected
nodes, keep locate actions predictable, and only expose actions that can
actually resolve the problem.

This follows the staged error-tab cleanup plan:

1. #12683 refined validation, runtime, and prompt error presentation.
2. #12705 simplified missing media error presentation.
3. #12735 simplified missing node pack error presentation.
4. #12768 simplified swap node error presentation.
5. This PR simplifies missing model presentation and the model-import
handoff.

## Why This PR Is Larger

Missing Models has more resolution paths than the previous error groups:

- OSS can refresh model state, download individual models, and download
all available models.
- Cloud cannot download directly from the panel; it resolves supported
rows through the model import dialog.
- Some Cloud rows cannot be resolved through import at all because the
node/widget cannot consume imported model assets.
- Importing from Cloud needs to know the originating missing-model row
so it can lock the expected model type and apply the imported model back
to the affected widgets.
- Already-imported files can still be unusable if they were imported
under a different model type than the missing node expects.

Because of those constraints, splitting the card layout from the dialog
handoff would leave either a misleading Import button or an import
dialog that does not know what it is resolving. This PR keeps that
behavior in one reviewable unit.

## User-Facing Behavior

### Shared Missing Models Card

- Replaces the older grouped presentation with compact model rows.
- Shows each missing model as the primary row label.
- Shows model metadata as a smaller sublabel instead of using large
section headers.
- Keeps locate-node controls visually consistent with the other
simplified error cards.
- Keeps rows expandable when multiple nodes reference the same missing
model.
- Shows the affected node rows under expanded models.
- Allows single-reference rows to locate the affected node without
rendering an extra duplicate child row.
- Keeps unknown rows visible, including their affected nodes, instead of
silently hiding them.
- Removes the old library-select UI from the Missing Models card.

### OSS Behavior

- Keeps the refresh action available from the Missing Models group
header.
- Keeps individual Download actions for downloadable models.
- Moves file size out of the Download button label and into the row
sublabel.
- Keeps Download all when multiple downloadable models are available.
- Places Download all at the bottom of the card rather than competing
with the group header.
- Leaves rows without a download URL as non-downloadable instead of
rendering a broken action.

### Cloud Behavior

- Shows Import only for missing models that can be resolved by importing
a model asset of the required type.
- Separates models that cannot be resolved through Cloud import into an
Import Not Supported section.
- Gives unsupported rows a direct explanation: nodes referencing those
models do not support imported models, so users need to open the node
and choose a supported built-in model or replace the node with a
supported loader.
- Treats unknown model type/directory as unsupported for Cloud import,
because the import dialog cannot lock a valid model type and the node
cannot safely consume the imported asset.
- Keeps affected nodes visible in the unsupported section so users still
have a path to locate and replace the node manually.

## Cloud Import Dialog Changes

The shared model import dialog now accepts missing-model context when
opened from the Missing Models card.

When that context is present:

- The dialog shows which missing model will be replaced.
- The dialog lists the affected node/widget references that will be
updated.
- The model type selector is locked to the required model
directory/type.
- The Back/import-another path is disabled when it would break the
targeted missing-model flow.
- Import progress can be associated with the originating missing-model
row.
- After import completion, matching missing-model references are applied
automatically where possible.
- If the selected file is already imported under an incompatible model
type, the dialog shows a targeted failure state explaining why this
import cannot resolve the missing model.

This keeps the generic import dialog reusable while adding only the
context-specific behavior needed for Missing Models.

## Implementation Notes

- `MissingModelCard.vue` owns the card-level grouping and OSS/Cloud
section decisions.
- `MissingModelRow.vue` owns per-model row rendering, expansion, locate
actions, import/download actions, and row-level progress states.
- `useMissingModelInteractions.ts` remains the interaction layer for
locating nodes and applying resolved model selections.
- `UploadModelDialog.vue`, `UploadModelConfirmation.vue`,
`UploadModelFooter.vue`, `UploadModelProgress.vue`, and
`useUploadModelWizard.ts` receive the missing-model context needed by
the Cloud import handoff.
- `MissingModelLibrarySelect.vue` is removed because the simplified card
no longer exposes that inline selection path.
- Locale and selector changes are limited to the new simplified
row/section states and removed unused Missing Models strings.

## Tests Added / Updated

- Unit coverage for Missing Models card grouping and row states.
- Unit coverage for importable vs unsupported Cloud rows.
- Unit coverage for model row expansion, locate actions, progress
display, and action availability.
- Unit coverage for upload confirmation/footer/progress behavior when a
missing-model context is present.
- Unit coverage for incompatible already-imported model handling.
- E2E coverage for OSS Missing Models presentation.
- E2E coverage for mode-aware Missing Models interactions.
- Cloud E2E coverage for importable rows vs Import Not Supported rows.
- Cloud E2E coverage for opening the import dialog with missing-model
replacement context.

## Review Focus

- Cloud import eligibility: unsupported or unknown model rows should not
expose Import as if the row can be resolved automatically.
- Missing-model context in the import dialog: the required model type
should be locked, and the affected node/widget references should be
clear.
- OSS parity: OSS should keep refresh, individual Download, and Download
all while visually matching the simplified Cloud card where possible.
- Narrow side panel behavior: row labels may wrap, but link, primary
action, and locate controls should not overlap.
- Scope boundaries: this PR intentionally does not redesign Missing Node
Pack / Swap Node / Missing Media again; visual parity issues shared
across those cards can be handled in a follow-up unification pass if
needed.

## Validation

- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- Related unit tests: 8 files / 84 tests passed
- `pnpm build`
- OSS Missing Models E2E: `errorsTabMissingModels.spec.ts` passed, 8/8
- Mode-aware Missing Models E2E subset passed, 11/11, excluding
unrelated local paste clipboard cases
- `pnpm build:cloud`
- Cloud Missing Models E2E: `errorsTabCloudMissingModels.spec.ts`
passed, 3/3
- Final Claude review: no Blocker or Major findings

## Breaking / Dependencies

- Breaking: none.
- Dependencies: none.

## Screenshots
OSS
<img width="575" height="393" alt="스크린샷 2026-06-12 오전 12 25 27"
src="https://github.com/user-attachments/assets/f5c44f95-711a-4d3d-99bd-f39ac2bb2012"
/>
<img width="659" height="351" alt="스크린샷 2026-06-12 오전 12 24 37"
src="https://github.com/user-attachments/assets/4bb65a47-c1aa-408b-836b-a1998412f815"
/>

Cloud

<img width="688" height="357" alt="스크린샷 2026-06-12 오전 12 23 59"
src="https://github.com/user-attachments/assets/9330a7e7-9f22-420f-82b3-dde0fb2b3dd1"
/>
<img width="531" height="437" alt="스크린샷 2026-06-12 오전 12 21 13"
src="https://github.com/user-attachments/assets/734bd911-f6f7-4872-8868-bb927ddeedd8"
/>

New import model flow



https://github.com/user-attachments/assets/c094c670-62b9-47ce-bfe1-2d09f4f7359d
2026-06-15 12:17:31 +00:00

175 lines
5.7 KiB
TypeScript

import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import {
interceptClipboardWrite,
getClipboardText
} from '@e2e/fixtures/utils/clipboardSpy'
import {
cleanupFakeModel,
loadWorkflowAndOpenErrorsTab
} from '@e2e/fixtures/helpers/ErrorsTabHelper'
const FAKE_MODEL_NAME = 'fake_model.safetensors'
function getModelLabel(group: Locator, modelName: string = FAKE_MODEL_NAME) {
return group.getByRole('button', { name: modelName, exact: true })
}
async function expectReferenceBadge(group: Locator, count: number) {
await expect(
group.getByTestId(TestIds.dialogs.missingModelReferenceCount)
).toHaveText(String(count))
}
test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await cleanupFakeModel(comfyPage)
})
test('Should show missing models group in errors tab', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const missingModelsGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(missingModelsGroup).toBeVisible()
await expect(
missingModelsGroup.getByTestId(TestIds.dialogs.errorGroupDisplayMessage)
).toHaveText(/\S/)
})
test('Should display model name and metadata', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const modelsGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(getModelLabel(modelsGroup)).toBeVisible()
await expect(modelsGroup.getByText('checkpoints')).toBeVisible()
})
test('Should expand model row to show referencing nodes', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_models_with_nodes'
)
const modelsGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
const expandButton = modelsGroup.getByTestId(
TestIds.dialogs.missingModelExpand
)
await expect(expandButton.first()).toBeVisible()
await expectReferenceBadge(modelsGroup, 2)
await expandButton.first().click()
await expect(
modelsGroup.getByTestId(TestIds.dialogs.missingModelLocate)
).toHaveCount(2)
})
test('Should copy model URL to clipboard', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await interceptClipboardWrite(comfyPage.page)
const copyButton = comfyPage.page.getByRole('button', {
name: 'Copy URL'
})
await expect(copyButton.first()).toBeVisible()
await copyButton.first().dispatchEvent('click')
const copiedText = await getClipboardText(comfyPage.page)
expect(copiedText).toContain('/api/devtools/')
})
test.describe('OSS-specific', { tag: '@oss' }, () => {
test('Should show Copy URL button for non-asset models', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const copyUrlButton = comfyPage.page.getByRole('button', {
name: 'Copy URL'
})
await expect(copyUrlButton.first()).toBeVisible()
})
test('Should show Download button for downloadable models', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const downloadButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelDownload
)
await expect(downloadButton.first()).toBeVisible()
await expect(downloadButton.first()).toHaveText('Download')
})
test('Should render Download all and Refresh actions for one downloadable model', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelActions)
).toBeVisible()
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelDownloadAll)
).toBeVisible()
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelRefresh)
).toBeVisible()
})
test('Should clear resolved missing model when Refresh is clicked', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await comfyPage.page.route(/\/object_info$/, async (route) => {
const response = await route.fetch()
const objectInfo = await response.json()
const ckptName =
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
await route.fulfill({ response, json: objectInfo })
})
const objectInfoResponse = comfyPage.page.waitForResponse((response) => {
const url = new URL(response.url())
return url.pathname.endsWith('/object_info') && response.ok()
})
const modelFoldersResponse = comfyPage.page.waitForResponse(
(response) => {
const url = new URL(response.url())
return url.pathname.endsWith('/experiment/models') && response.ok()
}
)
const refreshButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelRefresh
)
await Promise.all([
objectInfoResponse,
modelFoldersResponse,
refreshButton.click()
])
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
).toBeHidden()
})
})
})