mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-15 19:54:32 +00:00
## Summary Simplifies the Swap Nodes error card as the fourth slice of the catalog/error-tab presentation refactor, aligning it with the newer compact error-row patterns while preserving the existing replace and locate behavior. This follows the staged rollout plan from the earlier error-tab PRs: 1. #12683 refined execution-style errors: validation, runtime, and prompt errors. 2. #12705 simplified missing media errors into flat, locatable rows. 3. #12735 simplified missing node pack errors and aligned grouped-row behavior. 4. This PR applies the same simplification pass to Swap Nodes errors. 5. A later PR is expected to handle Missing Models, which is larger and intentionally kept separate. After the Missing Models slice lands, a follow-up consistency PR will normalize the shared row/disclosure pattern across Missing Node Packs, Swap Nodes, and Missing Models together. That follow-up will cover parameterized i18n labels for disclosure controls, shared text-button styling, and consistent disclosure semantics/accessibility across those grouped rows. ## Changes - **What**: Reworks the Swap Nodes card rows so each replacement group is presented as a compact row with the source node type, replacement target, replace action, and locate action. - **What**: For a single affected node, the visible row label can be clicked to locate the node, matching the interaction model used by the newer missing-media and missing-node rows. - **What**: For multiple affected nodes with the same replacement target, the group renders a count badge and a disclosure row. Expanding the group shows the affected node rows, each with its own locate action. - **What**: Removes the old node-id badge path from Swap Nodes rows. Node-id badges remain available to the other error cards that still own that behavior. - **What**: Keeps replacement behavior unchanged: per-group replacement and replace-all still call through the existing node replacement store flow. - **What**: Adds regression coverage for the new grouped-row UI, including same-type grouping in both Vue Nodes and LiteGraph render modes. - **Breaking**: None. - **Dependencies**: None. ## Review Focus Please focus on the Swap Nodes presentation and interaction symmetry with the previous error-tab PRs: - Single-node groups should remain directly locatable via the row label and the locate icon. - Multi-node groups should expose the count and expand/collapse behavior without adding duplicate focusable disclosure controls. - The visible row labels intentionally keep their own accessible names, while the separate locate icon uses the generic `Locate node on canvas` accessible name. This mirrors the established pattern from the previous slices. - The newly added Playwright fixture covers two same-type replaceable nodes so duplicate group keys and grouped disclosure behavior are exercised end-to-end. ## Validation - `pnpm format` - `pnpm test:unit src/platform/nodeReplacement/components/SwapNodeGroupRow.test.ts` - `pnpm test:browser:local browser_tests/tests/nodeReplacement.spec.ts --project=chromium` - Pre-commit hook: lint-staged, stylelint, oxfmt, oxlint, eslint, `pnpm typecheck`, `pnpm typecheck:browser` - Pre-push hook: `pnpm knip --cache` - Additional parallel code review pass completed locally; no blocker or major issues remained. ## Screenshots (if applicable) This PR <img width="561" height="362" alt="스크린샷 2026-06-11 오전 3 46 06" src="https://github.com/user-attachments/assets/65395467-6c2f-4aa1-84c5-3d9614c00c80" /> old (Main) <img width="611" height="798" alt="스크린샷 2026-06-11 오전 3 46 32" src="https://github.com/user-attachments/assets/3862d5df-f839-40c0-9488-ce64b051378e" />
252 lines
8.4 KiB
TypeScript
252 lines
8.4 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('Shows direct row label and locate action for a single replacement group', async ({
|
|
comfyPage
|
|
}) => {
|
|
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
|
const rowLabel = swapGroup.getByRole('button', {
|
|
name: 'E2E_OldSampler',
|
|
exact: true
|
|
})
|
|
|
|
await expect(rowLabel).toBeVisible()
|
|
await expect(
|
|
swapGroup.getByRole('button', {
|
|
name: 'Locate node on canvas',
|
|
exact: true
|
|
})
|
|
).toBeVisible()
|
|
await expect(
|
|
swapGroup.getByTestId(TestIds.dialogs.swapNodeGroupCount)
|
|
).toHaveCount(0)
|
|
|
|
await comfyPage.canvasOps.pan({ x: -800, y: -800 })
|
|
const offsetBeforeLocate = await comfyPage.canvasOps.getOffset()
|
|
|
|
await rowLabel.click()
|
|
|
|
await expect
|
|
.poll(() => comfyPage.canvasOps.getOffset())
|
|
.not.toEqual(offsetBeforeLocate)
|
|
})
|
|
|
|
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('Same-type replacement group', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting(
|
|
'Comfy.VueNodes.Enabled',
|
|
mode.vueNodesEnabled
|
|
)
|
|
await setupNodeReplacement(comfyPage, mockNodeReplacementsSingle)
|
|
await loadWorkflowAndOpenErrorsTab(
|
|
comfyPage,
|
|
'missing/node_replacement_same_type'
|
|
)
|
|
})
|
|
|
|
test('Groups same-type replacement rows behind the title disclosure', async ({
|
|
comfyPage
|
|
}) => {
|
|
const swapGroup = getSwapNodesGroup(comfyPage.page)
|
|
const countBadge = swapGroup.getByTestId(
|
|
TestIds.dialogs.swapNodeGroupCount
|
|
)
|
|
const childRows = swapGroup.getByRole('listitem')
|
|
const expandButton = swapGroup.getByRole('button', {
|
|
name: 'Expand E2E_OldSampler',
|
|
exact: true
|
|
})
|
|
|
|
await expect(expandButton).toBeVisible()
|
|
await expect(countBadge).toHaveText('2')
|
|
await expect(childRows).toHaveCount(0)
|
|
|
|
await expandButton.click()
|
|
await expect(childRows).toHaveCount(2)
|
|
await expect(
|
|
swapGroup.getByRole('button', {
|
|
name: 'E2E_OldSampler',
|
|
exact: true
|
|
})
|
|
).toHaveCount(2)
|
|
|
|
await swapGroup
|
|
.getByRole('button', {
|
|
name: 'Collapse E2E_OldSampler',
|
|
exact: true
|
|
})
|
|
.click()
|
|
await expect(childRows).toHaveCount(0)
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|