TechByteByByte

Part 31: Production Framework Architecture

Design a maintainable Playwright framework for real teams and long-lived products.

A framework gives tests, fixtures, page objects, data, configuration, and reports clear responsibilities.

test intent โ†’ supporting layers โ†’ browser โ†’ evidence

Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com

Every technique across this series so far has been genuinely correct in isolation. This part is about something different: how they actually fit together into one coherent, navigable, maintainable whole โ€” the difference between โ€œa folder full of test files that happen to workโ€ and โ€œa framework a team can confidently build on for years.โ€


The Full Folder Structure, and Why Each Layer Exists

automation-framework/
โ”‚
โ”œโ”€โ”€ tests/               # actual test files (.spec.ts) โ€” Part 6, Part 9
โ”œโ”€โ”€ pages/                # Page Object classes โ€” Part 20
โ”œโ”€โ”€ components/           # reusable component objects (header, cart badge) โ€” Part 20
โ”œโ”€โ”€ fixtures/              # custom fixtures โ€” Part 15, Part 29
โ”œโ”€โ”€ api/                  # API client helpers and endpoints โ€” Part 17
โ”œโ”€โ”€ database/              # database query helpers โ€” Part 22
โ”œโ”€โ”€ utils/                # generic helper functions not specific to any one page
โ”œโ”€โ”€ test-data/             # data factories, JSON fixtures โ€” Part 21, Part 30
โ”œโ”€โ”€ config/                # environment configs, constants
โ”œโ”€โ”€ reporters/             # custom reporters โ€” Part 34, Part 38
โ””โ”€โ”€ playwright.config.ts   # Part 6, Part 16

Walk through why each of these is separate, not just that it exists โ€” this reasoning is exactly what an interviewer is actually testing for when they ask you to โ€œdesign a framework,โ€ far more than whether you can recite the folder names correctly.

tests/

contains only test logic โ€” the actual test(...) blocks, with assertions expressing intent. It should read like a clear, human description of behavior being verified, with implementation detail pushed elsewhere.

pages/

and components/ hold everything about how to find and interact with the applicationโ€™s UI โ€” this is Part 20โ€™s separation of concerns made structural: if the UI changes, only these folders should need updating, never the tests themselves.

fixtures/

holds reusable setup and teardown, kept genuinely separate from page objects because a fixture is about test lifecycle (when something runs, and for which tests) while a page object is about UI interaction โ€” two related but genuinely distinct responsibilities.

api/

and database/ exist because Part 17 and Part 22 established that a mature framework touches more than just the UI layer โ€” keeping this logic in its own dedicated space, rather than scattered inline inside test files, means itโ€™s reusable and independently testable/maintainable.

test-data/

centralizes Part 21 and Part 30โ€™s data factories โ€” again, separated from both tests and page objects, because โ€œwhat data does this test useโ€ is a genuinely different concern from โ€œhow does this test interact with the UI.โ€

utils/

is deliberately the smallest, most disciplined folder, reserved for genuinely generic helpers not tied to any specific page or feature (a date-formatting helper, a retry-wrapper utility). Itโ€™s worth a direct, honest warning here: utils/ is precisely where undisciplined frameworks quietly accumulate an unmanageable dumping ground of loosely related, poorly organized code over time โ€” worth treating with real, active discipline, regularly asking โ€œdoes this genuinely belong here, or does it actually belong in a more specific, appropriately named folder instead?โ€

Analogy: The Architectโ€™s City Zoning Plan Imagine designing a modern city:

  • Unregulated City (Monolithic framework): People build heavy factories next to libraries, residential houses, and electrical grids all mixed together. If the library needs a water pipe repair, the workers block the main residential highway, cutting power to the factory (tests break because selector changes are tangled with mock data and database configurations).
  • Zoned City (Clean Architecture):
    • tests/ (Residential Zone): Where people actually live and tell stories (expressing test intent and assertions).
    • pages/ & components/ (Commercial Zone): Shops where transactions and interactions occur (locators, clicks, forms).
    • fixtures/ (Utility Grid): Providing standard electricity and clean water automatically (setup, context isolation, authentication files).
    • api/ & database/ (Industrial District): Heavy lifting behind the scenes (direct network payloads and ledger queries).
    • test-data/ (Resource Warehouses): Keeping data factories separate from residential streets.

