A Playwright suite for one team can be a folder of specs; a framework for many teams needs architecture. This guide covers framework layers, the design patterns that actually help in automation, an enterprise structure, and a comprehensive best-practice checklist.

Framework Architecture

Level: L5 (Architect). Where everything comes together.

What is it?

The overall structure of a professional Playwright framework: how folders, page objects, components, fixtures, utilities, data, config, and CI fit together into a maintainable, scalable whole.

Why do we need it?

Scattered tests don’t scale to hundreds of specs and multiple contributors. A deliberate architecture makes the suite readable, maintainable, parallel-safe, and easy to extend — the difference between a script collection and a framework.

Professional folder structure

playwright-framework/
├── tests/ # spec files (the "what")
│ ├── smoke/
│ ├── regression/
│ └── api/
├── pages/ # Page Objects (UI actions)
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ └── InventoryPage.ts
├── components/ # reusable UI components (Header, Card)
├── fixtures/ # custom fixtures (auth, page objects, data)
│ └── test.ts # extended `test`/`expect` imported everywhere
├── utils/ # helpers: logger, api client, dates, strings
├── data/ # static test data (JSON) + factories
│ ├── users.json
│ └── factories.ts
├── config/ # env loading + validation
│ └── env.ts
├── auth/ # storageState files (gitignored)
├── playwright.config.ts # central config
├── .env.example # documented env keys
├── Dockerfile
├── .github/workflows/ # CI
└── package.json

What each folder is for

  • tests/ — specs only; they orchestrate page objects and assert. Split by suite (smoke/regression/api) for selective runs.
  • pages/ — one class per page; encapsulate locators + actions (no assertions).
  • components/ — repeated widgets (nav, cards) composed into pages.
  • fixtures/ — the extended test that injects page objects, authed sessions, and data; imported by every spec.
  • utils/ — pure, typed helpers (logger, API client, date/string).
  • data/ — static reference data (JSON) + dynamic factories (Faker).
  • config/ — environment loading/validation (Module 55).
  • auth/ — storageState per role (gitignored — contains tokens).
  • playwright.config.ts — timeouts, retries, workers, projects, reporters (Module 25).

The dependency flow

tests → fixtures → (pages + components + utils + data + config)
│
└─ page objects use locators; utils/api seed data;
config supplies env; fixtures wire it together

Practical Example — a spec that shows the architecture

// tests/regression/checkout.spec.tsimport { test, expect } from '../../fixtures/test';   // custom fixturesimport { newUser } from '../../data/factories';test('user can complete checkout @regression', async ({ loginPage, inventoryPage, api }) => {  const user = await api.createUser(newUser());        // API seed (util)  await loginPage.goto();  await loginPage.login(user.email, user.password);    // page object  await inventoryPage.add('Sauce Labs Backpack');      // page object  await expect(inventoryPage.header.cartBadge).toHaveText('1'); // component + assert in test});

Line-by-Line Explanation

  • The spec imports the custom test (fixtures) — so loginPage, inventoryPage, and api arrive ready-to-use.
  • Data comes from a factory; seeding uses the api util; interactions use page objects/components; assertions stay in the test.
  • Every responsibility lives in its proper layer — the spec reads like a business scenario.

Common Mistakes

  • Business logic/assertions leaking into page objects.
  • No fixtures layer → repetitive setup in every spec.
  • Flat tests/ with no suite separation → can’t run subsets cleanly.
  • Utilities/data/config mixed together with no boundaries.

Best Practices

  • Clear layers: tests (assert) → fixtures (wire) → pages/components (act) → utils/data/config (support).
  • Custom test/expect in fixtures/test.ts, imported everywhere.
  • Separate suites for selective CI runs.
  • Gitignore auth/, artifacts, and .env*; commit .env.example.
  • Keep the architecture documented in a README.

Interview Questions

  • Q: Walk me through your framework structure. A: Layered — specs assert; fixtures inject page objects/auth/data; pages/components encapsulate UI; utils/data/config support; central config + CI + Docker around it.
  • Q: Where do assertions live? A: In tests, not page objects.
  • Q: How do specs get their dependencies? A: Via custom fixtures (dependency injection).
  • Q: How do you keep it scalable? A: Layer boundaries, composition, suite separation, unique data, and parallel-safe isolation.

Practice Exercise

Restructure your accumulated tests into this layout: create fixtures/test.ts, move page objects/components, add utils/api.ts, data/factories.ts, and config/env.ts. Convert one spec to the architecture in section 6.

Real-World Scenario

