Files
ComfyUI_frontend/browser_tests/tests/sidebar/workflows.spec.ts
pythongosssss 5899a9392e test: Simplify vue node/menu test setup (#11184)
## Summary
Simplifies test setup for common settings

## Changes

- **What**: 
- add vue-nodes tag to auto enable nodes 2.0
- remove UseNewMenu Top as this is default

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11184-test-Simplify-vue-node-menu-test-setup-3416d73d3650815487e0c357d28761fe)
by [Unito](https://www.unito.io)
2026-04-13 20:43:25 +00:00

404 lines
13 KiB
TypeScript

import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { openErrorsTab } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
test.describe('Workflows sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
// Open the sidebar
const tab = comfyPage.menu.workflowsTab
await tab.open()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('Can create new blank workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', '*Unsaved Workflow (2)'])
})
test('Can show top level saved workflows', async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json',
'workflow2.json': 'default.json'
})
const tab = comfyPage.menu.workflowsTab
await tab.open()
await expect
.poll(() => tab.getTopLevelSavedWorkflowNames())
.toEqual(expect.arrayContaining(['workflow1', 'workflow2']))
})
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1')
await expect
.poll(() => tab.getTopLevelSavedWorkflowNames())
.toEqual(expect.arrayContaining(['workflow1']))
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['workflow1', '*workflow1 (Copy)'])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['workflow1', '*workflow1 (Copy)', '*workflow1 (Copy) (2)'])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual([
'workflow1',
'*workflow1 (Copy)',
'*workflow1 (Copy) (2)',
'*workflow1 (Copy) (3)'
])
})
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'nodes/single_ksampler.json'
})
const tab = comfyPage.menu.workflowsTab
await tab.open()
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
await expect
.poll(() => comfyPage.nodeOps.getNodeCount())
.toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1').click()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
})
test('Can rename nested workflow from opened workflow item', async ({
comfyPage
}) => {
await comfyPage.workflow.setupWorkflowsDirectory({
foo: {
'bar.json': 'default.json'
}
})
const tab = comfyPage.menu.workflowsTab
await tab.open()
// Switch to the parent folder
await tab.getPersistedItem('foo').click()
// Switch to the nested workflow
await tab.getPersistedItem('bar').click()
const openedWorkflow = tab.getOpenedItem('foo/bar')
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', 'foo/baz'])
})
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', 'workflow3'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
})
test('Exported workflow does not contain localized slot names', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('default')
await expect
.poll(async () => {
const exportedWorkflow = await comfyPage.workflow.getExportedWorkflow({
api: false
})
for (const node of exportedWorkflow.nodes) {
for (const slot of node.inputs ?? []) {
if (slot.localized_name !== undefined)
return `input localized_name found: ${slot.localized_name}`
if (slot.label !== undefined)
return `input label found: ${slot.label}`
}
for (const slot of node.outputs ?? []) {
if (slot.localized_name !== undefined)
return `output localized_name found: ${slot.localized_name}`
if (slot.label !== undefined)
return `output label found: ${slot.label}`
}
}
return 'ok'
})
.toBe('ok')
})
test('Can export same workflow with different locales', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('default')
// Setup download listener before triggering the export
const downloadPromise = comfyPage.page.waitForEvent('download')
await comfyPage.menu.topbar.exportWorkflow('exported_default.json')
// Wait for the download and get the file content
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('exported_default.json')
// Get the exported workflow content
await expect
.poll(() => comfyPage.workflow.getExportedWorkflow({ api: false }))
.toBeDefined()
const downloadedContent = await comfyPage.workflow.getExportedWorkflow({
api: false
})
await comfyPage.settings.setSetting('Comfy.Locale', 'zh')
await comfyPage.setup()
// Compare the exported workflow with the original
delete downloadedContent.id
await expect
.poll(async () => {
const downloadedContentZh =
await comfyPage.workflow.getExportedWorkflow({ api: false })
delete downloadedContentZh.id
return downloadedContentZh
})
.toEqual(downloadedContent)
})
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow5'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
await comfyPage.confirmDialog.click('overwrite')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow5'])
})
test('Can save temporary workflow with unmodified name', async ({
comfyPage
}) => {
await expect
.poll(() => comfyPage.workflow.isCurrentWorkflowModified())
.toBe(false)
await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow')
// Should not trigger the overwrite dialog
await expect
.poll(() =>
comfyPage.page.locator('.comfy-modal-content:visible').count()
)
.toBe(0)
await expect
.poll(() => comfyPage.workflow.isCurrentWorkflowModified())
.toBe(false)
})
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
const topbar = comfyPage.menu.topbar
await topbar.saveWorkflow('workflow1')
await topbar.saveWorkflowAs('workflow2')
await comfyPage.nextFrame()
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow1', 'workflow2'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow2')
await topbar.saveWorkflowAs('workflow1')
await comfyPage.confirmDialog.click('overwrite')
// The old workflow1 should be deleted and the new one should be saved.
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow2', 'workflow1'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow1')
})
test('Restores missing nodes errors silently when switching back to workflow', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
const errorOverlay = comfyPage.page.getByTestId(
TestIds.dialogs.errorOverlay
)
await expect(errorOverlay).toBeVisible()
// Dismiss the error overlay
await errorOverlay.getByTestId(TestIds.dialogs.errorOverlayDismiss).click()
await expect(errorOverlay).toBeHidden()
// Load blank workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
// Switch back to the missing_nodes workflow — overlay should NOT
// reappear (silent restore), but errors tab should have content
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
await expect(errorOverlay).toBeHidden()
// Errors tab should still show missing nodes after silent restore
await openErrorsTab(comfyPage)
await expect(
comfyPage.page.getByTestId(TestIds.dialogs.missingNodePacksGroup)
).toBeVisible()
})
test('Can close saved-workflows from the open workflows section', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.saveWorkflow(
`tempWorkflow-${test.info().title}`
)
const closeButton = comfyPage.page.locator(
'.comfyui-workflows-open .close-workflow-button'
)
await closeButton.click()
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
})
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1')
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
await expect
.poll(() => tab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Workflow.ConfirmDelete', false)
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18'
await topbar.saveWorkflow(filename)
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual([filename])
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.nextFrame()
await comfyPage.contextMenu.clickMenuItem('Delete')
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
})
test('Can delete workflows', async ({ comfyPage }) => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18'
await topbar.saveWorkflow(filename)
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual([filename])
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.contextMenu.clickMenuItem('Delete')
await comfyPage.nextFrame()
await comfyPage.confirmDialog.click('delete')
await expect(workflowsTab.getOpenedItem(filename)).toBeHidden()
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow'])
})
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
const { workflowsTab } = comfyPage.menu
await workflowsTab.open()
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
await comfyPage.contextMenu.clickMenuItem('Duplicate')
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
await comfyPage.menu.workflowsTab.open()
// Wait for workflow to appear in Browse section after sync
const workflowItem =
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
await expect(workflowItem).toBeVisible()
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
// Get the bounding box of the canvas element
const canvasBoundingBox = (await comfyPage.page
.locator('#graph-canvas')
.boundingBox())!
// Calculate the center position of the canvas
const targetPosition = {
x: canvasBoundingBox.x + canvasBoundingBox.width / 2,
y: canvasBoundingBox.y + canvasBoundingBox.height / 2
}
await comfyPage.page.dragAndDrop(
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
'#graph-canvas',
{ targetPosition }
)
// Wait for nodes to be inserted after drag-drop with retryable assertion
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
.toBe(nodeCount * 2)
})
})