Skip to content

How to build a scalable Playwright framework

Updated 2026-09-28
6
layers
11
files to start
1.63
code checked against

A scalable Playwright framework is mostly Playwright defaults, organised: tests that read like scenarios, fixtures that inject page objects and auth, thin page objects built on role and label locators, test data in its own folder, API calls for setup, and CI that shards, retries and keeps traces. Everything else — wrappers, base classes, custom retry logic — waits until you can prove you need it.

Writing Playwright tests is easy. Keeping 500 of them fast, readable and trustworthy a year later is the actual job. This guide builds a framework layer by layer — every file is clickable in the explorer below and every snippet compiles against Playwright 1.63 — and it is just as explicit about what not to build.

Key takeaways

  • Start from npm init playwright@latest and change defaults only when a test forces you to.
  • Fixtures replace beforeEach and base-class inheritance: tests ask for loginPage and get one.
  • Page objects hold locators and intent-level actions; assertions stay in the test so failures read clearly.
  • Use the API for setup and cleanup, the UI only for the behaviour under test — and log in once with storageState.
  • In CI: install browsers with --with-deps, shard across machines, retry to surface flakes, and merge the shards' blob reports into one HTML report with traces.

The architecture in one picture

Each layer only talks to the one below it. Hover a layer to highlight it; Replay runs the walk-through again.

  1. TestsScenarios and assertions. Read like a user story; no selectors, no waits.
  2. FixturesInject page objects, API clients, test data and auth into each test.
  3. Page objects & APILocators and intent actions per screen; typed API helpers for setup.
  4. PlaywrightAuto-waiting, locators and browser contexts on Chromium, Firefox and WebKit.
  5. Reports & tracesHTML report, screenshots, video and a trace for every retried failure.
  6. CI/CDSharded runs on every pull request; artifacts kept for debugging.

The folder structure — click any file

Eleven files cover a real framework. Each one has a single job.

playwright-framework/
tests/
pages/
fixtures/
data/
utils/
.github/
workflows/
tests/auth.setup.ts

Logs in once per run and saves the session. Every browser project depends on it.

tests/auth.setup.tsTypeScript
import { test as setup, expect } from '@playwright/test';
import { users } from '../data/users';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(users.valid.email);
  await page.getByLabel('Password').fill(users.valid.password);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await page.context().storageState({ path: authFile });
});

The CI pipeline

What happens after git push, step by step.

  1. Push / PRA push to main or any pull request triggers the workflow.
  2. npm ciClean install from the lockfile — same versions as your laptop.
  3. Install browsersnpx playwright install --with-deps adds browsers and OS libraries.
  4. Run 4 shards--shard=1/4 … 4/4 run in parallel; each uploads a blob report.
  5. Merge reportsA merge job combines the blobs into one HTML report — even when tests fail.
  6. ReviewDownload the report, open the trace of any failed or flaky test.

Step 1 — Scaffold the project

The official initializer asks four questions and writes a working config, an example test and (optionally) a GitHub Actions workflow. Accept TypeScript, keep the tests folder, say yes to the workflow and to installing browsers.

Terminal
npm init playwright@latest

# verify
npx playwright test
npx playwright show-report

Page objects: thin on purpose

A page object answers two questions: where is it, and what can a user do here. Expose locators and intent-level actions (login, addToCart). Keep expectations in the test, so a failure message says what the test expected rather than which helper failed. Prefer getByRole and getByLabel; fall back to getByTestId for elements with no accessible name.

Tip: Return Locators, not strings or booleans. A test can then assert with auto-retrying expect(locator).toHaveText(…) instead of reading textContent once and hoping.

Fixtures instead of beforeEach and base classes

Fixtures are created only when a test asks for them, torn down automatically, and composable — a test that needs a logged-in admin and a seeded cart says so in its signature. They replace base-class inheritance chains, which get harder to change with every test you add.

API for setup, UI for the behaviour

Creating a user through five screens tests the signup flow again and again. Create it with one request call, test the screen you care about, and delete it afterwards. Combined with storageState auth, this is usually where a suite wins back the most run time.

What not to build (yet)

Each of these costs maintenance from day one. Add one only when a real test cannot be written without it.

  • Wrappers around click / fill / waitFor — Playwright already auto-waits and retries.
  • Custom retry loops — use retries in the config, and fix what the report marks as flaky.
  • Explicit waits and sleeps — web-first assertions wait for you.
  • BaseTest / BasePage inheritance trees — use fixtures and composition.
  • Excel or CSV readers before you have data-driven tests that need them.
  • A custom reporter before the built-in HTML report stops being enough.

Pro tips

Small habits that keep a framework healthy as it grows.

  • Tag tests ({ tag: '@smoke' }) and run subsets with --grep instead of maintaining separate suites.
  • Keep every test independent: its own data, no order dependency, safe to run in parallel.
  • Put BASE_URL and credentials in environment variables; commit a .env.example, never .env.
  • Retries in CI are a detector, not a fix — open the trace of every test the report marks as flaky.
  • Run npx playwright test --only-changed locally for fast feedback before pushing.

FAQ

tests/ for specs, pages/ for page objects, fixtures/ for a custom test with injected page objects, data/ for test data, utils/ for small helpers, plus playwright.config.ts and a CI workflow. Add folders only when a real need appears.

Sources

Related tools

Related guides