PRACTICAL GUIDE / playwright framework architecture fixtures
Architecture for a Domain-Driven Playwright Suite with Fixtures and Component Models
Design a domain-driven Playwright framework where business scenarios use typed fixtures, component models, stable primitives, and failure-focused evidence.
In this guide9 sections
- Start with business capabilities, not folders
- Enforce a one-way dependency direction
- Give component models a narrow interaction boundary
- Use domain fixtures as composition roots
- Keep scenario files declarative but discriminating
- Design fixture scope from mutation behavior
- Preserve evidence before destructive cleanup
- Configure projects at architecture boundaries
- Abstract only after a boundary repeats
What you will learn
- Start with business capabilities, not folders
- Enforce a one-way dependency direction
- Give component models a narrow interaction boundary
- Use domain fixtures as composition roots
A scalable Playwright framework should make business risk obvious at the test file and technical mechanics obvious at their owning layer. When a test mixes API seeding, account selection, selectors, cleanup, and report attachments, every scenario becomes its own small framework. Domain-driven structure gives those responsibilities stable homes.
The objective is not to hide Playwright. It is to preserve a one-way path from business scenario to domain fixture to component model to Playwright primitive. Failures should travel back with enough evidence to identify which boundary broke.
Start with business capabilities, not folders
List the product transitions the suite protects: approve a claim, renew a subscription, transfer ownership, or reject a refund. Group setup and models around those capabilities. A domain is useful when it has shared language, state, and ownership; a folder named pages is only a storage location.
The Playwright fixtures guide describes fixtures as isolated, on-demand, and composable. Those properties make fixtures good composition roots for domain scenarios. They do not make every helper a fixture. Lifecycle and dependency ownership remain the deciding factors.
Animated field map
Domain-Driven Playwright Dependency Flow
Business intent requests a domain environment, component models translate interactions, Playwright executes them, and reporters retain evidence.
01 / business scenarios
Business scenarios
State product rules, actors, transitions, and observable outcomes.
02 / domain fixtures
Domain fixtures
Compose identity, data, models, lifecycle, and scenario evidence.
03 / component models
Component models
Own locators and interactions for coherent rendered surfaces.
04 / playwright primitives
Playwright primitives
Provide browser contexts, locators, requests, assertions, and tracing.
05 / reporter evidence
Reporter evidence
Return domain state, steps, attachments, and failure ownership.
Enforce a one-way dependency direction
Tests may depend on a domain test surface. Domain fixtures may depend on API clients, data builders, and component models. Component models may depend on Page or Locator. Low-level modules must not import tests or call domain fixtures. This direction prevents a selector change from knowing about billing policy and prevents a data client from opening a browser.
A practical layout makes the boundary visible without forcing a package for every class:
tests/
billing/
renewal.spec.ts
test-support/
domains/
billing-test.ts
components/
renewal-panel.ts
payment-form.ts
clients/
billing-api.ts
data/
subscription-builder.ts
evidence/
domain-attachments.tsExport test and expect from one supported domain entry point. A test that imports @playwright/test directly can bypass fixture extensions and evidence hooks. Enforce the import surface with linting or a small architecture check, but allow infrastructure-level tests to use the base runner intentionally.
Give component models a narrow interaction boundary
A component model should represent a coherent rendered surface, not an entire application. It owns stable locators, atomic interactions, and state that the component exposes. It should not create customer records, choose a tenant, or decide whether a declined payment is acceptable.
// test-support/components/renewal-panel.ts
import { expect, type Locator, type Page } from '@playwright/test';
export class RenewalPanel {
readonly root: Locator;
private readonly renewButton: Locator;
constructor(page: Page) {
this.root = page.getByTestId('renewal-panel');
this.renewButton = this.root.getByRole('button', { name: 'Renew subscription' });
}
async chooseTerm(term: 'Monthly' | 'Annual'): Promise<void> {
await this.root.getByRole('radio', { name: term }).check();
}
async submit(): Promise<void> {
await expect(this.renewButton).toBeEnabled();
await this.renewButton.click();
}
status(): Locator {
return this.root.getByRole('status');
}
}Returning a locator for observable state keeps the test's business assertion visible. A model may assert a local invariant required to act, such as an enabled submit control, but a method named renewSuccessfully() should not click, swallow errors, and assert a business result invisibly.
Use domain fixtures as composition roots
The fixture creates a scenario-ready environment and owns everything it allocates. It can seed through an API, navigate to the right aggregate, construct component models, and release the record afterward. Return domain language rather than a bag of unrelated helpers.
// test-support/domains/billing-test.ts
import { test as base, expect } from '@playwright/test';
import { RenewalPanel } from '../components/renewal-panel';
type Subscription = {
id: string;
plan: string;
status: 'active' | 'past_due';
};
type BillingScenario = {
subscription: Subscription;
renewal: RenewalPanel;
};
type BillingFixtures = {
activeSubscription: BillingScenario;
};
export const test = base.extend<BillingFixtures>({
activeSubscription: async ({ page, request }, use, testInfo) => {
const created = await request.post('/api/test-support/subscriptions', {
data: { plan: 'standard', status: 'active' },
});
expect(created.ok()).toBeTruthy();
const subscription = (await created.json()) as Subscription;
await testInfo.attach('subscription-seed.json', {
body: Buffer.from(JSON.stringify({ id: subscription.id, plan: subscription.plan })),
contentType: 'application/json',
});
try {
await page.goto(`/billing/subscriptions/${subscription.id}`);
await use({ subscription, renewal: new RenewalPanel(page) });
} finally {
const deleted = await request.delete(
`/api/test-support/subscriptions/${subscription.id}`,
);
if (!deleted.ok() && deleted.status() !== 404) {
throw new Error(`Subscription cleanup failed: ${deleted.status()}`);
}
}
},
});
export { expect };The cleanup accepts an already-deleted resource because the scenario may legitimately remove it. Other failures remain visible. In a production framework, redact customer details from attachments and give test-support endpoints strict environment and authorization controls.
Keep scenario files declarative but discriminating
Readable does not mean vague. The test should state the input that matters, the transition, and the observed outcome. Hide transport mechanics; keep risk-bearing decisions in view.
import { expect, test } from '../../test-support/domains/billing-test';
test('an active annual subscription renews without losing its plan', async ({
activeSubscription,
}) => {
const { renewal, subscription } = activeSubscription;
await renewal.chooseTerm('Annual');
await renewal.submit();
await expect(renewal.status()).toHaveText('Renewal scheduled');
await expect(renewal.root).toContainText(subscription.plan);
});Do not compress a whole scenario into billing.renewActiveSubscription(). That removes the transition from review and makes different tests look identical. The abstraction should reduce accidental detail while leaving the reason for the test legible.
Design fixture scope from mutation behavior
Test-scoped fixtures are the default for mutable domain records because they maximize isolation and make cleanup ownership simple. Worker-scoped fixtures can amortize expensive account or environment setup, but tests in that worker must not compete through shared mutable state. A browser context does not isolate server-side records.
Use option fixtures or project metadata for stable environment policy, such as region or product edition. Do not branch throughout component methods on process.env. Centralized options make the supported matrix typed and reviewable.
Automatic fixtures are appropriate for cross-cutting evidence or policy that every test needs. They are a poor fit for expensive domain setup because automatic work runs even when a scenario does not consume it. Keep domain fixtures lazy so test dependencies reveal cost.
Preserve evidence before destructive cleanup
Fixture teardown still runs after a test failure, so cleanup can erase the server state needed for diagnosis. Attach safe identifiers and domain status before deletion when the test outcome differs from expectation. If backend records are essential, export a redacted snapshot or correlate them with a run ID rather than leaving shared environments dirty indefinitely.
Cleanup must be idempotent and bounded. A terminated worker may never reach teardown, so remote resources also need expiry, a run-level sweeper, or deterministic namespace deletion. Report cleanup failure separately from assertion failure; otherwise a cleanup exception can obscure the product regression that triggered it.
Keep component-level Playwright steps meaningful. Box noisy fixture wrappers when appropriate, but do not hide the API seed, navigation, or cleanup phase that an operator needs to distinguish framework failure from product failure.
Configure projects at architecture boundaries
Projects should represent meaningful execution policies such as browser, authenticated role, or environment contract. They should not compensate for unclear domains by creating a project for every folder. Use testMatch, metadata, and fixture options to connect a project to supported suites while keeping domain modules independent of CI job names.
Avoid a global fixture that assumes every project has the same authentication or base URL. Resolve required values during fixture setup and fail with a precise configuration message. A missing billing API credential should not become a navigation timeout in the first test.
Keep reporters and trace settings in root configuration, while domain fixtures contribute attachments and named steps. This produces a common evidence envelope without making the reporter understand every business object.
Test the framework boundaries as code. A component model can have focused tests against a small fixture page, a domain client can have API contract tests, and the domain fixture can have one smoke scenario that proves setup and teardown. These checks localize framework regressions before a full suite reports dozens of business failures. They also let maintainers change an internal layer while preserving the supported domain test import and fixture types.
Abstract only after a boundary repeats
Duplication is sometimes useful evidence that two domains are not actually the same. Two components with similar Save buttons may have different readiness and error contracts. Merge them only after the shared behavior and ownership are clear.
Prefer a small function for pure data transformation, a component model for rendered interaction, a client for transport, and a fixture for lifecycle. Avoid a generic BasePage, universal CRUD fixture, or assertion wrapper that reduces every outcome to a boolean. Those abstractions centralize code while discarding domain meaning.
Set a deletion rule for abstractions too. If a wrapper only renames one Playwright call, forces every test through unused setup, or requires environment branches for unrelated domains, remove or split it. Framework code should earn its maintenance cost by protecting a lifecycle, dependency, or product vocabulary that more than one meaningful scenario shares.
Use this architecture review checklist:
- Scenario names describe a business rule or risk-bearing transition.
- Dependencies point from tests toward domain, component, and primitive layers.
- Each fixture owns setup, safe evidence, teardown, and interruption recovery.
- Component methods stay within one rendered surface and expose observable state.
- Mutable server records are isolated at the scope where tests can collide.
- Project policy is centralized without environment branches inside models.
- Failures identify configuration, setup, interaction, assertion, or cleanup ownership.
Migrate one domain at a time. Move selectors into focused component models, wrap lifecycle in a typed fixture, preserve scenario assertions in tests, and run old and new paths against the same behavior until confidence is earned. The architecture is working when adding a business case requires composing established boundaries, not copying another test file's private framework.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
What makes a Playwright suite domain-driven?
Tests and fixtures are organized around business capabilities and state transitions, while page details remain behind component models. Scenario names describe product risk instead of navigation mechanics.
How are domain fixtures different from page objects?
A domain fixture composes data, identity, API setup, UI models, cleanup, and evidence for a scenario. A page or component object owns interaction with one rendered surface and should not provision business records.
Should every Playwright helper become a fixture?
No. Use fixtures for lifecycle-managed or configurable dependencies. Keep pure transformations as functions and create component models directly when they require no setup or teardown ownership.
Where should assertions live in a component model architecture?
Component models can expose state and narrow invariant checks, but scenario-level business outcomes belong in tests. Hiding every assertion inside methods makes failures and intent harder to review.
How should a domain fixture handle cleanup after a failed test?
Place cleanup after use in a finally-safe fixture lifecycle, make deletion idempotent, preserve failure evidence before destructive reset, and provide a run-level recovery path for interrupted workers.
RELATED GUIDES
Continue the learning route
GUIDE 01
Composing Modular Playwright Fixtures with mergeTests and Boxed Steps
Compose modular Playwright fixtures with mergeTests, explicit dependency ownership, boxed reporting, and a stable test-support import surface.
GUIDE 02
Playwright Page Component Objects for Shared Navigation, Tables, and Modals
Design Playwright page component objects for shared navigation, tables, and modals with semantic locators, fixtures, composition, and focused assertions.
GUIDE 03
Strict TypeScript Patterns for Playwright Fixtures, Models, and Test Data
Apply strict TypeScript to Playwright fixtures, models, environment data, and case tables with compiler checks, runtime validation, and safe narrowing.
GUIDE 04
How to Build a Test Automation Framework from Scratch
Learn how to build a test automation framework from scratch with layers, design patterns, reporting, CI/CD hooks, and a practical starter architecture.