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.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide9 sections
  1. Start with business capabilities, not folders
  2. Enforce a one-way dependency direction
  3. Give component models a narrow interaction boundary
  4. Use domain fixtures as composition roots
  5. Keep scenario files declarative but discriminating
  6. Design fixture scope from mutation behavior
  7. Preserve evidence before destructive cleanup
  8. Configure projects at architecture boundaries
  9. 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.

  1. 01 / business scenarios

    Business scenarios

    State product rules, actors, transitions, and observable outcomes.

  2. 02 / domain fixtures

    Domain fixtures

    Compose identity, data, models, lifecycle, and scenario evidence.

  3. 03 / component models

    Component models

    Own locators and interactions for coherent rendered surfaces.

  4. 04 / playwright primitives

    Playwright primitives

    Provide browser contexts, locators, requests, assertions, and tracing.

  5. 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:

Example
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.ts

Export 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.

TypeScript
// 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.

TypeScript
// 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.

TypeScript
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.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

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.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

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.