mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-06 07:51:57 +00:00
## 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`
173 lines
5.9 KiB
TypeScript
173 lines
5.9 KiB
TypeScript
import {
|
|
comfyPageFixture as test,
|
|
comfyExpect as expect
|
|
} from '@e2e/fixtures/ComfyPage'
|
|
import {
|
|
mockNodeReplacements,
|
|
mockNodeReplacementsSingle
|
|
} from '@e2e/fixtures/data/nodeReplacements'
|
|
import { loadWorkflowAndOpenErrorsTab } from '@e2e/fixtures/helpers/ErrorsTabHelper'
|
|
import {
|
|
getSwapNodesGroup,
|
|
setupNodeReplacement
|
|
} from '@e2e/fixtures/helpers/NodeReplacementHelper'
|
|
import { TestIds } from '@e2e/fixtures/selectors'
|
|
|
|
const renderModes = [
|
|
{ name: 'vue nodes', vueNodesEnabled: true },
|
|
{ name: 'litegraph', vueNodesEnabled: false }
|
|
] as const
|
|
|
|
test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
|
|
for (const mode of renderModes) {
|
|
test.describe(`(${mode.name})`, () => {
|
|
test.describe('Single replacement', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.VueNodes.Enabled',
|
|
mode.vueNodesEnabled
|
|
)
|
|
await setupNodeReplacement(comfyPage, mockNodeReplacementsSingle)
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/node_replacement_simple'
|
|
)
|
|
})
|
|
|
|
test('Swap Nodes group appears in errors tab for replaceable nodes', async ({
|
|
comfyPage
|
|
}) => {
|
|
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
|
await expect(swapGroup).toBeVisible()
|
|
await expect(
|
|
swapGroup.getByTestId(TestIds.dialogs.errorGroupDisplayMessage)
|
|
).toHaveText(/\S/)
|
|
await expect(swapGroup).toContainText('E2E_OldSampler')
|
|
await expect(
|
|
swapGroup.getByRole('button', { name: 'Replace All', exact: true })
|
|
).toBeVisible()
|
|
})
|
|
|
|
test('Replace Node replaces a single group in-place', async ({
|
|
comfyPage
|
|
}) => {
|
|
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
|
await swapGroup.getByRole('button', { name: /replace node/i }).click()
|
|
await expect(swapGroup).toBeHidden()
|
|
|
|
const workflow = await comfyPage.workflow.getExportedWorkflow()
|
|
expect(
|
|
workflow.nodes,
|
|
'Node count should be unchanged after in-place replacement'
|
|
).toHaveLength(2)
|
|
|
|
const nodeTypes = workflow.nodes.map((n) => n.type)
|
|
expect(nodeTypes).not.toContain('E2E_OldSampler')
|
|
expect(nodeTypes).toContain('KSampler')
|
|
|
|
const ksampler = workflow.nodes.find((n) => n.type === 'KSampler')
|
|
expect(
|
|
ksampler?.id,
|
|
'Replaced node should keep the original id'
|
|
).toBe(1)
|
|
|
|
const linkFromReplacedToDecode = workflow.links?.find(
|
|
(l) => l[1] === 1 && l[3] === 2
|
|
)
|
|
expect(
|
|
linkFromReplacedToDecode,
|
|
'Output link from replaced node to VAEDecode should be preserved'
|
|
).toBeDefined()
|
|
})
|
|
|
|
test('Widget values are preserved after replacement', async ({
|
|
comfyPage
|
|
}) => {
|
|
await getSwapNodesGroup(comfyPage.page)
|
|
.getByRole('button', { name: /replace node/i })
|
|
.click()
|
|
|
|
const workflow = await comfyPage.workflow.getExportedWorkflow()
|
|
const ksampler = workflow.nodes.find((n) => n.type === 'KSampler')
|
|
|
|
expect(ksampler?.widgets_values).toBeDefined()
|
|
const widgetValues = ksampler!.widgets_values as unknown[]
|
|
expect(widgetValues).toEqual([
|
|
42,
|
|
'randomize',
|
|
20,
|
|
7,
|
|
'euler',
|
|
'normal',
|
|
1
|
|
])
|
|
})
|
|
|
|
test('Success toast is shown after replacement', async ({
|
|
comfyPage
|
|
}) => {
|
|
await getSwapNodesGroup(comfyPage.page)
|
|
.getByRole('button', { name: /replace node/i })
|
|
.click()
|
|
|
|
await expect(comfyPage.visibleToasts.first()).toContainText(
|
|
/replaced|swapped/i
|
|
)
|
|
})
|
|
})
|
|
|
|
test.describe('Multi-type replacement', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.VueNodes.Enabled',
|
|
mode.vueNodesEnabled
|
|
)
|
|
await setupNodeReplacement(comfyPage, mockNodeReplacements)
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/node_replacement_multi'
|
|
)
|
|
})
|
|
|
|
test('Replace All replaces all groups across multiple types', async ({
|
|
comfyPage
|
|
}) => {
|
|
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
|
await expect(swapGroup).toBeVisible()
|
|
await expect(swapGroup).toContainText('E2E_OldSampler')
|
|
await expect(swapGroup).toContainText('E2E_OldUpscaler')
|
|
|
|
await swapGroup
|
|
.getByRole('button', { name: 'Replace All', exact: true })
|
|
.click()
|
|
await expect(swapGroup).toBeHidden()
|
|
|
|
const workflow = await comfyPage.workflow.getExportedWorkflow()
|
|
const nodeTypes = workflow.nodes.map((n) => n.type)
|
|
expect(nodeTypes).not.toContain('E2E_OldSampler')
|
|
expect(nodeTypes).not.toContain('E2E_OldUpscaler')
|
|
expect(nodeTypes).toContain('KSampler')
|
|
expect(nodeTypes).toContain('ImageScaleBy')
|
|
})
|
|
|
|
test('Output connections are preserved across replacement with output mapping', async ({
|
|
comfyPage
|
|
}) => {
|
|
await getSwapNodesGroup(comfyPage.page)
|
|
.getByRole('button', { name: 'Replace All', exact: true })
|
|
.click()
|
|
|
|
const replacedNodeOutputLinkCount = await comfyPage.page.evaluate(
|
|
() =>
|
|
window.app!.graph!.getNodeById(2)?.outputs[0]?.links?.length ?? 0
|
|
)
|
|
expect(
|
|
replacedNodeOutputLinkCount,
|
|
'Replaced upscaler should still drive its downstream consumer'
|
|
).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|