Skip to content

Playwright Test Ownership

Audience: All developers
Key rule: You own fixing failing Playwright tests on your PRs — not QA.

Playwright E2E tests run on every PR via CI. When a test fails on your PR, it is your responsibility to diagnose and resolve it before merging. QA owns the test suite infrastructure and baseline coverage; developers own keeping tests green on their PRs.


When a Playwright Test Fails on Your PR

Follow this process from top to bottom. Most failures resolve within steps 1–3.

Step 1 — Read the Failure

Check the CI output or the automated PR comment for the test result. Capture:

  • Which test failed — the spec file and test name
  • Error type — timeout, locator not found, assertion mismatch, network error
  • Error message — the full Playwright error with the selector or assertion that failed
  • Screenshot / trace — available in CI artifacts if configured

Tip

The automated PR comment (when enabled) gives you a summary of failures without digging through CI logs. Start there.

Step 2 — Determine the Cause

Run through these checks in order. Stop at the first one that matches.

Check 1: Did your PR change what the test checks?

git diff --name-only origin/develop...HEAD

Cross-reference the changed files with the failing test. If your PR modified a component, page, or user flow that the test covers, ask:

  • Did you rename, remove, or restructure a UI element the test targets?
  • Did you change text content, labels, or ARIA attributes the selector uses?
  • Did you change the user flow (different navigation, reordered steps, new modal)?
  • Did you add/remove a feature flag that changes what renders?

If YES — the test needs updating to match your intentional changes. Go to Step 3A.

If NO — your PR likely introduced a real bug. Go to Step 3B.

Tip

