Files
ComfyUI_frontend/browser_tests/tests/sidebar/workflows.spec.ts
Alexander Brown f1bb756929 fix: remove redundant and counterproductive e2e timeout overrides (#11110)
## Summary

Alright, alright, alright. These e2e tests have been runnin' around like
they're late for somethin', settin' tight little timeouts like the
world's gonna end in 250 milliseconds. Man, you gotta *breathe*. Let the
framework do its thing. Go slow to go fast, that's what I always say.

## Changes

- **What**: Removed ~120 redundant timeout overrides from auto-retrying
Playwright assertions (`toBeVisible`, `toBeHidden`, `toHaveCount`,
`toBeEnabled`, `toHaveAttribute`, `toContainText`, `expect.poll`) where
5000ms is already the default. Also removed sub-5s timeouts (1s, 2s, 3s)
that were just *begging* for flaky failures — like wearin' a belt and
suspenders and also holdin' your pants up with both hands. Raised the
absurdly short timeouts in `customMatchers.ts` (250ms `toPass` → 5000ms,
256ms poll → default). Kept `timeout: 5000` on `.toPass()` calls
(defaults to 0), `.waitFor()`, `waitForRequest`, `waitForFunction`,
intentionally-short timeouts inside retry loops, and conditional
`.isVisible()/.catch()` checks — those fellas actually need the help.

## Review Focus

Every remaining timeout in the diff is there for a *reason*. The ones on
`.toPass()` stay because that API defaults to zero — it won't retry at
all without one. The ones on `.waitFor()` and `waitForRequest` stay
because those are locator actions, not auto-retrying assertions. The
intentionally-short ones inside `toPass` retry loops
(`interaction.spec.ts`) and the negative assertions (`actionbar.spec.ts`
confirming no response arrives) — those are *supposed* to be tight.

The short timeouts on regular assertions were actively *encouragin'*
flaky failures. That's like settin' your alarm for 4 AM and then gettin'
mad you're tired. Just... don't do that, man. Let things take the time
they need.

38 files, net -115 lines. Less code, more chill. That's livin'.

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-04-10 19:47:20 +00:00

398 lines
13 KiB
TypeScript

import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
test.describe('Workflows sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
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('Reports missing nodes warning again 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).not.toBeVisible()
// Load blank workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
// Switch back to the missing_nodes workflow — overlay should reappear
// so users can install missing node packs without a page reload
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
await expect(errorOverlay).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)).not.toBeVisible()
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)).not.toBeVisible()
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)
})
})