| Choose between E2E, VR, unit tests, and Drupal FunctionalJavascript |
Layer Selection |
Default to Playwright web-first E2E for any test whose acceptance criterion includes "the user sees" or "the user clicks." Keep VR and E2E in separate test files — VR demands frozen animations and network idle; mixing the two produces tests that are both over-sensitive and under-sensitive. |
| Pick the right locator (role, text, test-id, CSS) |
Locators |
Use getByRole() first, then getByLabel() for form fields, then getByTestId() for explicit test contracts — the priority order doubles as a soft a11y check. Avoid CSS class chains; they break on every theme refactor. Locators are lazy and compose — chain and filter instead of reaching for .first(). |
| Use every web-first assertion correctly |
Web-First Assertions |
Always use expect(locator) — not isVisible() or textContent() — because only expect() auto-retries until the condition holds or expect.timeout elapses. Never use waitForTimeout(); replace with a web-first assertion or expect.poll() for conditions outside the DOM. |
Build custom fixtures with test.extend |
Fixtures |
Replace beforeEach with test.extend fixtures — they're typed, compose across files, and scope setup to only the tests that need it. Use worker scope for expensive shared resources (OAuth token, DB pool); use test scope (default) for anything tests must not share. |
| Set up auth flows with storageState |
Authentication |
Run a one-time setup project that logs in via API, saves storageState to a .auth/ file, then configure test projects to read that file via dependencies. Never commit .auth/ files — they contain live session cookies. Prefer programmatic API login over UI login for speed and stability. |
Mock network calls with page.route |
Network Mocking |
Use page.route() with fulfill to return canned responses for third-party dependencies and error states; use abort to silence analytics. Never mock the endpoint that is the subject of the test. Always await route handler setup before the action that triggers the request. |
| Test APIs or set up state via API + verify via UI |
API Testing |
The request fixture shares the browser context's cookie jar — use it to log in via API then drive UI as an authenticated user. The killer pattern: create state via JSON:API (fast), drive UI for the feature under test, verify via API (authoritative). |
| Organize tests with describe/hooks/tags/parallelism |
Test Organization |
Use fullyParallel:true globally and reserve test.describe.serial only for genuine multi-step wizards where each step depends on the previous. Use test.step() aggressively in long tests — steps appear collapsibly in the HTML report and Trace Viewer, making triage dramatically faster. |
| Debug failures (UI Mode, Trace Viewer, Inspector) |
Debugging |
Start with UI Mode (npx playwright test --ui) — it caches setup, has a locator picker, and shows DOM snapshots per action. Set trace:'on-first-retry' in CI config so the first lean run is fast and the retry captures the smoking gun cheaply. |
| Run E2E in CI with sharding |
CI Patterns |
Shard with --shard=N/M, collect blob reports from each node, then merge into one HTML report. Set workers:'50%' in CI (browsers are RAM-hungry), retries:2, trace:'on-first-retry', and artifacts failure-only. Add forbidOnly:true to catch committed test.only leaks. |
| Apply Drupal-specific E2E patterns |
Drupal & DDEV Patterns |
Wait for AJAX with waitForResponse targeting /system/ajax — not waitForTimeout or jQuery.active. Handle Big Pipe by asserting on final content, not placeholder markup. Create entities via JSON:API, drive UI for the feature under test, verify via JSON:API. |
| Use ATK helpers in plain Playwright |
ATK Integration |
Set testIdAttribute:'data-qa-id' in playwright.config.ts to get ATK's stable selector guarantees without writing your own preprocess hooks. Import only the ATK helper functions you need — drupalLogin, createNode — rather than adopting the full 36-test catalog. |
| Avoid common mistakes |
Anti-Patterns |
The top three flake causes are waitForTimeout as the default wait, tests that depend on each other's state, and brittle CSS selectors instead of role/label/test-id. Replace each with web-first assertions, fixture-based state isolation, and getByRole/getByLabel. |
| Find services and references |
Code Reference |
Quick lookup for getByRole/getByLabel/getByTestId locators, expect() assertions, test.extend fixtures, page.route mocking, request fixture, and the sharding/debugging CLI commands. |