To trace what a failing test actually checks: spec file (tests/<module>/*.spec.ts) → page object method (pages/<feature>.page.ts) → selector (selectors/<feature>.selectors.ts). Follow this chain to find which DOM element or assertion is failing.

Check 2: Is it intermittent / flaky?

Signs of flakiness:

  • The test passes on retry but fails intermittently
  • The error is a timeout waiting for an element that should exist
  • The test doesn't properly wait for loading states

If flaky — fix the test's wait logic. This is a test quality issue. Add proper waitFor calls instead of relying on implicit timeouts.

Check 3: Did another PR break it?

git log origin/develop --oneline -10 --merges

If a recent merge to develop changed the same area, the failure may be a regression from that PR — not yours. Rebase on latest develop. If it still fails, flag it to the engineer who merged the conflicting PR.

Check 4: Is it an environment issue?

Signs:

  • HTTP 401/403 (auth expired)
  • Navigation timeout (environment is down)
  • Test passes locally but fails in CI

If environment issue — not your code. Report in #tech Slack channel.

Default — Treat as a real bug

If none of the above checks explain the failure, the test is catching a genuine regression. Fix the application code, not the test.

Step 3A — Update the Test

When your PR intentionally changed the behaviour the test asserts on:

  1. Update selectors in selectors/<feature>.selectors.ts — prefer data-testid attributes. Check selectors/common.selectors.ts for shared selectors (loading spinners, modals, table elements).
  2. Update page object methods in pages/<feature>.page.ts — these extend BasePage and encapsulate all interactions. Adjust wait conditions if the page loads differently.
  3. Update the spec in tests/<module>/<action>.spec.ts — change expected values, remove obsolete steps, add test cases for new behaviour. Specs should stay thin — only page object calls and expect assertions.
  4. If your PR added a new page that needs E2E coverage, you must also register it:
    • Create selectors/<feature>.selectors.ts
    • Create pages/<feature>.page.ts extending BasePage
    • Register the page object in fixtures/auth.fixture.ts
    • Add the module tag to tags.config.ts (ModuleTag enum + PR_LABEL_TO_TAGS)
  5. Run it locally to verify:
cd playwright-new
npx playwright test tests/<module>/<test-file>.spec.ts --headed
  1. Commit with a clear message explaining why the test changed:
Update E2E test for <feature> to match <your change>

The test was asserting on <old behaviour> which was intentionally
changed to <new behaviour> in this PR.

Step 3B — Fix the Bug

The test is doing its job — it caught a real regression.

  1. Read the assertion — it tells you exactly what broke ("expected element to be visible", "expected text to contain X")
  2. Reproduce locally — run bun run dev in cred-web-commercial, then navigate the same flow in a browser to confirm the bug
  3. Fix the application code — not the test
  4. Verify the test passes after your fix:
cd playwright-new
npx playwright test tests/<module>/<test-file>.spec.ts

Warning

Never modify a test to make it pass when the application is broken. That defeats the purpose of the test.


Escalation Path

Resolve it yourself first. Escalate only after genuine effort.

Situation Action
You can't determine if it's a bug or stale test after 15 minutes Ask in #tech Slack with the test name, error, and what you've checked
The failure is clearly a CI infrastructure issue (Docker, network, auth) Report in #tech — this is not your code
Another engineer's recent merge caused the failure Slack them with the PR link and test name. Still try to fix it yourself.
The test is fundamentally flawed (bad architecture, wrong approach) Open a Linear ticket tagged testing and ping QA. Fix or skip with a ticket reference in the meantime.
You need to skip a test temporarily Add .skip() with a Linear ticket URL in the skip reason. Never skip without a ticket.

Decision Tree

Test fails on your PR
│
├─ Did YOUR PR change the component/flow under test?
│   ├─ YES → Does the test assert on the changed behaviour?
│   │         ├─ YES → Update the test (Step 3A)
│   │         └─ NO  → Real bug — fix the app (Step 3B)
│   └─ NO  → Real bug — fix the app (Step 3B)
│
├─ Is it intermittent / timing-based?
│   └─ YES → Fix test wait logic (not a bug, not outdated)
│
├─ Did another PR on develop break it?
│   └─ YES → Rebase, then fix or flag to that engineer
│
├─ Is it a CI / environment issue?
│   └─ YES → Report in #tech, not your code
│
└─ Default → Real bug — fix the app (Step 3B)

Anti-Patterns

Don't do this Why Do this instead
Delete a failing test Destroys regression coverage Fix the test or the app
Add .skip() without a ticket Test rots and is never re-enabled File a Linear ticket, reference it in the skip reason
Change selectors to match broken UI The UI is the bug, not the selector Fix the component
Assume all failures are "stale tests" ~80% of failures ARE real bugs Follow the decision tree
Loosen assertions (remove toBeVisible checks) Reduces test value Keep assertions strict; fix the app
Wait for QA to fix it QA owns the suite, not your PR failures Diagnose and fix it yourself
Merge with a failing test Breaks the baseline for everyone Fix it first

Running Tests Locally

Note

playwright-new/ uses npm (not Bun), even though the rest of the monorepo uses Bun. Always use npm and npx inside this directory.

cd playwright-new

# First-time setup
npm install
npx playwright install --with-deps chromium

# Run a specific test file
npx playwright test tests/<module>/<test-file>.spec.ts

# Run with browser visible (for debugging)
npx playwright test tests/<module>/<test-file>.spec.ts --headed

# Playwright UI mode (interactive debugging)
npm run test:ui

# Run tests matching a module tag
npx playwright test --grep @companies

# Run multiple module tags
npx playwright test --grep "@companies|@people"

# Run tests affected by your file changes
node scripts/run-affected-tests.mjs

Test Architecture (Quick Reference)

The playwright-new/ directory follows a Page Object Model (POM) pattern. Specs are thin — they only call page object methods and make assertions.

playwright-new/
├── fixtures/           # Test setup (auth, page object registration)
│   ├── auth.fixture.ts # Custom test fixture with typed page objects
│   └── global-setup.ts # One-time setup (auth state caching)
├── pages/              # Page objects (extend BasePage)
│   ├── base.page.ts    # Base class with shared helpers
│   ├── companies.page.ts
│   ├── people.page.ts
│   └── ...
├── selectors/          # DOM selectors as const objects
│   ├── common.selectors.ts  # Shared selectors (loading, modals, table)
│   ├── companies.selectors.ts
│   └── ...
├── tests/              # Spec files grouped by module
│   ├── companies/
│   ├── people/
│   ├── deals/
│   └── ... (35+ modules)
├── helpers/            # Shared test utilities
├── scripts/            # CI and automation scripts
│   ├── run-affected-tests.mjs  # Maps file changes → relevant test tags
│   └── resolve-tags-from-diff.mjs
├── tags.config.ts      # Module tag registry + PR label mappings
└── playwright.config.ts
Artifact Location Purpose
Specs tests/<module>/*.spec.ts Thin test scenarios — only page object calls + assertions
Page objects pages/<feature>.page.ts Encapsulate all page interactions (extend BasePage)
Selectors selectors/<feature>.selectors.ts Centralised DOM selectors (data-testid, ARIA roles)
Fixtures fixtures/auth.fixture.ts Custom Playwright test with typed page object fixtures
Module tags tags.config.ts Map modules to tags (@companies, @people, etc.) and PR labels
Helpers helpers/*.ts Shared utilities (navigation, test data, export helpers)
Affected test runner scripts/run-affected-tests.mjs Auto-maps your file changes to relevant test tags

FAQ

Q: A test I didn't touch is failing on my PR. Is that still my responsibility?
A: Yes. If your code changes caused the failure, you fix it. If you've confirmed your changes didn't cause it (Check 1 above), rebase on latest develop. If it still fails, flag it in #tech — but don't merge with a failing test.

Q: Can I just re-run CI and hope it passes?
A: Only if you genuinely suspect flakiness (intermittent failures, timing issues). If it fails consistently, re-running is just wasting CI minutes. Diagnose the root cause.

Q: The test references a component I completely rewrote. Do I need to rewrite the test too?
A: Yes. If you changed the component's structure, selectors, or user flow, you must update the corresponding selectors, page object, and spec to match.

Q: I'm writing a new spec file. What do I import?
A: Import test and expect from ../../fixtures/auth.fixturenot from @playwright/test. This gives you typed page object fixtures (e.g., companiesPage, peoplePage) that are pre-authenticated.

Q: I need to add a data-testid to a component for the test. Is that OK?
A: Yes — data-testid attributes are the preferred selector strategy. Add them to the component and reference them in playwright-new/selectors/.

Q: How do I know which tests cover the area I changed?
A: Run node scripts/run-affected-tests.mjs from inside playwright-new/ — it uses tag-mappings.mjs to auto-map your file changes to relevant test module tags. Or search the selectors/ and pages/ directories for references to your component.

Q: A test is fundamentally broken and needs a major rewrite. What do I do?
A: Skip it with .skip() and a Linear ticket reference, then open a ticket tagged testing for QA to prioritise the rewrite. Don't let a broken test block your PR indefinitely.


Cursor Skill for AI-Assisted Diagnosis

The test-failure-diagnosis Cursor skill in cred-web-commercial automates much of this process. When a Playwright test fails, tell Cursor:

Diagnose this Playwright test failure: [paste the error output]

The skill walks through the same decision tree above — checking your branch diff, reading the test file, selector, and page object, and recommending whether to fix the app or update the test. Use it to speed up diagnosis, but always verify the recommendation before applying it.