A startup’s 30 ad-hoc specs became unmaintainable at 300. Refactoring into this layered architecture (fixtures + pages + components + utils) let five engineers work in parallel without stepping on each other, and onboarding a new SDET dropped from weeks to days.

Advertisement

Design Patterns (Applied to Automation)

Level: L5 (Architect).

What is it?

Proven software design patterns applied to test automation: Page Object, Factory, Builder, Strategy, Singleton (with caveats), Facade, and Dependency Injection (via fixtures).

Why do we need it?

Patterns give a shared vocabulary and battle-tested structures for common problems (creating data, varying behavior, wiring dependencies), keeping large frameworks clean and extensible.

The patterns that matter here

Encapsulate a page’s locators/actions in a class. Problem solved: selector churn, duplication.

// data/factories.tsimport { faker } from '@faker-js/faker';export const userFactory = (overrides: Partial<User> = {}): User => ({  email: faker.internet.email(),  password: 'P@ssw0rd!',  role: 'user',  ...overrides,           // customize per test});

Problem solved: unique, typed test data without duplication.

class OrderBuilder {  private order: Order = { items: [], coupon: null, gift: false };  addItem(sku: string, qty = 1) { this.order.items.push({ sku, qty }); return this; }  withCoupon(code: string) { this.order.coupon = code; return this; }  asGift() { this.order.gift = true; return this; }  build() { return this.order; }}const order = new OrderBuilder().addItem('BACKPACK').withCoupon('SAVE10').asGift().build();

Problem solved: readable construction of complex, optional-field data.

interface LoginStrategy { login(page: Page, u: User): Promise<void>; }class UiLogin implements LoginStrategy { /* fill form + click */ async login(){} }class ApiLogin implements LoginStrategy { /* set storageState via API */ async login(){} }async function authenticate(page: Page, user: User, strategy: LoginStrategy) {  await strategy.login(page, user);   // caller chooses UI vs API login}
Problem solved: choose UI login (when testing login) vs fast API login (everywhere else) without duplicating tests.
class CheckoutFacade {  constructor(private login: LoginPage, private inv: InventoryPage, private cart: CartPage) {}  async buy(user: User, sku: string) {         // one call hides many steps    await this.login.login(user.email, user.password);    await this.inv.add(sku);    await this.cart.checkout();  }}

Problem solved: readable high-level flows built from page objects.

Playwright fixtures are DI: they construct and inject page objects, auth, and data. Problem solved: decoupled, testable wiring without manual construction.

A single shared instance (e.g., a config object). Caveat: shared mutable singletons break parallel isolation. Safe only for immutable config; avoid for anything stateful across tests/workers.

Line-by-Line Explanation (Strategy example)

  • A common LoginStrategy interface defines login.
  • UiLogin/ApiLogin implement it differently.
  • authenticate takes any strategy — the test decides: use UiLogin in login tests, ApiLogin for speed elsewhere. No branching logic scattered around.

Common Mistakes

  • Pattern overuse (abstraction for its own sake) → harder to read.
  • Stateful Singletons shared across parallel workers → flaky collisions.
  • Builders/factories that duplicate what a simple object literal would do.
  • Reinventing DI when fixtures already provide it.

Best Practices

  • Reach for a pattern only when it solves a real, recurring problem.
  • Factory/Builder for data; Strategy for interchangeable behavior; Facade for readable flows; DI via fixtures.
  • Keep Singletons immutable (config only).
  • Prefer composition over inheritance.

Interview Questions

