writing-playwright-tests
Writing Playwright Tests for ComfyUI_frontend
Golden Rules
-
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.
- Assets live in
browser_tests/assets/ - Load with
await comfyPage.workflow.loadWorkflow('feature/my_workflow') - Create new assets by starting with
browser_tests/assets/default.jsonand manually editing the JSON to match your desired graph state
- Assets live in
Vue Nodes vs LiteGraph: Decision Guide
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 |
Vue Nodes requires explicit opt-in:
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
Vue Node state uses CSS classes:
const BYPASS_CLASS = /before:bg-bypass\/60/
await expect(node).toHaveClass(BYPASS_CLASS)
Common Issues
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 |
Test Tags
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'] }, () => {
Retry Patterns
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 })
Screenshot Baselines
- Screenshots are Linux-only. Don't commit local screenshots.
- To update baselines: Add PR label
New Browser Test Expectations - Mask dynamic content:
await expect(comfyPage.canvas).toHaveScreenshot('page.png', { mask: [page.locator('.timestamp')] })
CI Debugging
- Download artifacts from failed CI run
- Extract and view trace:
npx playwright show-trace trace.zip - CI deploys HTML report to Cloudflare Pages (link in PR comment)
- Reproduce CI:
CI=true pnpm test:browser - Local runs:
pnpm test:browser:local
Anti-Patterns
Avoid 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
Quick Start Template
// 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')
})
})
Finding Patterns
# 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/
Key Files to Read
| 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.