๐Ÿ“Š Visual Flowchart: Framework Directory Architecture

Here is how components, page objects, fixtures, and API clients relate in a mature production-grade automation framework:

graph TD
    subgraph TestExecution ["Test Execution Layer"]
        Spec["tests/checkout.spec.ts"]
    end

subgraph LifecycleLayer ["Lifecycle Layer"]
        Fixture["fixtures/my-fixtures.ts<br>(Setup, Teardown)"]
    end

subgraph UIInteraction ["UI Interaction Layer"]
        Page["pages/InventoryPage.ts"] -->|Contains| Component["components/CartBadge.ts"]
    end

subgraph DataService ["Data and Service Layer"]
        Data["test-data/userFactory.ts"]
        API["api/ApiClient.ts"]
        DB["database/dbClient.ts"]
    end

Spec -.->|1. Declares parameter| Fixture
    Fixture -->|2. Instantiates| Page
    Fixture -->|3. Loads template| Data

Page -->|4. Verifies state against| API
    API -->|5. Compares with source| DB

Separation of Concerns, Stated as a Principle

Every folder above expresses the same underlying idea, worth being able to state directly and confidently in an interview: each piece of the framework should have exactly one clear reason to change. A test file should change only when the expected behavior being verified changes. A page object should change only when the applicationโ€™s UI changes. A fixture should change only when shared setup requirements change.

A test-data factory should change only when the shape of the data changes.

When these responsibilities are kept cleanly separated, a single real-world change โ€” say, a UI redesign โ€” touches exactly the files actually responsible for that concern (page objects), and nothing else needs to be touched at all, which is precisely the payoff Part 20 first demonstrated concretely with a single LoginPage class, now scaled to an entire framework.


Naming Conventions

Worth stating plainly, because consistency here genuinely compounds in value as a framework grows and more people work on it: file and class names should directly and unambiguously reflect what they represent โ€” LoginPage.ts contains a LoginPage class, checkout.spec.ts contains tests about checkout, userFactory.ts builds user test data. A newcomer to the codebase should be able to navigate directly to the right file based purely on a reasonable guess from its name, without needing to open several files first just to figure out where something actually lives.

Linting and Formatting for a Test Codebase

A test codebase is still a real codebase, and it benefits from the exact same code-quality tooling real application code does. ESLint enforces code-quality rules automatically โ€” and, genuinely relevant to Part 14โ€™s earlier warning, this is precisely the mechanism that can catch and block an accidentally committed test.only before it ever merges:

// .eslintrc.json (simplified example)
{
  "plugins": ["playwright"],
  "rules": {
    "playwright/no-focused-test": "error",
    "playwright/no-skipped-test": "warn",
    "playwright/no-wait-for-timeout": "warn"
  }
}

Notice playwright/no-wait-for-timeout โ€” an actual, real, available lint rule that directly enforces Part 12โ€™s core lesson, automatically, on every commit, rather than relying purely on individual code review vigilance to catch it every single time.

Prettier

handles formatting consistency โ€” indentation, quote style, line length โ€” automatically, removing an entire category of genuinely pointless debate and inconsistency from code review discussions, letting reviewers focus their attention on substance (is this the right locator strategy? is this test actually verifying the right thing?) rather than bikeshedding over formatting style.

npm install --save-dev eslint eslint-plugin-playwright prettier

Configuring both, and running them automatically as part of a CI pipeline (a direct preview of Part 32) before a pull request can even be merged, is a genuinely standard, valuable practice for any real, multi-contributor framework โ€” catching entire categories of the exact anti-patterns this series has flagged throughout (hard waits, test.only, inconsistent style) automatically, before a human reviewer even needs to notice them manually.


How It Works in a Real Test Run

A maintainable framework separates specifications, task or page abstractions, fixtures, data builders, API clients, configuration, and reporting utilities. Dependencies should point inward toward stable contracts rather than allowing every layer to import everything else.

Follow one failure through the architecture: spec states intent โ†’ fixture owns setup โ†’ page/component performs interaction โ†’ assertion proves outcome โ†’ reporter attaches evidence. If diagnosing a failed click requires opening six unrelated helper layers, the abstraction is too indirect.

