Playwright Advanced Guide — Practices, Isolation, Parallelism, Reporting & CI/CD¶
Where this fits: you know the project anatomy, the framework structure, API testing, and TypeScript. This guide answers the senior/lead-level "how do you actually run this at scale" questions — best practices, test isolation, parallelization across machines, UI vs API, reporting, and CI/CD — each with concrete examples. It doubles as interview prep.
1. What Makes "Good" Test Automation?¶
Automation is not "record and playback". A senior approach optimises for trust, speed, and maintainability — in that order. A flaky fast suite is worse than no suite, because it erodes trust.
| Principle | What it means in Playwright |
|---|---|
| Deterministic | No waitForTimeout(3000). Use web-first assertions (expect(locator).toBeVisible()) that auto-retry |
| Isolated | Each test sets up its own state and can run alone, in any order (see §3) |
| Resilient selectors | getByRole / getByTestId, never CSS tied to styling or auto-generated classes |
| Fast | Parallel by default; set up state via API not UI (see API testing §6) |
| Readable | Tests describe behaviour; Page Objects hold the "how" |
| Observable | Failures produce a trace, screenshot, and video — debuggable without re-running |
| Layered | A test pyramid: many API/unit, fewer E2E UI — don't push everything through the browser |
// ❌ fragile & slow
await page.waitForTimeout(2000);
await page.click('.btn-primary.css-1x2y3z');
// ✅ deterministic & resilient
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Saved')).toBeVisible(); // auto-waits, auto-retries
The golden rule of selectors: prefer user-facing locators (
getByRole,getByLabel,getByText) → then test ids (getByTestId) → CSS/XPath only as a last resort. This survives restyles and refactors.Catch the #1 flake cause automatically: a missing
awaitbefore a Playwright call is the most common source of flakiness. Enforce it with ESLint — the official recommendation is the@typescript-eslint/no-floating-promisesrule, which fails the lint (and your CI) on any un-awaited async call. Lint becomes a free, always-on reviewer.
2. Propagating Automation Knowledge Across a Team¶
Automation only scales if the practices scale. As a lead, the framework is half the job — enablement is the other half.
| Mechanism | How it spreads knowledge |
|---|---|
| Conventions doc | A living CONTRIBUTING.md / wiki: folder structure, naming, selector strategy, "definition of done" for a test |
| Page Object / service layer | New joiners reuse LoginPage, UserService — they inherit good patterns without knowing internals |
| Code review checklist | Every PR checked for: no hard waits, resilient selectors, isolation, meaningful assertions |
| Pairing & mob sessions | Pair on the first few tests; record a short Loom walkthrough of the framework |
| Shared fixtures & helpers | Encapsulate auth, seeding, test data once — the team consumes, doesn't reinvent |
| Templates / generators | A spec template or snippet so everyone starts from the same skeleton |
| CI as the teacher | The pipeline enforces standards (lint, format, flake quarantine) so quality is automatic, not nagged |
| Brown-bags & a "flaky test" ritual | Regular sessions on new features; a standing process to triage and fix flaky tests, not mute them |
Lead insight: make the right way the easy way. If reusable fixtures, a snippet, and a green-by-default pipeline exist, people fall into good practice. Documentation alone is rarely enough — encode standards into the tooling.
3. Running Tests in Isolation¶
Isolation = every test is independent. It creates its own state, doesn't depend on another test running first, and doesn't leak state to the next. This is what makes parallelism and re-runs safe.
Playwright's built-in isolation: BrowserContext¶
Each test runs in its own BrowserContext — effectively a fresh, private browser session (separate cookies, storage, cache). Tests cannot pollute each other even when run in parallel.
flowchart TB
B["🌐 One Browser process"] --> C1["BrowserContext (test A)<br/>own cookies/storage"]
B --> C2["BrowserContext (test B)<br/>own cookies/storage"]
B --> C3["BrowserContext (test C)<br/>own cookies/storage"]
C1 --> P1["page"]
C2 --> P2["page"]
C3 --> P3["page"]
Run a single test / file in isolation¶
npx playwright test login.spec.ts # one file
npx playwright test -g "valid user can log in" # one test by title (grep)
npx playwright test login.spec.ts:42 # test at a specific line
npx playwright test --project=chromium # one browser project
Per-test setup with fixtures (clean state every time)¶
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ request }) => {
await request.post('/api/test/reset'); // fresh DB state per test (via API = fast)
});
test('isolated test', async ({ page }) => {
// starts from a known, clean state — no dependency on other tests
});
Serial vs parallel within a file¶
By default tests in a file can run in parallel. If tests must share state and run in order (rare — usually a smell), opt in explicitly:
test.describe.configure({ mode: 'serial' }); // run in order, stop on first failure
// or force independence:
test.describe.configure({ mode: 'parallel' });
Diagnose order-dependence:
npx playwright test --repeat-each=3and shuffle by running with different worker counts. A test that only passes "in the suite" is hiding a state leak.
4. Parallelization — One Machine and Many¶
Playwright is parallel by default. Two layers: workers (parallelism on one machine) and sharding (splitting across many machines/CI jobs).
Workers (single machine)¶
A worker is an OS process running tests concurrently. More workers = faster, bounded by CPU.
// playwright.config.ts
export default defineConfig({
fullyParallel: true, // parallelise tests WITHIN files too
workers: process.env.CI ? 4 : '50%', // 50% of cores locally, fixed in CI
});
npx playwright test --workers=8 # override at runtime
npx playwright test --workers=1 # force serial (debugging)
Sharding (across machines / CI jobs)¶
For big suites, split the work across N parallel CI machines. Each shard runs a slice; you then merge the reports.
# machine 1 of 4
npx playwright test --shard=1/4
# machine 2 of 4
npx playwright test --shard=2/4
# ... 3/4, 4/4 on their own runners
flowchart LR
SUITE["🧪 500 tests"] --> S1["Shard 1/4<br/>~125 tests · runner A"]
SUITE --> S2["Shard 2/4<br/>~125 tests · runner B"]
SUITE --> S3["Shard 3/4<br/>~125 tests · runner C"]
SUITE --> S4["Shard 4/4<br/>~125 tests · runner D"]
S1 & S2 & S3 & S4 --> MERGE["📊 merge-reports →<br/>one HTML report"]
| Lever | Scope | Use when |
|---|---|---|
workers |
One machine, many processes | Always — free speed up to CPU limits |
fullyParallel |
Parallelise tests inside each file | Tests are independent (they should be) |
--shard=i/n |
Split across machines/CI jobs | Suite too big for one runner's time budget |
projects |
Same tests × browsers/devices | Cross-browser / mobile coverage |
Cost of parallelism: it only works if tests are isolated (§3). Shared global state (a single seeded user, a fixed DB row) causes race conditions. Parallel-safe data = unique data per test (e.g. email with a timestamp/uuid).
5. UI Testing vs API Testing in Playwright¶
Same tool, two altitudes. Knowing which to use — and combining them — is a senior skill.
| Dimension | UI testing | API testing |
|---|---|---|
| What it drives | Real browser — page.click/fill, DOM |
HTTP via request fixture |
| Verifies | What the user sees & does | The contract: status, body, schema |
| Speed | Slower (render, network, paint) | Fast (no browser) |
| Stability | More moving parts → more flake risk | Very stable |
| Browser needed? | Yes (npx playwright install) |
No (pure-API project) |
| Best for | Critical user journeys, visual/UX | Business logic, edge cases, data setup, broad coverage |
| Failure clue | Trace + screenshot + video | Status code + response body |
// UI: behaviour through the browser
test('checkout flow', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});
// API: contract directly
test('checkout API', async ({ request }) => {
const res = await request.post('/api/checkout', { data: { cartId: 1 } });
expect(res.status()).toBe(201);
expect((await res.json()).status).toBe('confirmed');
});
// HYBRID (best): arrange via API, assert via UI — fast + realistic
test('seeded order shows in UI', async ({ request, page }) => {
const id = (await (await request.post('/api/orders', { data:{item:'Widget'} })).json()).id;
await page.goto('/orders');
await expect(page.getByTestId(`order-${id}`)).toBeVisible();
});
Strategy: push coverage down the pyramid. Test business rules and edge cases at the API layer (fast, stable); reserve UI tests for genuine user journeys. Full depth on API specifics in API Testing with Playwright and Advanced API Testing.
6. Reporting¶
Playwright has built-in reporters; you can run several at once (one for humans, one for CI machines).
// playwright.config.ts
export default defineConfig({
reporter: [
['html', { open: 'never' }], // rich, interactive — for humans
['list'], // live console output
['junit', { outputFile: 'results/junit.xml' }], // for CI dashboards
['json', { outputFile: 'results/report.json' }], // for custom processing
],
});
| Reporter | Purpose |
|---|---|
html |
Interactive report: pass/fail, durations, embedded traces, screenshots, videos |
list / line / dot |
Console output (verbose → minimal). Default is list locally, dot on CI |
junit |
XML consumed by Jenkins, Azure DevOps, GitLab test tabs |
json |
Machine-readable for custom tooling/trend dashboards |
blob |
Intermediate format from each shard, merged into one report |
github |
Inline annotations on GitHub Actions PRs |
| 3rd-party | Allure, Currents, Tesults for history/trends |
The Trace Viewer — your #1 debugging tool¶
Capture a trace on failure and time-travel through the run: every action, DOM snapshot, network call, and console log.
npx playwright show-report # open the HTML report
npx playwright show-trace trace.zip # open a specific trace
Merging sharded reports¶
# each shard uploads its blob report; then on a final job:
npx playwright merge-reports --reporter=html ./all-blob-reports
7. CI/CD Integration¶
The suite must run automatically on every push/PR and gate merges. Below: a production GitHub Actions workflow with sharding, artifact upload, and report merge.
name: e2e
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4] # 4 parallel runners
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}/4
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: always() # upload even on failure — you need the evidence
with:
name: blob-report-${{ matrix.shard }}
path: blob-report/
retention-days: 7
merge-report:
needs: [test]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- uses: actions/download-artifact@v4
with: { path: all-blob-reports, pattern: blob-report-* }
- run: npx playwright merge-reports --reporter=html ./all-blob-reports
- uses: actions/upload-artifact@v4
with: { name: html-report, path: playwright-report/ }
flowchart LR
PUSH["push / PR"] --> RUN["4 sharded jobs<br/>run in parallel"]
RUN --> ART["upload blob reports<br/>(+ traces on failure)"]
ART --> MRG["merge-report job<br/>→ single HTML report"]
MRG --> GATE{"all green?"}
GATE -->|yes| MERGE["✅ allow merge / deploy"]
GATE -->|no| BLOCK["❌ block + publish report"]
CI best practices:
- Use the official Playwright Docker image or
playwright install --with-depsso browser deps are present. - Pin versions (
npm ci+ lockfile) for reproducible runs (see Project Anatomy). - Set
retries: process.env.CI ? 2 : 0— retry genuine flakes in CI only, never mask them locally. - Always upload artifacts (
if: always()) — traces/screenshots are how you debug a red pipeline without re-running. - Run API smoke tests first as a fast gate; heavier UI shards after.
- Authenticate once in a setup project and reuse
storageStateacross shards. - Quarantine (tag + track), don't delete, flaky tests — fix them as a ritual (§2).
- When the same suite runs against multiple environments, set a global
testConfig.tag(e.g.@staging) — it brings clarity to the merged report and produces a unique blob-report name per environment.
8. Rapid-Fire Q&A (Interview Style)¶
| Question | Crisp answer |
|---|---|
| How is each test isolated? | Own BrowserContext — separate cookies/storage; clean state per test, parallel-safe |
| How do you parallelize? | workers (one machine) + fullyParallel; --shard=i/n across CI machines; merge blob reports |
| Run one test only? | -g "title", file.spec.ts:line, or --project= |
| Avoid flaky tests? | Web-first auto-retrying assertions, resilient locators, no hard waits, unique data per test, CI retries |
| UI vs API? | UI = browser/user behaviour (slower); API = request/contract (fast, stable). Hybrid: API setup + UI verify |
| Reporting? | html for humans + junit/blob for CI; trace viewer for debugging; merge sharded reports |
| CI/CD? | GitHub Actions matrix sharding, install --with-deps, upload artifacts if: always(), merge reports, gate merge |
| Share knowledge? | Conventions doc, Page Objects/fixtures, PR checklist, pairing, CI-enforced standards |
| Cross-browser? | projects for chromium/firefox/webkit + mobile devices, same specs |
Where to Go Next¶
- Playwright Framework Tutorial — the structure these practices live in
- API Testing with Playwright — the fast layer of the pyramid
- Playwright Project Anatomy — config,
node_modules, reproducible installs - Advanced API Testing — WebSockets, media, AI/non-deterministic APIs
- Official docs: playwright.dev/docs/test-parallel · test-sharding · test-reporters