QA How-To
How to Build a Playwright TypeScript framework from scratch (2026)
Learn to build a Playwright TypeScript framework from scratch with fixtures, page objects, projects, CI, reporting, and reliable test design at scale.
22 min read | 3,008 words
TL;DR
Use Playwright Test with @playwright/test, focused domain objects, deterministic test data, and explicit lifecycle management. Prove one scenario and its failure evidence locally and in CI, then scale without introducing shared mutable state.
Key Takeaways
- Start Playwright Test with one complete vertical slice before adding framework layers.
- Keep @playwright/test lifecycle, automation code, domain abstractions, and assertions clearly separated.
- Design test data and cleanup for isolated parallel execution from the beginning.
- Prefer observable conditions and meaningful contracts over sleeps and broad retries.
- Publish focused failure evidence from CI while redacting secrets and sensitive data.
- Add abstractions only when they improve domain readability or remove proven duplication.
Build a Playwright TypeScript framework from scratch means creating a small, reliable test system whose responsibilities are obvious to every contributor. This guide gives you a production-minded path using Playwright Test, TypeScript, and @playwright/test, with runnable examples and decisions that remain sound as the suite grows.
The goal is not the largest framework. The goal is fast feedback, trustworthy failures, safe parallel execution, and code that a new SDET can change without decoding hidden behavior. You will build the foundation, organize automation by intent, add diagnostics, and prepare the suite for continuous integration.
TL;DR
| Concern | Recommended choice | Why |
|---|---|---|
| Browser lifecycle | Built-in page fixture | Isolation and automatic cleanup |
| Selectors | Role, label, text, test id | User-facing and resilient |
| Retries | CI only | Exposes local flakiness |
| Artifacts | Trace on first retry | Useful evidence at manageable cost |
Start with one end-to-end vertical slice. Keep lifecycle in @playwright/test, automation in Playwright Test, behavior in focused domain objects, and expectations in tests. Run npx playwright test locally and make the same command the center of CI.
1. Build a Playwright TypeScript framework from scratch: Define the Architecture Before Writing Tests
Treat Playwright Test as the runner, assertion library, browser manager, fixture engine, and reporter. Add layers only when they remove real duplication. Keep specifications readable, page objects focused on interaction, fixtures focused on lifecycle, and utilities free of test-runner state. This boundary prevents a helper library from becoming an invisible second framework.
A good first milestone is one vertical slice: open the app, perform one business action, assert one outcome, and publish its trace in CI. That slice proves configuration, imports, environment handling, diagnostics, and reporting before the suite grows.
The review question for this stage is simple: can another engineer explain where this responsibility lives and why? If the answer depends on tribal knowledge, add a small naming improvement or a focused README note. Do not compensate with a large framework manual that becomes stale. Executable examples and conventional locations are better documentation.
2. Bootstrap TypeScript and Playwright Correctly
Use the official initializer, commit the lockfile, and keep the generated configuration understandable. Strict TypeScript settings catch incorrect fixture types and nullable configuration early. Use environment variables for deployment-specific values, but validate required values at startup instead of allowing an undefined URL to fail halfway through a test.
Do not add Cucumber, a dependency injection container, or a custom runner during bootstrap. Playwright already provides projects, tags, annotations, hooks, fixtures, and parallel workers. Extra orchestration should have a demonstrated business need.
Keep the feedback loop short. Run the narrowest relevant test while developing, then the complete suite before merging. A framework is successful when failures point directly to an application rule, environment dependency, or automation defect. It is not successful merely because it has many layers.
Installation
npm init playwright@latest
npm install -D dotenv
3. Design a Scalable Folder Structure
Organize by responsibility and business capability. Tests describe behavior. Pages expose meaningful actions and observable UI. Fixtures compose dependencies. Test data builders create valid domain objects. Small support modules parse environment settings or generate identifiers.
Avoid a universal BasePage filled with click, wait, and type wrappers. Such wrappers hide Playwright diagnostics and encourage weak selectors. Composition keeps APIs smaller and lets each page own only the behavior it understands.
Treat configuration as input with a schema. Identify required values, defaults, allowed choices, and sensitive fields. Print safe effective configuration at run start, but redact secrets. This turns many CI mysteries into immediate, actionable messages.
Recommended project tree
playwright.config.ts
package.json
tsconfig.json
tests/
auth.setup.ts
checkout.spec.ts
src/
pages/
LoginPage.ts
fixtures/
test.ts
data/
users.ts
support/
env.ts
4. Configure Projects, Timeouts, and Artifacts
Centralize defaults in playwright.config.ts and override at the smallest useful scope. A test timeout is a budget for the complete test, while assertion timeouts govern polling assertions. Navigation and action timeouts can usually remain at their defaults because Playwright auto-waits for actionability.
Projects are configuration profiles, not merely browser names. Use them for desktop browsers, mobile emulation, authenticated states, locales, or deployment smoke suites. Keep the default matrix small so pull requests remain fast, then schedule the wider matrix.
Design for parallel execution even if the first suite is serial. Avoid global mutable state, fixed record names, shared downloads, and order dependence. Isolation is cheaper to establish with ten tests than to retrofit after one thousand.
Core configuration
import { defineConfig, devices } from '@playwright/test';
import 'dotenv/config';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [['html', { open: 'never' }], ['junit', { outputFile: 'test-results/junit.xml' }]],
use: {
baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev/todomvc',
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'] } }
]
});
5. Build Page Objects and Component Objects
Model intent with methods such as addItem or submitOrder, not mechanical wrappers such as clickButton. Expose locators when the test needs to make a business assertion, because assertions belong in the test unless the assertion is an invariant of the component.
Prefer getByRole and getByLabel because they align tests with accessible behavior. Use getByTestId for stable application contracts when user-facing locators are ambiguous. Never freeze an ElementHandle in a constructor. Locator objects are lazy and retry-aware.
Use failure evidence to guide abstraction. Repeated business setup may deserve a builder or client. Repeated low-level calls do not automatically deserve a wrapper, especially when wrapping removes useful library diagnostics or blocks new APIs.
Runnable design example
import { expect, type Locator, type Page } from '@playwright/test';
export class TodoPage {
readonly input: Locator;
readonly items: Locator;
constructor(private readonly page: Page) {
this.input = page.getByPlaceholder('What needs to be done?');
this.items = page.getByTestId('todo-item');
}
async open() { await this.page.goto('/'); }
async add(text: string) { await this.input.fill(text); await this.input.press('Enter'); }
}
// tests/todo.spec.ts
import { test } from '@playwright/test';
import { TodoPage } from '../src/pages/TodoPage';
test('adds a todo', async ({ page }) => {
const todo = new TodoPage(page);
await todo.open();
await todo.add('review release');
await expect(todo.items).toContainText(['review release']);
});
6. Use Typed Fixtures for Dependency Injection
A fixture declares how a dependency is created, supplied, and disposed. Test-scoped fixtures are ideal for pages and per-test data. Worker-scoped fixtures fit expensive services that are safe to share. Dependencies between fixtures should remain explicit in the fixture signature.
Keep authentication setup separate when storage state can be reused safely. Never share a mutable browser page across parallel tests. If a fixture creates server data, pair it with cleanup or create uniquely named records so retries and parallel workers cannot collide.
Code review should check readability at the test call site, deterministic cleanup, assertion quality, and the failure message. These qualities matter more than maximizing reuse. A few repeated lines can be safer than one configurable helper with many boolean switches.
Lifecycle or transformation example
import { test as base } from '@playwright/test';
import { TodoPage } from '../pages/TodoPage';
type Fixtures = { todoPage: TodoPage };
export const test = base.extend<Fixtures>({
todoPage: async ({ page }, use) => {
await use(new TodoPage(page));
}
});
export { expect } from '@playwright/test';
7. Create Reliable Tests and Test Data
Make every test independently runnable. Create prerequisites through APIs when UI setup is not the behavior under test. Generate unique business identifiers, freeze time only when the application supports it, and assert externally visible outcomes.
Web-first assertions such as toBeVisible and toHaveText poll until their condition is satisfied. Replacing them with sleeps makes the suite slower and less reliable. When an event has no visible representation, wait for a specific response, URL, download, or application signal.
Maintain a small smoke subset that proves the deployment is testable, then separate broader regression by capability. Tags and markers describe purpose or constraints, not ownership politics. Delete quarantined tests that no longer protect a meaningful risk.
8. Run the Framework in CI and Scale It
CI should install from the lockfile, install required browser binaries, run tests, and always upload reports and failure artifacts. Shard only after tests are isolated. Merge blob reports when a matrix or shards produce separate result sets.
Track flaky tests as defects with owners. Retries are a diagnostic safety net, not a pass criterion. Review trace timelines, network calls, console messages, and screenshots to identify whether the failure belongs to the test, product, environment, or data.
Record decisions that affect every contributor, such as naming, lifecycle scope, supported environments, and artifact retention. Leave local implementation details close to code. This balance keeps onboarding practical without creating a second source of truth.
9. How to build a Playwright TypeScript framework from scratch
A practical implementation sequence keeps risk visible. First, commit the smallest dependency and configuration set. Second, automate one representative happy path through the intended public abstractions. Third, force that test to fail and confirm the report tells you why. Fourth, add one negative or boundary case and prove that data cleanup works. Fifth, run two tests concurrently and look for shared state. Finally, place the exact local command in CI.
Do not postpone diagnostics until the suite is large. A framework without useful failure evidence makes every product defect expensive to investigate. Likewise, do not enable maximum parallelism before isolation is measured. A serial test that accidentally depends on another test may appear healthy for months. The first parallel run will expose the dependency, but fixing the design early is much cheaper.
Use pull request review as part of framework governance. A reviewer should be able to identify the scenario, setup, action, expectation, and cleanup without jumping through many files. Shared abstractions should have one reason to change and should not accept unrelated flags. When a contributor needs a special case, decide whether it represents a real domain concept or a one-off test detail.
This is also the stage to connect related skills. Review Playwright locator strategy, Playwright fixtures explained, Playwright CI best practices for deeper treatment of selectors, contracts, execution, and troubleshooting that complement this build.
10. Verify the Framework Before Expanding It
Verification should cover more than a green test. Run from a clean checkout, use a fresh dependency cache when feasible, and confirm the documented command works. Deliberately break a locator, endpoint, assertion, or table value. The resulting message should identify the failed operation and preserve enough context to reproduce it. Then interrupt setup or throw from the test and confirm teardown still releases resources.
Run the suite twice to detect leaked state. Reverse test order or use a random order plugin only as a diagnostic, because tests should not need a particular order. Execute with at least two workers if the runner supports it. Check that filenames, ports, accounts, and generated identifiers remain independent. If a test targets a shared environment, make ownership and cleanup rules explicit.
Finally, inspect the published CI experience as a maintainer would. The job name should show the relevant environment and test slice. A failed job should retain its report. Secrets should not appear in console output or artifacts. The suite should return a nonzero exit code for real failures and should not silently convert skipped setup into success. These checks turn a code sample into an operational framework.
11. Production Readiness Checklist
Use this checklist as a release gate for the framework itself. A checked item means the behavior was demonstrated, not merely configured. Save the command and evidence in the pull request so another engineer can repeat the check.
Installation and configuration
- A clean checkout installs with the declared npm workflow and does not depend on packages, plugins, browser drivers, or environment files that exist only on the author's machine. Dependency versions are constrained, the relevant manifest is committed, and the supported runtime version is documented.
- The framework validates its effective environment before test execution. Required URLs, browser choices, tokens, and paths produce direct messages when absent or invalid. Safe defaults are limited to local non-sensitive settings. No secret is committed, printed, placed in a screenshot, or copied into a report.
Lifecycle and isolation
- Every test can run by itself, first, last, or beside another test. Setup creates the state the scenario needs, while teardown releases sessions and owned records even after an assertion or setup-adjacent operation fails. Rerunning the suite does not encounter leftovers from the prior run.
- Lifecycle scope matches mutability. A dependency shared across tests is either immutable or deliberately synchronized. Browser sessions, scenario state, request-specific headers, and mutable domain records are not stored in process-wide global variables. Generated files and identifiers include enough uniqueness to avoid worker collisions.
Test design and review
- Test names describe a rule and expected outcome, not a sequence of clicks or calls. The arrange, act, and assert phases are visible even when helper methods are used. Assertions prove the business result and include values that explain a mismatch. A test does not pass merely because Playwright Test raised no exception.
- Abstractions expose domain intent and have cohesive responsibilities. There is no catch-all utility class, hidden retry loop, or base class that every test must inherit only to access unrelated helpers. A contributor can still use the underlying library documentation because wrappers preserve standard concepts.
Reliability and diagnostics
- Synchronization waits for an observable condition and has a bounded timeout. Fixed sleeps are absent from normal test paths. Negative cases distinguish an expected rejection from an infrastructure failure. Any retry policy is visible in runner configuration and accompanied by evidence that allows the first failure to be studied.
- A deliberately broken test produces actionable evidence. The report identifies the scenario and failed expectation. Relevant artifacts survive a failed CI command and use collision-free names. Sensitive headers, cookies, credentials, personal data, and confidential payload fields are redacted before artifacts leave the runner.
Continuous integration and ownership
- The main CI command is the same one documented for local use:
npx playwright test --reporter=blob. CI installs declared dependencies, supplies only approved secrets, returns the correct exit code, and publishes machine-readable results plus focused diagnostics. Pull requests run a fast risk-based subset, while broader coverage has a clear scheduled or release trigger. - Every recurring failure has an owner and classification. The team distinguishes product defects, automation defects, environment incidents, test-data conflicts, and intentional changes. Quarantine is time-bound and visible. Historical pass rate does not excuse a test that no longer protects a meaningful requirement.
Change safety
Before declaring the foundation ready, make one small framework change, such as adding an environment, browser, endpoint version, or new table shape. Observe how many files must change and whether existing tests remain untouched. Cross-cutting edits are sometimes legitimate, but routine extension should follow an obvious path. If adding one scenario requires editing a central switch statement and several base classes, revisit the boundaries now.
Also perform a dependency-update rehearsal. Read release notes for Playwright Test and @playwright/test, update on a branch, run the clean-install path, and inspect warnings as well as failures. The framework should make upgrades boring: direct APIs, small adapters at genuine boundaries, and no reliance on undocumented internals. Record only compatibility decisions that future contributors need.
Production readiness does not mean feature completeness. It means the framework has a trustworthy path for installing, executing, failing, diagnosing, cleaning up, and changing. New capabilities can then be added in response to product risk without weakening those properties.
Interview Questions and Answers
Q: What layers belong in a Playwright Test framework?
I keep the runner responsible for lifecycle and results, Playwright Test responsible for automation, domain clients or page objects responsible for business operations, and tests responsible for expectations. Configuration and data builders are focused supporting layers. I add a layer only when it creates a clear boundary.
Q: How do you prevent flaky tests?
I isolate data and state, wait for observable conditions, use stable selectors or contracts, and collect evidence on failure. I do not use arbitrary sleeps or automatic reruns as the primary fix. Retries can help diagnose nondeterminism, but the flaky test remains a defect.
Q: What belongs in a page object or API client?
It should expose cohesive business operations and hide local interaction mechanics. Tests should still own scenario-specific assertions. I avoid generic wrappers that merely rename library methods because they reduce diagnostic value without adding domain meaning.
Q: How do you support parallel execution?
Every test receives independent lifecycle state and unique data. I remove static mutable objects, fixed filenames, shared accounts, and order assumptions. Then I increase workers gradually while monitoring environment limits and artifact collisions.
Q: How should secrets and environment settings be handled?
I inject them through environment variables or an approved secret store and validate them at startup. Defaults are safe only for non-sensitive local values. Logs and reports redact tokens, passwords, cookies, and sensitive payload fields.
Q: What evidence should CI retain?
I retain the runner report, machine-readable results, and focused failure diagnostics. UI suites need screenshots and browser-level evidence, while API suites need sanitized request and response details. Artifact collection must run even when the test command fails.
Q: When is framework abstraction excessive?
It is excessive when engineers must trace several wrappers to understand one action, or when options and boolean flags replace clear use cases. I prefer composition, small domain APIs, and direct library calls where they remain readable.
Q: How do you introduce a new framework to a team?
I deliver one vertical slice, document the few global decisions, and pair on the first contributions. Pull request checks enforce formatting and test execution. I expand capabilities in response to real suite needs instead of predicting every future requirement.
A strong interview answer explains the decision, the tradeoff, and a concrete failure mode it prevents. Adapt these models to projects you have actually worked on rather than reciting terminology.
Common Mistakes
- Building wrappers around every Playwright Test call before repeated needs exist. This hides familiar APIs and produces longer stack traces.
- Sharing mutable lifecycle objects or test data across cases. The suite becomes order-dependent and unsafe under parallel execution.
- Putting scenario-specific assertions inside generic pages, clients, fixtures, or step definitions. This makes negative testing and reuse difficult.
- Using sleeps, broad retries, or catch-all exception handling to make failures disappear. These techniques delay feedback and conceal the actual synchronization or contract problem.
- Logging secrets or personal data in reports. Diagnostic value never removes the need for redaction and controlled artifact retention.
- Treating a successful local run as complete verification. A clean CI environment often reveals undeclared dependencies, path assumptions, missing binaries, and timezone differences.
- Growing one utility class into a second framework. Split by cohesive responsibility and keep public APIs small.
- Keeping obsolete tests because they once found a bug. Every test should protect a current risk and have an owner when it fails.
Conclusion
To build a Playwright TypeScript framework from scratch, begin with clear boundaries and one trustworthy vertical slice. Use @playwright/test for execution, Playwright Test for automation, focused domain objects for readable operations, and deterministic data plus cleanup for isolation. Add evidence and CI before adding scale.
Your next step is concrete: create the project, implement the first representative scenario, make it fail on purpose, and inspect the evidence. Once that experience is fast and clear, expand capability by capability while protecting the same design rules.
Interview Questions and Answers
What layers belong in a Playwright Test framework?
I keep the runner responsible for lifecycle and results, Playwright Test responsible for automation, domain clients or page objects responsible for business operations, and tests responsible for expectations. Configuration and data builders are focused supporting layers. I add a layer only when it creates a clear boundary.
How do you prevent flaky tests?
I isolate data and state, wait for observable conditions, use stable selectors or contracts, and collect evidence on failure. I do not use arbitrary sleeps or automatic reruns as the primary fix. Retries can help diagnose nondeterminism, but the flaky test remains a defect.
What belongs in a page object or API client?
It should expose cohesive business operations and hide local interaction mechanics. Tests should still own scenario-specific assertions. I avoid generic wrappers that merely rename library methods because they reduce diagnostic value without adding domain meaning.
How do you support parallel execution?
Every test receives independent lifecycle state and unique data. I remove static mutable objects, fixed filenames, shared accounts, and order assumptions. Then I increase workers gradually while monitoring environment limits and artifact collisions.
How should secrets and environment settings be handled?
I inject them through environment variables or an approved secret store and validate them at startup. Defaults are safe only for non-sensitive local values. Logs and reports redact tokens, passwords, cookies, and sensitive payload fields.
What evidence should CI retain?
I retain the runner report, machine-readable results, and focused failure diagnostics. UI suites need screenshots and browser-level evidence, while API suites need sanitized request and response details. Artifact collection must run even when the test command fails.
When is framework abstraction excessive?
It is excessive when engineers must trace several wrappers to understand one action, or when options and boolean flags replace clear use cases. I prefer composition, small domain APIs, and direct library calls where they remain readable.
How do you introduce a new framework to a team?
I deliver one vertical slice, document the few global decisions, and pair on the first contributions. Pull request checks enforce formatting and test execution. I expand capabilities in response to real suite needs instead of predicting every future requirement.
Frequently Asked Questions
How long does it take to build a Playwright TypeScript framework from scratch?
A useful first vertical slice can be built in a focused session, but a production-ready framework evolves with real risks. Prioritize lifecycle, one representative scenario, cleanup, diagnostics, and CI before adding integrations.
Which runner should I use with Playwright Test?
This guide uses @playwright/test because it provides the lifecycle and reporting model shown here. The best runner is one the team understands and can execute consistently in local and CI environments.
Should every test use a page object or client?
Use an abstraction when it expresses stable domain behavior or removes meaningful duplication. A direct library call is acceptable when it is clearer and local to one scenario.
How many tests should run in parallel?
Start with one worker, prove test and data isolation, then increase gradually within environment capacity. There is no universal worker count because browsers, APIs, accounts, and CI machines have different limits.
Should failed tests be retried automatically?
Retries can collect evidence for nondeterministic failures, especially in CI. A test that passes only on retry is still flaky and should be investigated, owned, and fixed.
What should be stored in source control?
Store test code, safe configuration defaults, dependency manifests, and documentation. Never store credentials, access tokens, private keys, or sensitive production data.
How do I know the framework is maintainable?
A new contributor can locate a scenario, understand its setup and assertion, run it with one documented command, and diagnose a deliberate failure. Changes remain local rather than requiring edits across unrelated layers.
Related Guides
- How to Build a Cypress framework from scratch (2026)
- How to Build a REST Assured API framework from scratch (2026)
- How to Build a Selenium Java framework from scratch (2026)
- How to Build a Selenium Python framework from scratch (2026)
- How to Build a BDD framework with Cucumber and Playwright (2026)
- How to Add CI to a test framework (2026)