  • Q: Which patterns do you use in automation? A: Page Object, Factory/Builder for data, Strategy for UI-vs-API login, Facade for flows, DI via fixtures.
  • Q: Why is Singleton risky in parallel tests? A: Shared mutable state leaks across workers → non-deterministic failures.
  • Q: How does Playwright provide DI? A: Fixtures construct and inject dependencies (page objects, auth, data).
  • Q: Factory vs Builder? A: Factory creates a ready object from defaults/overrides; Builder assembles step-by-step for complex, optional fields.

Practice Exercise

Implement a userFactory (Factory), an OrderBuilder (Builder), and UI/API LoginStrategy classes. Write one test that seeds via factory and logs in via ApiLogin, and one login test that uses UiLogin.

Real-World Scenario

Most tests didn’t need to test login — they just needed to be logged in. Introducing a LoginStrategy let 90% of tests use fast ApiLogin while login-specific tests kept UiLogin, cutting suite time significantly with no loss of coverage.

Best Practices Recap

Patterns are tools, not trophies. Use Factory/Builder for data, Strategy for behavior choices, Facade for readable flows, and lean on fixtures for DI. Keep shared state immutable to stay parallel-safe.

Enterprise Framework

Level: L5 (Architect).

What is it?

A production-grade framework that scales to thousands of tests, many contributors, multiple environments and browsers, CI/CD, dashboards, and cross-team reuse — combining every prior module into one cohesive system.

Why do we need it?

Enterprises need reliability at scale: fast feedback, trustworthy results, easy onboarding, environment safety, and reporting stakeholders can read. An enterprise framework is an engineered product, not a test folder.

What makes a framework “enterprise-grade”

Reliability → web-first assertions, isolation, unique data, ~zero flakiness
Scalability → parallel workers + CI sharding; API/storageState setup
Maintainability → layered architecture, POM/components, fixtures (DI)
Configurability → env-driven (DEV/QA/UAT/PROD), validated, secret-safe
Cross-browser → chromium/firefox/webkit + mobile emulation
CI/CD → PR smoke gate, nightly regression, dashboards
Observability → HTML+JUnit reports, traces, logging, flakiness metrics
Reusability → shared utils/components, maybe an internal npm package
Governance → coding standards, linting, PR review, branch protection

Reference layout (adds enterprise concerns)

├── tests/{smoke,regression,api,e2e}/
├── pages/ components/ fixtures/ utils/ data/ config/
├── reporters/ # custom reporter (e.g., post to Slack/dashboard)
├── scripts/ # seeding, cleanup, report merge
├── .github/workflows/ # (or Jenkinsfile) sharded CI + nightly
├── Dockerfile docker-compose.yml
├── playwright.config.ts # projects, retries(CI), workers, reporters
├── eslint + prettier + tsconfig # standards
└── docs/ # onboarding + conventions

Practical Example — enterprise config choices

export default defineConfig({  fullyParallel: true,  retries: process.env.CI ? 2 : 0,  workers: process.env.CI ? 6 : undefined,  reporter: [    ['html', { open: 'never' }],    ['junit', { outputFile: 'results.xml' }],    ['blob'],                                   // for shard merging    ['./reporters/slack-reporter.ts'],          // custom notifications  ],  use: {    baseURL: process.env.BASE_URL,    trace: 'on-first-retry',    screenshot: 'only-on-failure',    video: 'retain-on-failure',  },  projects: [    { name: 'setup', testMatch: /auth\.setup\.ts/ },    { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: 'auth/user.json' }, dependencies: ['setup'] },    { name: 'firefox',  use: { ...devices['Desktop Firefox'] }, dependencies: ['setup'] },    { name: 'webkit',   use: { ...devices['Desktop Safari'] }, dependencies: ['setup'] },  ],});

Line-by-Line Explanation

  • Parallel + CI retries + multiple workers = fast, resilient runs.
  • Four reporters: human (HTML), CI (JUnit), sharding (blob), and a custom Slack reporter for team visibility.
  • Failure-only artifacts keep storage sane while preserving evidence.
  • A setup project produces auth state; browser projects depend on it — login once, run everywhere.

Governance & standards

  • ESLint + Prettier + strict tsconfig enforced in CI.
  • PR review required; branch protection requires green tests.
  • Naming conventions and a documented docs/ onboarding guide.
  • A flakiness dashboard; “flaky” is a tracked bug, not accepted noise.

Common Mistakes

  • Scaling test count without scaling architecture → unmaintainable.
  • Accepting chronic flakiness (erodes trust in the whole suite).
  • No standards/linting → inconsistent, hard-to-review code.
  • Reporting only engineers can read (no stakeholder-friendly view).

Best Practices

  • Engineer for reliability first; a flaky enterprise suite is worthless.
  • Layered architecture + fixtures + components for maintainability.
  • Env safety (validation + PROD guards).
  • Sharded CI, failure-only artifacts, dashboards, and notifications.
  • Enforce standards via CI; document conventions.

Interview Questions

  • Q: What makes a framework enterprise-grade? A: Reliability, scalability, maintainability, configurability, CI/CD, observability, governance — engineered together.
  • Q: How do you scale to thousands of tests? A: Parallel + sharding, fast setup (API/storageState), strict isolation/unique data, layered architecture.
  • Q: How do you keep a big suite trustworthy? A: Near-zero flakiness, treat flaky as bugs, strong reporting/observability.
  • Q: How do you onboard new engineers? A: Documented conventions, fixtures that hide boilerplate, clear architecture.

Practice Exercise

Upgrade your framework toward enterprise-grade: add ESLint/Prettier, a setup auth project, sharded CI, failure-only artifacts, and a simple custom reporter (log a summary line). Document conventions in docs/.

Real-World Scenario

A platform team maintains a shared enterprise framework consumed by six product teams via an internal npm package of fixtures/components. Standardization means a locator or auth change ships once and every team benefits — and the nightly sharded run gates all releases.

Best Practices (Comprehensive)

Level: L4–L5. The consolidated rulebook, each with the why.

Locators

  • Prefer semantic/user-facing locators: getByRole > getByLabel > getByText > getByPlaceholder > getByTestId > CSS > XPath. Why: they mirror how users/assistive tech find elements and survive refactors.
  • Add data-testid for volatile UI. Why: stable hook independent of styling/text.
  • Avoid auto-generated ids/hashed classes and absolute XPath. Why: they change every build.

Assertions

  • Use web-first expect(locator).... Why: auto-retries eliminate timing races.
  • Avoid comparing await textContent() manually. Why: reads a single instant → flaky.
  • Assert outcomes, not implementation. Why: portable across browsers/refactors.

Waiting

  • Never waitForTimeout for sync. Why: too short = flaky, too long = slow.
  • Sync on real signals: auto-waiting, waitForResponse, waitForURL. Why: deterministic.

Test isolation

  • Each test independent; no shared order. Why: enables parallelism and reliable reruns.
  • Fresh context per test (default). Why: no state bleed.

Test data

  • Unique data per test (Faker/UUID). Why: prevents parallel collisions.
  • Seed/clean via API. Why: fast, reliable setup; tidy environments.
  • No secrets in data files. Why: security.

Authentication

  • Log in once → storageState → reuse. Why: huge speed win, less flakiness.
  • Per-role storageState. Why: clean role-based testing.

Structure & design

  • POM + components; assertions in tests. Why: maintainability, single source of truth.
  • Fixtures for DI. Why: remove boilerplate, decouple wiring.
  • Layered architecture. Why: scales to many tests/contributors.

Parallel & CI

  • Parallel workers + CI sharding. Why: fast feedback at scale.
  • Retries in CI only (1–2) with trace on retry. Why: absorb infra flakiness without hiding bugs.
  • npm ci + playwright install --with-deps. Why: reproducible, launch-ready runners.

Reporting & logging

  • HTML + JUnit; publish artifacts + traces. Why: human + machine + debuggability.
  • Leveled logging; capture console/pageerror on key pages. Why: surface silent frontend errors; never log secrets.

Error handling & maintainability

  • Fail fast with clear messages in shared code. Why: faster diagnosis.
  • Small, descriptive commits; PR review; linting. Why: quality and collaboration.
  • Meaningful test/method names by intent. Why: readability as documentation.

Scalability & security

  • Env-driven config, validated; PROD guardrails. Why: safe multi-env runs.
  • Secrets from CI stores; gitignore .env*/auth/. Why: prevent leaks.
  • Mock only what you must. Why: keep tests representative of reality.

The golden rules (memorize)

  1. Web-first assertions, always.
  2. No hard waits — sync on real signals.
  3. Semantic locators over brittle selectors.
  4. Independent tests + unique data.
  5. Log in once; reuse storageState.
  6. Page objects do; tests assert.
  7. Fixtures inject; utils support.
  8. Retries for infra, not bugs.
  9. Reports + traces on failure.
  10. Secrets never in the repo.

Interview Questions

  • Q: Top 3 practices for reliable tests? A: Web-first assertions, no hard waits, unique data + isolation.
  • Q: How do you keep tests maintainable? A: POM/components, fixtures, layered architecture, assertions in tests.
  • Q: How do you keep the suite fast? A: Parallel + sharding, API/storageState setup, mock non-essential calls.

Practice Exercise

Audit an existing spec against these rules. List every violation (hard wait, brittle locator, shared data, assertion in a page object, etc.) and fix them. Re-run to confirm stability.

Real-World Scenario

A team adopted this checklist as a PR review template. Within a month, flakiness dropped and new tests were consistent by default — the rulebook became the shared standard that kept a growing suite trustworthy.

FAQs

Which design patterns are used in Playwright frameworks?

Page Object Model (often with components), fixtures as dependency injection, factories and builders for test data, and strategy objects for environments or users.

Do you still need page objects with Playwright fixtures?

Usually yes: page objects hold locators and actions, and fixtures create and inject them, so tests stay short and pages stay reusable.