Files
ComfyUI_frontend/browser_tests/tests/propertiesPanel/errorsTabMissingModels.spec.ts
jaeone94 874b486640 fix: resolve missing resource error messages (#12646)
## Summary

Resolve missing resource error groups through the error catalog so
missing nodes, replaceable nodes, missing models, and missing media use
consistent panel and single-error overlay copy.

## Changes

- **What**: Adds missing-resource resolvers for `missing_node`,
`swap_nodes`, `missing_model`, and `missing_media` that provide
`displayMessage`, `toastTitle`, and `toastMessage` alongside the
existing group titles. The Errors tab now renders a group-level
`displayMessage` under non-execution group headers, which gives grouped
missing-resource cards the same explanatory message path used by
validation/runtime errors without adding per-row detail fields that
these grouped cards do not need.
- **What**: Moves missing node and swap node explanatory copy out of
card-local hardcoded text and into `errorCatalog.missingErrors.*` keys.
`MissingNodeCard` and `SwapNodesCard` now focus on rendering their
grouped rows and actions, while the shared error group header owns the
explanatory copy.
- **What**: Adds environment-aware copy for missing node packs and
missing models. Cloud messages explain unsupported resources and
replacement/import paths without suggesting local execution, while OSS
messages point users toward installing or downloading the missing
resources.
- **What**: Adds single-error overlay/toast copy for missing resources.
Missing media uses a concise input-focused title/message, missing models
distinguish Cloud unsupported models from OSS missing files, and missing
nodes/swap nodes use node-type-aware copy.
- **What**: Deduplicates missing node and swap node toast decisions by
distinct node type so repeated instances of the same missing/replaceable
node do not accidentally switch the single-error copy to plural copy.
- **What**: Preserves representative missing media candidate metadata so
missing media toast copy can use a human-readable node name such as
`Load Image is missing a required media file.`
- **What**: Removes unused missing-resource resolver fields such as
grouped `displayDetails`, grouped `displayItemLabel`, and the unused
`mediaTypes` source parameter after deciding those fields do not fit
grouped missing-resource cards.
- **Breaking**: None.
- **Dependencies**: None.

## Review Focus

- Missing resource groups are still grouped cards. This PR intentionally
gives them group-level display and toast copy, but does not split
missing resources into one error item per underlying candidate.
- Missing resource count semantics are intentionally not normalized
here. Error overlay totals, store counts, and grouped card counts still
follow the existing behavior; a follow-up PR can define those count
units separately.
- The Cloud/OSS message variants remain explicit in the resolver instead
of being abstracted into a generic variant helper. That keeps this PR
focused on the messaging behavior and avoids a broader resolver
refactor.
- Only `src/locales/en/main.json` is updated directly. Other locales
should be synced by the existing localization flow.

## Screenshots (if applicable)

<img width="668" height="245" alt="스크린샷 2026-06-05 오전 3 16 49"
src="https://github.com/user-attachments/assets/98b50ac3-67e1-438d-8c37-e06c7bf465ee"
/>
<img width="666" height="195" alt="스크린샷 2026-06-05 오전 3 16 58"
src="https://github.com/user-attachments/assets/92da95b1-03d6-4739-97e6-c573982bfec9"
/>
<img width="505" height="358" alt="스크린샷 2026-06-05 오전 3 17 27"
src="https://github.com/user-attachments/assets/4d0e1a6e-13b9-4097-9fb5-19fe0c5331dc"
/>
<img width="507" height="324" alt="스크린샷 2026-06-05 오전 3 17 44"
src="https://github.com/user-attachments/assets/054e42f8-0d0c-44b5-8a67-e467fc04f1fc"
/>


## Validation

- `pnpm format`
- `pnpm lint`
- `pnpm test:unit src/platform/errorCatalog/errorMessageResolver.test.ts
src/components/rightSidePanel/errors/useErrorGroups.test.ts
src/components/rightSidePanel/errors/TabErrors.test.ts`
- `pnpm typecheck`
- push hook: `knip --cache`
2026-06-05 05:25:46 +00:00

161 lines
5.1 KiB
TypeScript

import { expect } 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'
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 with referencing node count', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
const modelsGroup = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelsGroup
)
await expect(modelsGroup).toContainText(/fake_model\.safetensors\s*\(\d+\)/)
})
test('Should expand model row to show referencing nodes', async ({
comfyPage
}) => {
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/missing_models_with_nodes'
)
const locateButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelLocate
)
await expect(locateButton.first()).toBeHidden()
const expandButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelExpand
)
await expect(expandButton.first()).toBeVisible()
await expandButton.first().click()
await expect(locateButton.first()).toBeVisible()
})
test('Should copy model name to clipboard', async ({ comfyPage }) => {
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
await interceptClipboardWrite(comfyPage.page)
const copyButton = comfyPage.page.getByTestId(
TestIds.dialogs.missingModelCopyName
)
await expect(copyButton.first()).toBeVisible()
await copyButton.first().dispatchEvent('click')
const copiedText = await getClipboardText(comfyPage.page)
expect(copiedText).toContain('fake_model.safetensors')
})
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.getByTestId(
TestIds.dialogs.missingModelCopyUrl
)
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()
})
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()
})
})
})