mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 16:30:57 +00:00
## Summary Add Playwright e2e tests for the node replacement feature (swap nodes UI in the errors tab). ## Changes - **What**: 6 e2e test cases across two describe blocks covering single and multi-type node replacement flows. Tests verify swap nodes group visibility, in-place replacement, widget value preservation, Replace All across multiple types, output connection preservation, and success toast display. Includes typed mock data for `/api/node_replacements` and two workflow fixture files with fake missing node types mapped to real core nodes. ## Review Focus - Mock setup pattern in `setupNodeReplacement` — enables feature flag via `page.evaluate` and routes the API endpoint - Workflow fixture design — uses fake node types (E2E_OldSampler, E2E_OldUpscaler) that map to real registered types (KSampler, ImageScaleBy) - Assertion coverage for link preservation after replacement ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11242-test-add-e2e-tests-for-node-replacement-flows-3426d73d3650811e87d7f0d96fd66433) by [Unito](https://www.unito.io) --------- Co-authored-by: Connor Byrne <c.byrne@comfy.org>
169 lines
5.7 KiB
TypeScript
169 lines
5.7 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'
|
|
|
|
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).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)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|