Writes Playwright e2e tests for ComfyUI_frontend. Use when creating, modifying, or debugging browser tests. Triggers on: playwright, e2e test, browser test, spec file.
The design rules that apply at every test level (behavioral assertions,
lowest proving level, table-driven variants, isolation, no sleeps) are in
docs/guidance/testing-principles.md. This skill covers the Playwright and
ComfyUI mechanics.
ALWAYS look at existing tests first. Search browser_tests/tests/ for similar patterns before writing new tests.
ALWAYS read the fixture code. The APIs are in browser_tests/fixtures/ - read them directly instead of guessing.
Use premade JSON workflow assets instead of building workflows programmatically.
browser_tests/assets/await comfyPage.workflow.loadWorkflow('feature/my_workflow')browser_tests/assets/default.json and manually editing the JSON to match your desired graph stateSeed starting settings with test.use({ initialSettings }). The main
fixture replaces backend settings before every test; do not add settings
resets in afterEach. Keep runtime setters for changes under test. Nested
test.use replaces the parent's settings object, so include inherited
overrides. See Starting settings and isolation.
Choose based on what you're testing, not personal preference:
| Testing... | Use | Why |
|---|---|---|
| Vue-rendered node UI, DOM widgets, CSS states | comfyPage.vueNodes.* |
Nodes are DOM elements, use locators |
| Canvas interactions, connections, legacy nodes | comfyPage.nodeOps.* |
Canvas-based, use coordinates/references |
| Both in same test | Pick primary, minimize switching | Avoid confusion |
Always add { tag: '@vue-nodes' } to the test or test.describe when it needs
Vue Nodes. The fixture enables the renderer before boot and waits for the nodes.
Never manually set Comfy.VueNodes.Enabled, including through initialSettings,
or call comfyPage.vueNodes.waitForNodes() in tests.
Vue Node state uses CSS classes:
const BYPASS_CLASS = /before:bg-bypass\/60/
await expect(node).toHaveClass(BYPASS_CLASS)
These are frequent causes of flaky tests - check them first, but investigate if they don't apply:
| Symptom | Common Cause | Typical Fix |
|---|---|---|
| Test passes locally, fails in CI | Missing nextFrame() | Add await comfyPage.nextFrame() after canvas ops (not needed after loadWorkflow()) |
| Keyboard shortcuts don't work | Missing focus | Add await comfyPage.canvas.click() first |
| Double-click doesn't trigger | Timing too fast | Add { delay: 5 } option |
| Elements end up in wrong position | Drag animation incomplete | Use { steps: 10 } not { steps: 1 } |
| Widget value wrong after drag-drop | Upload incomplete | Add { waitForUpload: true } |
| Test fails when run with others | Test pollution | Add afterEach with resetView() |
| Local screenshots don't match CI | Platform differences | Screenshots are Linux-only, use PR label |
subtree intercepts pointer events |
Canvas overlay (z-999) | Use dispatchEvent on the DOM element to bypass overlay |
| Context menu empty / wrong items | Node not selected | Select node first: vueNodes.selectNode() or nodeRef.click('title') |
navigateIntoSubgraph timeout |
Node too small in asset | Use node size [400, 200] minimum in test asset JSON |
Add appropriate tags to every test:
| Tag | When to Use |
|---|---|
@smoke |
Quick essential tests |
@slow |
Tests > 10 seconds |
@screenshot |
Visual regression tests |
@canvas |
Canvas interactions |
@node |
Node-related |
@widget |
Widget-related |
@mobile |
Mobile viewport (runs on Pixel 5 project) |
@2x |
HiDPI tests (runs on 2x scale project) |
test.describe('Feature', { tag: ['@screenshot', '@canvas'] }, () => {
Never use waitForTimeout - it's always wrong.
| Pattern | Use Case |
|---|---|
| Auto-retrying assertions | toBeVisible(), toHaveText(), etc. (prefer these) |
expect.poll() |
Single value polling |
expect().toPass() |
Multiple assertions that must all pass |
// Prefer auto-retrying assertions when possible
await expect(node).toBeVisible()
// Single value polling
await expect.poll(() => widget.getValue(), { timeout: 2000 }).toBe(100)
// Multiple conditions
await expect(async () => {
expect(await node1.getValue()).toBe('foo')
expect(await node2.getValue()).toBe('bar')
}).toPass({ timeout: 2000 })
New Browser Test Expectationsawait expect(comfyPage.canvas).toHaveScreenshot('page.png', {
mask: [page.locator('.timestamp')]
})
pnpm dlx playwright show-trace trace.zipCI=true pnpm test:browserpnpm test:browser:localAvoid these common mistakes:
Arbitrary waits - Use retrying assertions instead
// ❌ await page.waitForTimeout(500)
// ✅ await expect(element).toBeVisible()
Implementation-tied selectors - Use test IDs or semantic selectors
// ❌ page.locator('div.container > button.btn-primary')
// ✅ page.getByTestId('submit-button')
Missing nextFrame after canvas ops - Canvas needs sync time
await node.drag({ x: 50, y: 50 })
await comfyPage.nextFrame() // Required
Shared state between tests - Tests must be independent
// ❌ let sharedData // Outside test
// ✅ Define state inside each test
// Path depends on test file location - adjust '../' segments accordingly
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('FeatureName', { tag: ['@canvas'] }, () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('should do something', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('myWorkflow')
const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
// ... test logic
await expect(comfyPage.canvas).toHaveScreenshot('expected.png')
})
})
# Find similar tests
grep -r "KSampler" browser_tests/tests/
# Find usage of a fixture method
grep -r "loadWorkflow" browser_tests/tests/
# Find tests with specific tag
grep -r '@screenshot' browser_tests/tests/
| Purpose | Path |
|---|---|
| Main fixture | browser_tests/fixtures/ComfyPage.ts |
| Helper classes | browser_tests/fixtures/helpers/ |
| Component objects | browser_tests/fixtures/components/ |
| Test selectors | browser_tests/fixtures/selectors.ts |
| Vue Node helpers | browser_tests/fixtures/VueNodeHelpers.ts |
| Test assets | browser_tests/assets/ |
| Existing tests | browser_tests/tests/ |
Read the fixture code directly - it's the source of truth for available methods.