Interview Questions

Q: What is the core underlying principle behind separating a framework into folders like pages/, fixtures/, and test-data/, rather than putting everything in one place?

Ans: The core principle is that each piece of the framework should have exactly one clear reason to change โ€” a page object changes only when the UI changes, a fixture changes only when shared setup requirements change, test data changes only when the shape of the data changes. Keeping these concerns cleanly separated means a single real-world change, like a UI redesign, only requires touching the files actually responsible for that specific concern, rather than needing to hunt through and modify scattered, intermixed code across the whole codebase.

Q: Why is api/ typically kept as a separate folder from pages/, even though both might be used together within the same test?

Ans: pages/ is specifically about UI interaction โ€” locators and actions performed through the browser. api/ is about direct HTTP interaction, entirely separate from any browser or UI at all, as established in Part 17. Even though a single test might use both together (recall Part 17โ€™s combined API-and-UI patterns), they represent genuinely distinct responsibilities and change for different, independent reasons, which justifies keeping them structurally separate.

Q: Why is the utils/ folder specifically called out as needing active discipline, more so than other folders?

Ans: Because itโ€™s the natural, easy dumping ground for anything that doesnโ€™t obviously belong elsewhere โ€” without active discipline, it tends to accumulate a large, poorly organized, loosely related pile of code over time, undermining the very navigability and clear separation of concerns the rest of the folder structure is designed to provide. Regularly questioning whether something newly added to utils/ might actually belong in a more specific, appropriately named location helps prevent this drift.

Q: How does an ESLint rule like playwright/no-wait-for-timeout connect back to Part 12โ€™s discussion of waiting?

Ans: Part 12 established that waitForTimeout is generally an anti-pattern, since itโ€™s a fixed, unreliable guess rather than a genuine wait for an actual condition. This specific lint rule enforces that guidance automatically, on every commit, flagging or blocking its use without relying purely on individual code reviewers to consistently catch and call it out manually every single time it appears.

Q: Why would a team invest in automated formatting (Prettier) for a test codebase specifically, beyond just general code cleanliness?

Ans: Automated formatting removes an entire category of pointless, low-value debate and inconsistency from code review โ€” reviewers no longer need to comment on indentation or quote style, and can instead focus their limited review attention on substance, like whether a locator strategy or an assertion is actually correct and meaningful. This matters especially for a growing team where consistency across many contributors would otherwise be genuinely difficult to maintain by convention alone.

Q: A newcomer joins the team and struggles to find where a specific piece of test-data-generation logic lives. What does this suggest about the frameworkโ€™s structure, and what would you check?

Ans: It suggests either a naming or organizational inconsistency โ€” the logic might be misplaced (perhaps sitting in utils/ when it genuinely belongs in test-data/), or its file/function naming doesnโ€™t clearly reflect its actual purpose. Iโ€™d check whether the folder structure and naming conventions were actually followed consistently as the framework grew, since this kind of difficulty is often an early, concrete symptom of structural discipline eroding over time as a codebase scales.


Exercises โ€” Part 31

Understand: Explain, in your own words, why a genuine UI redesign should ideally only require changes within the pages/ and components/ folders of a well-structured framework, and not within tests/ at all.

Simple Practice: Take several artifacts youโ€™ve built across this series โ€” a LoginPage class, a custom fixture, a Faker.js-based data factory, an API helper โ€” and organize them into the folder structure from this part, updating import paths accordingly.

Real-World Scenario: Set up ESLint with the eslint-plugin-playwright rules shown in this part, deliberately write a test containing test.only and a waitForTimeout, and run the linter to confirm it correctly flags both issues.

Challenge: Design, in writing, a folder structure and naming convention plan for a hypothetical framework testing three genuinely distinct application areas (say, a customer-facing site, an admin dashboard, and a public API) that all share some common page components and fixtures, while keeping each areaโ€™s specific tests, page objects, and data cleanly separated from the others.


Next: Part 32 โ€” CI/CD

โ€” from โ€œwhat does CI/CD even meanโ€ through a full, working GitHub Actions pipeline running your Playwright suite automatically on every push.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed