Files
ComfyUI_frontend/browser_tests/tests/dialog.spec.ts
Alexander Brown 0132c77c7d test: harden 82 Playwright specs for deterministic CI runs (#10967)
## Summary

Harden 98 E2E spec files and 8 fixtures/helpers for deterministic CI
runs by replacing race-prone patterns with retry-safe alternatives.

No source code changes -- only `browser_tests/` is touched.

## Changes

- **E2E spec hardening** (98 spec files, 6 fixtures, 2 helpers):

  | Fix class | Sites | Examples |
  |-----------|-------|---------:|
| `expect(await ...)` -> `expect.poll()` | ~153 | interaction,
defaultKeybindings, workflows, featureFlags |
| `const x = await loc.count(); expect(x)` -> `toHaveCount()` | ~19 |
menu, linkInteraction, assets, bottomPanelShortcuts |
| `nextFrame()` -> `waitForHidden()` after menu clicks | ~22 |
contextMenu, rightClickMenu, subgraphHelper |
| Redundant `nextFrame()` removed | many | defaultKeybindings, minimap,
builderSaveFlow |
| `expect(async () => { ... }).toPass()` retry blocks | 5 | interaction
(graphdialog dismiss guard) |
| `force:true` removed from `BaseDialog.close()` | 1 | BaseDialog
fixture |
| ContextMenu `waitForHidden` simplified (check-then-act race removed) |
1 | ContextMenu fixture |
| Non-deterministic node order -> proximity-based selection | 1 |
interaction (toggle dom widget) |
  | Tight poll timeout (250ms) -> >=2000ms | 2 | templates |

- **Helper improvements**: Exposed locator getters on
`ComfyPage.domWidgets`, `ToastHelper.toastErrors`, and
`WorkflowsSidebarTab.activeWorkflowLabel` so callers can use retrying
assertions (`toHaveCount()`, `toHaveText()`) directly.

- **Flake pattern catalog**: Added section 7 table to
`browser_tests/FLAKE_PREVENTION_RULES.md` documenting 8 pattern classes
for reviewers and future authors.

- **Docs**: Fixed bad examples in `browser_tests/README.md` to use
`expect.poll()`.

- **Breaking**: None
- **Dependencies**: None

## Review Focus

- All fixes follow the rules in
`browser_tests/FLAKE_PREVENTION_RULES.md`
- No behavioral changes to tests -- only timing/retry strategy is
updated
- The `ContextMenu.waitForHidden` simplification removes a
swallowed-error anti-pattern; both locators now use direct `waitFor({
state: 'hidden' })`

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-09 20:50:56 -07:00

161 lines
5.2 KiB
TypeScript

import { expect } from '@playwright/test'
import type { Keybinding } from '@/platform/keybindings/types'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Settings', () => {
test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
await comfyPage.page.keyboard.press('Control+,')
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsDialog).toBeVisible()
const contentArea = settingsDialog.locator('main')
await expect(contentArea).toBeVisible()
await expect
.poll(() => contentArea.evaluate((el) => el.clientHeight))
.toBeGreaterThan(30)
})
test('Can open settings with hotkey', async ({ comfyPage }) => {
await comfyPage.page.keyboard.down('ControlOrMeta')
await comfyPage.page.keyboard.press(',')
await comfyPage.page.keyboard.up('ControlOrMeta')
const settingsLocator = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsLocator).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(settingsLocator).not.toBeVisible()
})
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
await test.step('Setting should persist', async () => {
await expect
.poll(() => comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed'))
.toBe(maxSpeed)
})
})
test('Should persist keybinding setting', async ({ comfyPage }) => {
// Open the settings dialog
await comfyPage.page.keyboard.press('Control+,')
// Open the keybinding tab
const settingsDialog = comfyPage.page.locator(
'[data-testid="settings-dialog"]'
)
await expect(settingsDialog).toBeVisible()
await settingsDialog
.locator('nav [role="button"]', { hasText: 'Keybinding' })
.click()
await expect(
comfyPage.page.getByPlaceholder('Search Keybindings...')
).toBeVisible()
// Focus the 'New Blank Workflow' row
const newBlankWorkflowRow = comfyPage.page.locator('tr', {
has: comfyPage.page.getByRole('cell', { name: 'New Blank Workflow' })
})
await newBlankWorkflowRow.click()
// Click add keybinding button (New Blank Workflow has no default keybinding)
const addKeybindingButton = newBlankWorkflowRow.locator(
'.icon-\\[lucide--plus\\]'
)
await addKeybindingButton.click()
// Set new keybinding
const input = comfyPage.page.getByPlaceholder('Enter your keybind')
await input.press('Alt+n')
const requestPromise = comfyPage.page.waitForRequest(
(req) =>
req.url().includes('/api/settings') &&
!req.url().includes('/api/settings/') &&
req.method() === 'POST'
)
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('Modify keybinding')
.getByText('Save')
await saveButton.click()
const request = await requestPromise
const expectedSetting: Keybinding = {
commandId: 'Comfy.NewBlankWorkflow',
combo: {
key: 'n',
ctrl: false,
alt: true,
shift: false
}
}
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
})
})
test.describe('Support', () => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
// Prevent loading the external page
await comfyPage.page
.context()
.route('https://support.comfy.org/**', (route) =>
route.fulfill({ body: '<html></html>', contentType: 'text/html' })
)
const popupPromise = comfyPage.page.waitForEvent('popup')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const popup = await popupPromise
const url = new URL(popup.url())
expect(url.hostname).toBe('support.comfy.org')
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await popup.close()
})
})
test.describe('Signin dialog', () => {
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const nodeNum = await comfyPage.nodeOps.getNodeCount()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy()
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
void window.app!.extensionManager.dialog.showSignInDialog()
})
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
await input.waitFor({ state: 'visible' })
await input.press('Control+v')
await expect(input).toHaveValue('test_password')
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toBe(nodeNum)
})
})