Why You Need a Framework
Playwright out of the box is powerful, but 'a folder full of .spec.ts files' is not a framework. Once your suite grows past 50 tests, three shared engineers, or one production release cycle, the absence of shared conventions starts to hurt: duplicated login code, brittle selectors copy-pasted between files, ad-hoc reporters, and 'works on my machine' failures in CI.
An enterprise-grade Playwright framework provides opinionated answers to the questions every real project faces — where does test data live, how do we authenticate, how do we tag and slice the suite, how do we ship reports to the CI dashboard, and how do we keep flakiness under 1%. Get those right and adding the 500th test feels the same as adding the 5th.
Prerequisites
- Node.js 18+ and a Playwright project scaffolded with npm init playwright@latest.
- Strong TypeScript fundamentals — this guide leans on generics and interfaces.
- Familiarity with the Page Object Model (see our POM guide).
- Access to a CI system (GitHub Actions, GitLab CI, or Jenkins) for the reporting section.
Recommended Repo Layout
Structure signals intent. A newcomer opening the repo should be able to guess where things live within thirty seconds. The layout below scales cleanly from ten tests to a few thousand.
playwright-framework/
├── tests/
│ ├── ui/ # browser-driven flows
│ ├── api/ # pure API tests via APIRequestContext
│ └── e2e/ # cross-layer critical journeys
├── pages/ # one class per screen
├── components/ # reusable UI components (header, modal…)
├── fixtures/ # custom test fixtures
├── data/ # JSON/CSV test data & builders
├── utils/ # date, string, environment helpers
├── playwright.config.ts
└── .github/workflows/e2e.ymlplaywright.config.ts
The config file is where a framework earns its keep. Retries in CI only, tracing on first retry, per-project browser matrices, and reporters that both humans and CI dashboards can consume — all defined once.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['list'], ['html', { open: 'never' }], ['junit', { outputFile: 'reports/junit.xml' }]],
use: {
baseURL: process.env.BASE_URL ?? 'https://staging.example.com',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});Custom Fixtures
Fixtures are the beating heart of a Playwright framework. They inject fully-constructed dependencies — page objects, API clients, authenticated sessions — into every test that asks for them, and clean up automatically when the test finishes. Once fixtures are in place, individual tests shrink to just the intent.
import { test as base, APIRequestContext } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type Fixtures = {
login: LoginPage;
dashboard: DashboardPage;
api: APIRequestContext;
};
export const test = base.extend<Fixtures>({
login: async ({ page }, use) => use(new LoginPage(page)),
dashboard: async ({ page }, use) => use(new DashboardPage(page)),
api: async ({ playwright }, use) => {
const api = await playwright.request.newContext({ baseURL: process.env.API_URL });
await use(api);
await api.dispose();
},
});
export { expect } from '@playwright/test';Data-Driven Tests
Real regression suites rarely test a single input. Loop over a data table to generate one test per case — each gets its own name, its own trace, and its own pass/fail line in the report.
const users = [
{ role: 'admin', menu: 'Users' },
{ role: 'editor', menu: 'Posts' },
{ role: 'viewer', menu: 'Feed' },
];
for (const u of users) {
test(`${u.role} sees the ${u.menu} menu`, async ({ login, page }) => {
await login.loginAs(u.role, 'secret');
await expect(page.getByRole('navigation')).toContainText(u.menu);
});
}Test Tagging & Selection
A large suite needs slicing. Tag tests with @smoke, @regression, @critical, or @slow so pull-request runs stay fast while nightly runs cover everything.
test('checkout @smoke @critical', async () => { /* ... */ });
// Run only smoke tests on PRs
// npx playwright test --grep @smokeReporting & Traceability
- HTML reporter for humans — npx playwright show-report opens an interactive local dashboard.
- JUnit reporter feeds CI dashboards (GitHub Actions test summary, GitLab widgets, Jenkins).
- Trace Viewer is the killer feature — trace: 'on-first-retry' captures a full DOM+network timeline of any failure.
- Upload reports and traces as build artifacts so any engineer can debug a red build without re-running it locally.
- Wire in Allure or a bespoke reporter if leadership requires long-term trend dashboards.
Common Pitfalls
- No retries in CI — a single network blip fails your pipeline. Set retries: 2 on CI only.
- Sharing browser context between tests — leads to flaky pollution. Use one context per test (Playwright's default).
- Hardcoding baseURL — always read from process.env.BASE_URL so the same suite runs against staging, production, and local.
- Skipping trace on first retry — the biggest debugging shortcut Playwright offers.
- A monolithic fixtures file — split fixtures by concern (auth, pages, api) once it grows past 200 lines.
Debugging Tips
- Open the failing trace with npx playwright show-trace trace.zip — you can scrub the timeline, inspect the DOM at every action, and see every network call.
- Reproduce a CI-only failure locally with CI=true npx playwright test --project=chromium.
- Use test.only during local debugging, then rely on ESLint's no-focused-tests rule to prevent it landing in main.
- For flaky tests, run npx playwright test --repeat-each=20 to reproduce intermittent failures fast.
When to Use — and When Not To
Invest in this framework structure when you have more than one engineer contributing tests, a suite you expect to run in CI on every commit, or a product with a real release cadence. The upfront cost pays back within weeks.
Skip the full framework for exploratory automation, one-off scraping tasks, or spikes that will be deleted next week. A single spec file is fine at that scale — do not over-engineer.
FAQ
Should I use Playwright Test or Playwright Library?
Playwright Test (the @playwright/test runner) for anything test-shaped. The Library is for scripts, scraping, or embedding automation inside another tool.
Where does authentication live?
Use globalSetup to log in once, save storageState to a JSON file, and reference it in playwright.config.ts under use.storageState. Every test starts already authenticated.
How many workers should I run in CI?
Start with 4 and tune based on your CI runner CPU/RAM. Too many workers thrash the machine and increase flakiness.
Do I need Docker to run in CI?
Not required, but the official mcr.microsoft.com/playwright image gives you a reproducible browser environment with all system dependencies baked in — highly recommended for CI.
How do I keep flakiness below one percent?
Three levers make the biggest difference: use Playwright assertions (which auto-retry) instead of manual textContent checks, isolate every test with its own browser context and its own data, and treat every intermittent failure as a bug to fix rather than a nuisance to retry away. Track your flake rate week over week — if it drifts up, something regressed.
Should test data live in the repo or be generated on the fly?
Both. Small deterministic fixtures (a handful of user roles, canonical products) live under data/ as JSON. Anything that needs to be unique per run — email addresses, order numbers, tenant slugs — is generated on the fly with a builder and a random suffix, so parallel workers never collide.
How do we onboard a new engineer to the framework?
A good framework carries its own onboarding — a README that documents the layout, a make test-smoke that runs in under two minutes, and a template spec file with comments. If a new hire cannot land a green test on day one, the framework has too much friction and needs to be simplified.
Summary
A production Playwright framework is not a single file — it is a set of conventions: a predictable folder layout, a well-tuned playwright.config.ts, fixtures that inject dependencies, page objects that hide DOM detail, tag-based slicing for fast feedback, and reporters that leadership can actually read. Ship those conventions once and the marginal cost of every new test drops to near zero, which is the real win of enterprise test automation.
Treat the framework itself as a product. It has users (the engineers writing tests), a changelog (the git history of playwright.config.ts and fixtures/), and a support surface (the README and the on-call channel when CI breaks). The teams that get the most out of Playwright are the ones where a small platform group owns and evolves the framework, while feature teams focus on writing tests against the abstractions it provides.
Finally, do not over-build up front. Start with the layout, config, and fixtures above, then let real pain drive every new addition — a custom reporter when leadership asks for trend data, a visual-regression plugin when a UI redesign burns you, a sharding strategy when the suite crosses ten minutes. The framework grows with the product, and every convention earns its place by solving a problem you actually hit.
Get Playwright tutorials in your inbox
Weekly tips, real-world examples, and framework patterns – no spam, unsubscribe anytime.