# E2E Testing Guidelines

See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded for `*.spec.ts`).
See `@browser_tests/FLAKE_PREVENTION_RULES.md` when triaging or editing
flaky browser tests.

## Directory Structure

```text
browser_tests/
├── assets/           - Test data (JSON workflows, images)
├── fixtures/
│   ├── ComfyPage.ts      - Main fixture (delegates to helpers)
│   ├── ComfyMouse.ts     - Mouse interaction helper
│   ├── VueNodeHelpers.ts - Vue Nodes 2.0 helpers
│   ├── selectors.ts      - Centralized TestIds
│   ├── data/             - Static test data (mock API responses, workflow JSONs, node definitions)
│   ├── components/       - Page object classes (locators, user interactions)
│   │   ├── Actionbar.ts
│   │   ├── ContextMenu.ts
│   │   ├── ManageGroupNode.ts
│   │   ├── SettingDialog.ts
│   │   ├── SidebarTab.ts
│   │   ├── Templates.ts
│   │   ├── Topbar.ts
│   │   └── ...
│   ├── helpers/          - Focused helper classes (domain-specific actions)
│   │   ├── CanvasHelper.ts
│   │   ├── CommandHelper.ts
│   │   ├── KeyboardHelper.ts
│   │   ├── NodeOperationsHelper.ts
│   │   ├── SettingsHelper.ts
│   │   ├── WorkflowHelper.ts
│   │   └── ...
│   └── utils/            - Standalone utility functions (used by tests or fixtures)
│       ├── builderTestUtils.ts
│       ├── clipboardSpy.ts
│       ├── fitToView.ts
│       ├── perfReporter.ts
│       └── ...
└── tests/            - Test files (*.spec.ts)
```

### Architectural Separation

- **`fixtures/data/`** — Static test data only. Mock API responses, workflow JSONs, node definitions. No code, no imports from Playwright.
- **`fixtures/components/`** — Page object components. Classes that own locators for a specific UI region (e.g. `Actionbar`, `ContextMenu`, `ManageGroupNode`).
- **`fixtures/helpers/`** — Helper classes that coordinate actions across multiple regions without owning a locator surface of their own (e.g. `CanvasHelper`, `WorkflowHelper`, `NodeOperationsHelper`).
- **`fixtures/utils/`** — Standalone utility functions. Exported functions (not classes) used by tests or fixtures (e.g. `fitToView`, `clipboardSpy`, `builderTestUtils`).

### Placement Rule

When adding a new file, use this decision tree:

```mermaid
flowchart TD
    A[New file in browser_tests/fixtures/] --> B{Has any code?}
    B -- No, JSON/data only --> D[fixtures/data/]
    B -- Yes --> C{Is it a class?}
    C -- No, exported functions --> U[fixtures/utils/]
    C -- Yes --> E{Owns locators for a<br/>specific UI region?}
    E -- Yes --> P[fixtures/components/]
    E -- No, coordinates actions<br/>across the app --> H[fixtures/helpers/]
```

## Page Object Locator Style

Define UI element locators as `public readonly` properties assigned in the constructor — not as getter methods. Getters that simply return a locator add unnecessary indirection and hide the object shape from IDE auto-complete.

```typescript
// ✅ Correct — public readonly, assigned in constructor
export class MyDialog extends BaseDialog {
  public readonly submitButton: Locator
  public readonly cancelButton: Locator

  constructor(page: Page) {
    super(page)
    this.submitButton = this.root.getByRole('button', { name: 'Submit' })
    this.cancelButton = this.root.getByRole('button', { name: 'Cancel' })
  }
}

// ❌ Avoid — getter-based locators
export class MyDialog extends BaseDialog {
  get submitButton() {
    return this.root.getByRole('button', { name: 'Submit' })
  }
}
```

**Keep as getters only when:**

- Lazy initialization is needed (`this._tab ??= new Tab(this.page)`)
- The value is computed from runtime state (e.g. `get id() { return this.userIds[index] }`)
- It's a private convenience accessor (e.g. `private get page() { return this.comfyPage.page }`)

When a class has cached locator properties, prefer reusing them in methods rather than rebuilding locators from scratch.

## Polling Assertions

Prefer `expect.poll()` over `expect(async () => { ... }).toPass()` when the block contains a single async call with a single assertion. `expect.poll()` is more readable and gives better error messages (shows actual vs expected on failure).

```typescript
// ✅ Correct — single async call + single assertion
await expect
  .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 250 })
  .toBe(0)

// ❌ Avoid — nested expect inside toPass
await expect(async () => {
  expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 250 })
```

Reserve `toPass()` for blocks with multiple assertions or complex async logic that can't be expressed as a single polled value.

## Gotchas

| Symptom                                            | Cause                                       | Fix                                                                                                     |
| -------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `subtree intercepts pointer events` on DOM widgets | Canvas `z-999` overlay intercepts `click()` | Use Playwright's `locator.dispatchEvent('contextmenu', { bubbles: true, cancelable: true, button: 2 })` |
| Context menu empty or wrong items                  | Node not selected                           | Select node first: `vueNodes.selectNode()` or `nodeRef.click('title')`                                  |
| `navigateIntoSubgraph` timeout                     | Node too small in test asset JSON           | Use node size `[400, 200]` minimum                                                                      |

## After Making Changes

- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory
- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files
- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint

## Skill Documentation

A Playwright test-writing skill exists at `.claude/skills/writing-playwright-tests/SKILL.md`.

The skill documents **meta-level guidance only** (gotchas, anti-patterns, decision guides). It does **not** duplicate fixture APIs - agents should read the fixture code directly in `browser_tests/fixtures/`.
