Files
ComfyUI_frontend/docs/guidance/playwright.md
Christian Byrne 61af482cd4 docs: document when to use page.evaluate vs user actions in browser tests (#10658)
## Summary

Document acceptable vs avoidable `page.evaluate` patterns in Playwright
E2E tests, with migration candidates for existing offenders.

## Changes

- **What**: Add "When to Use `page.evaluate`" section to
`docs/guidance/playwright.md` with acceptable/avoid/preferred guidance
and 8 migration candidates identified from audit

## Review Focus

Whether the migration candidate list covers the right tests and whether
the acceptable/avoid boundary is drawn correctly.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10658-docs-document-when-to-use-page-evaluate-vs-user-actions-in-browser-tests-3316d73d36508186be90f263f36daf75)
by [Unito](https://www.unito.io)
2026-04-06 16:56:43 -07:00

255 lines
8.8 KiB
Markdown

---
globs:
- '**/*.spec.ts'
---
# Playwright E2E Test Conventions
See `docs/testing/*.md` for detailed patterns.
## Best Practices
- Follow [Playwright Best Practices](https://playwright.dev/docs/best-practices)
- Do NOT use `waitForTimeout` — use Locator actions and retrying assertions
- Prefer specific selectors (role, label, test-id)
- Test across viewports
## Window Globals
Browser tests access `window.app`, `window.graph`, and `window.LiteGraph` which are
optional in the main app types. Use non-null assertions (`!`) in E2E tests only:
```typescript
window.app!.graph!.nodes
window.LiteGraph!.registered_node_types
```
TODO: Consolidate into a central utility (e.g., `getApp()`) with runtime type checking.
## Type Assertions
Use specific type assertions when needed, never `as any`.
Acceptable:
```typescript
window.app!.extensionManager
id: 'TestSetting' as TestSettingId
type TestSettingId = keyof Settings
```
Forbidden:
```typescript
settings: testData as any
data as unknown as SomeType
```
Access internal state via `page.evaluate` and stores directly — don't change public API types to expose internals.
## Assertion Best Practices
Assert preconditions explicitly with a custom message so failures point to the broken assumption:
```typescript
expect(node.widgets, 'Widget count changed — update test fixture').toHaveLength(
4
)
await node.move(100, 200)
expect.soft(menuItem1).toBeVisible()
expect.soft(menuItem2).toBeVisible()
// Bad — bare expect on a precondition gives no context when it fails
expect(node.widgets).toHaveLength(4)
```
- `expect(x, 'reason')` for precondition checks unrelated to the test's purpose
- `expect.soft()` to verify multiple invariants without aborting on the first failure
## Test Structure: Arrange/Act/Assert
1. All mock setup, state resets, and fixture arrangement belongs in `test.beforeEach()` or Playwright fixtures
2. Inside `test()`, only act (user actions) and assert
3. Never call `clearAllMocks` or reset mock state mid-test
```typescript
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('test.json')
})
test('should do something', async ({ comfyPage }) => {
await comfyPage.menu.topbar.click()
await expect(comfyPage.menu.nodeLibraryTab.root).toBeVisible()
})
```
## Creating New Test Helpers
New domain-specific test helpers (e.g., `AssetHelper`, `JobHelper`) should be
registered as Playwright fixtures via `base.extend()` rather than attached as
properties on `ComfyPage`. This enables automatic setup/teardown.
### Extend `base` from Playwright
Keep each fixture self-contained by extending `@playwright/test` directly.
Compose fixtures together with `mergeTests` when a test needs multiple helpers.
```typescript
// browser_tests/fixtures/assetFixture.ts
import { test as base } from '@playwright/test'
export const test = base.extend<{
assetHelper: AssetHelper
}>({
assetHelper: async ({ page }, use) => {
const helper = new AssetHelper(page)
await helper.setup()
await use(helper)
await helper.cleanup() // automatic teardown
}
})
```
### Rules
- **Do NOT** add new helpers as properties on `ComfyPage`
- Each fixture gets automatic cleanup via the callback after `use()`
- Keep fixtures modular — extend `@playwright/test` base, not
`comfyPageFixture`, so they can be composed via `mergeTests`
## Custom Assertions
Add assertion methods directly on the page object or helper class instead of extending `comfyExpect`. Page object methods are discoverable via IntelliSense without special imports.
```typescript
// ✅ Page object assertions
await node.expectPinned()
await node.expectBypassed()
// ❌ Do not add custom matchers to comfyExpect
```
## Test Tags
- `@mobile` — Mobile viewport tests
- `@2x` — High DPI tests
## Test Data
- Check `browser_tests/assets/` for fixtures
- Use realistic ComfyUI workflows
- When multiple nodes share the same title, use `vueNodes.getNodeByTitle(name).nth(n)` — Playwright strict mode will fail on ambiguous locators
## Fixture Data & Schemas
When creating test fixture data, import or reference existing Zod schemas and TypeScript
types from `src/` instead of inventing ad-hoc shapes. This keeps test data in sync with
production types.
Key schema locations:
- `src/schemas/apiSchema.ts` — API response types (`PromptResponse`, `SystemStats`, `User`, `UserDataFullInfo`, WebSocket messages)
- `src/schemas/nodeDefSchema.ts` — Node definition schema (`ComfyNodeDef`, `InputSpec`, `ComboInputSpec`)
- `src/schemas/nodeDef/nodeDefSchemaV2.ts` — V2 node definition schema
- `src/platform/remote/comfyui/jobs/jobTypes.ts` — Jobs API Zod schemas (`zJobDetail`, `zJobsListResponse`, `zRawJobListItem`)
- `src/platform/workflow/validation/schemas/workflowSchema.ts` — Workflow validation (`ComfyWorkflowJSON`, `ComfyApiWorkflow`)
- `src/types/metadataTypes.ts` — Asset metadata types
## Typed API Mocks
When mocking API responses with `route.fulfill()`, **always** type the response body
using existing schemas or generated types — never use untyped inline JSON objects.
This catches shape mismatches at compile time instead of through flaky runtime failures.
All three generated-type packages (`ingest-types`, `registry-types`, `generatedManagerTypes`)
are auto-generated from their respective OpenAPI specs. Prefer these as the single
source of truth for any mock that targets their endpoints.
### Sources of truth
| Endpoint category | Type source |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Cloud-only (hub, billing, workflows) | `@comfyorg/ingest-types` (`packages/ingest-types`, auto-generated from OpenAPI) |
| Registry (releases, nodes, publishers) | `@comfyorg/registry-types` (`packages/registry-types`, auto-generated from OpenAPI) |
| Manager (queue tasks, packages) | `generatedManagerTypes.ts` (`src/workbench/extensions/manager/types/`, auto-generated from OpenAPI) |
| Python backend (queue, history, settings, features) | Manual Zod schemas in `src/schemas/apiSchema.ts` |
| Node definitions | `src/schemas/nodeDefSchema.ts` |
| Templates | `src/platform/workflow/templates/types/template.ts` |
### Patterns
```typescript
// ✅ Import the type and annotate mock data
import type { ReleaseNote } from '@/platform/updates/common/releaseService'
const mockRelease: ReleaseNote = {
id: 1,
project: 'comfyui',
version: 'v0.3.44',
attention: 'medium',
content: '## New Features',
published_at: new Date().toISOString()
}
body: JSON.stringify([mockRelease])
// ❌ Untyped inline JSON — schema drift goes unnoticed
body: JSON.stringify([{ id: 1, project: 'comfyui', version: 'v0.3.44', ... }])
```
## When to Use `page.evaluate`
### Acceptable (use sparingly)
Reading internal state that has no UI representation (prefer locator
assertions whenever possible):
```typescript
// Reading graph/store values
const nodeCount = await page.evaluate(() => window.app!.graph!.nodes.length)
const linkSlot = await page.evaluate(() => window.app!.graph!.links.get(1)?.target_slot)
// Reading computed properties or store state
await page.evaluate(() => useWorkflowStore().activeWorkflow)
// Setting up test fixtures (registering extensions, mock error handlers)
await page.evaluate(() => {
window.app!.registerExtension({ name: 'TestExt', settings: [...] })
})
```
### Avoid
Performing actions that have a UI equivalent — use Playwright locators and user
interactions instead:
```typescript
// Bad: setting a widget value programmatically
await page.evaluate(() => { node.widgets![0].value = 512 })
// Good: click the widget and type the value
await widgetLocator.click()
await widgetLocator.fill('512')
// Bad: dispatching synthetic DOM events
await page.evaluate(() => { btn.dispatchEvent(new MouseEvent('click', ...)) })
// Good: use Playwright's click
await page.getByTestId('more-options-button').click()
// Bad: calling store actions that correspond to user interactions
await page.evaluate(() => { app.queuePrompt(0) })
// Good: click the Queue button
await page.getByRole('button', { name: 'Queue' }).click()
```
### Preferred
Use helper methods from `browser_tests/fixtures/helpers/` that wrap real user
interactions (e.g., `comfyPage.settings.setSetting`, `comfyPage.nodeOps`,
`comfyPage.workflow.loadWorkflow`).
## Running Tests
```bash
pnpm test:browser:local # Run all E2E tests
pnpm test:browser:local -- --ui # Interactive UI mode
```