A browser test can be easy to write and difficult to maintain. Consider a test that signs in, creates a workspace, changes its name, and verifies the result. In isolation, the implementation looks straightforward. But what happens when the same test runs across three browsers, four CI machines, and several retry attempts? Who owns the workspace? Can another test change the same account? Does a failed setup leave data behind? Can someone diagnose the failure without rerunning the entire suite? These are questions of Playwright test architecture, not simply questions about selectors. Getting this structure right is what separates a suite that scales from one that collapses under its own maintenance burden. Teams that need expert support in building this foundation can rely on Codoid’s QA automation services to design and implement a maintainable framework.
Those questions are architectural, not simply questions about selectors.
A useful design principle is:
Tests describe behavior. Page objects describe UI interactions. Playwright fixtures own resource lifecycles. Configuration and CI control execution.
This article develops that separation into a practical TypeScript architecture, including test-data ownership, authentication boundaries, parallel execution, and a sharded CI pipeline. Throughout this guide, we will explore how Playwright fixtures solve the problem of resource ownership in a way that keeps tests readable, isolated, and reliable.
Related Blogs
- Organize Around Responsibilities
- Keep Page Objects Focused on the UI
- Let Playwright Fixtures Own Setup and Cleanup
- Keep Specifications Small but Explicit
- Treat Authentication and Backend Isolation Separately
- Design for Parallel Execution
- Make Configuration an Explicit Execution Policy
- Build CI Around Reproducibility
- Keep the Architecture Maintainable
1. Organize Around Responsibilities, Not Just Folders
Start with a small structure whose boundaries are easy to explain:
.
├── e2e/
│ ├── api/
│ │ └── workspaces-api.ts
│ ├── components/
│ ├── fixtures/
│ │ └── test.ts
│ ├── pages/
│ │ └── workspace-page.ts
│ └── specs/
│ └── workspaces/
│ └── rename-workspace.spec.ts
├── playwright.config.ts
├── tsconfig.json
├── package.json
└── .github/
└── workflows/
└── e2e.yml
The directory names matter less than the responsibilities behind them:
| S. No | Layer | Owns | Should not own |
|---|---|---|---|
| 1 | Specifications | Scenarios and business assertions | Selectors scattered across tests |
| 2 | Page and component objects | UI interactions and UI-specific synchronization | Account provisioning or database cleanup |
| 3 | API clients | Application requests and response handling | Test-runner lifecycle decisions |
| 4 | Fixtures | Resource creation, dependency wiring, and cleanup | Entire business scenarios |
| 5 | Configuration and CI | Browsers, scheduling, environments, and artifacts | Hidden changes to what a test verifies |
Playwright’s page-object model provides an application-facing interface over browser interactions, while Playwright fixtures provide reusable setup and teardown. The architecture should use those capabilities rather than build another framework around them.
For application specifications, establish one import convention:
import { test, expect } from '../../fixtures/test';
The fixture module becomes the entry point for your suite’s configured test object. Supporting modules can still import Playwright types directly.
As the suite grows, organize specifications by product capability, such as billing, workspaces, or permissions, not by arbitrary categories such as “positive tests” and “negative tests.” For a larger codebase, moving feature-specific page objects and API clients alongside their specifications may improve ownership. Do not introduce that complexity before it solves a real navigation problem.
2. Keep Page Objects Focused on the UI
A useful page object expresses application operations:
await workspacePage.rename('Release planning');
An unhelpful abstraction merely renames Playwright:
await basePage.clickElement('saveButton');
The first communicates intent. The second adds indirection without explaining the application.
A Small Page Object
// e2e/pages/workspace-page.ts
import {
expect,
type Locator,
type Page,
} from '@playwright/test';
export class WorkspacePage {
readonly heading: Locator;
private readonly nameInput: Locator;
private readonly saveButton: Locator;
private readonly saveStatus: Locator;
constructor(private readonly page: Page) {
this.heading = page.getByRole('heading', { level: 1 });
this.nameInput = page.getByLabel('Workspace name', {
exact: true,
});
this.saveButton = page.getByRole('button', {
name: 'Save changes',
exact: true,
});
this.saveStatus = page.getByRole('status');
}
async open(workspaceId: string): Promise<void> {
const id = encodeURIComponent(workspaceId);
await this.page.goto(`/workspaces/${id}/settings`);
}
async rename(name: string): Promise<void> {
await this.nameInput.fill(name);
await this.saveButton.click();
// Application contract: "Saved" means persistence completed.
await expect(this.saveStatus).toHaveText('Saved');
}
}
Prefer locators based on roles, labels, and explicit test IDs over selectors coupled to incidental DOM structure. Playwright locators resolve the matching element when used, which helps them work with interfaces that rerender.
Put Assertions Where They Explain the Right Contract
“Never put assertions in page objects” is too rigid.
The Saved assertion above defines when rename() has completed successfully. The specification should still own the business claim: the new name appears and survives a reload.
This gives each assertion a clear purpose. The page object checks an interaction’s completion condition; the test checks the behavior being evaluated.
Use Playwright’s retrying assertions for observable UI conditions. An assertion such as await expect(locator).toHaveText(...) waits for the expected state, whereas asserting against an immediately retrieved value does not provide the same retry behavior.
Prefer Composition to a Universal Base Page
When several pages share a navigation bar, date picker, or confirmation dialog, extract a component object rooted at that component’s locator.
A WorkspacePage can contain a NavigationBar and a DeleteDialog. It does not need to inherit from a BasePage that eventually accumulates every interaction in the application.
Keep constructors free of navigation, account creation, and other asynchronous side effects. Constructing an object should not secretly change the test environment.
For more on this topic, see Codoid’s Playwright vs Selenium comparison.
3. Let Playwright Fixtures Own Setup and Cleanup
Playwright fixtures are more than reusable beforeEach hooks. They declare dependencies and establish resource lifetimes.
Playwright initializes non-automatic fixtures when needed. A dependency is initialized before its consumer and torn down afterward. Test-scoped fixtures are recreated for each test execution; worker-scoped fixtures live for the worker process. A worker-scoped fixture cannot depend on a test-scoped fixture.
A practical scope policy is:
| S. No | Resource | Recommended scope |
|---|---|---|
| 1 | Built-in browser | Worker, as managed by Playwright |
| 2 | Browser context and page | Test |
| 3 | Page/component object bound to a page | Test |
| 4 | Mutable workspace, order, or document | Test |
| 5 | Immutable worker metadata | Worker |
| 6 | Reusable account or expensive service | Worker only when its sharing rules are explicit |
The built-in page, context, and request fixtures provide test-isolated resources, while the browser is shared to avoid unnecessary startup work.
Separate Resource Operations from Resource Ownership
First, define a small API client. This example assumes an idempotent test-data endpoint that accepts a client-generated workspace ID and returns success only after the resource is ready.
// e2e/api/workspaces-api.ts
import type { APIRequestContext } from '@playwright/test';
export type Workspace = {
id: string;
name: string;
};
export class WorkspacesApi {
constructor(private readonly request: APIRequestContext) {}
async seed(
workspace: Workspace,
namespace: string,
): Promise<void> {
const id = encodeURIComponent(workspace.id);
const response = await this.request.put(
`/__e2e__/workspaces/${id}`,
{
data: {
name: workspace.name,
namespace,
},
},
);
if (!response.ok()) {
throw new Error(
`Seed workspace ${workspace.id}: HTTP ${response.status()}`,
);
}
}
async remove(workspaceId: string): Promise<void> {
const id = encodeURIComponent(workspaceId);
const response = await this.request.delete(
`/__e2e__/workspaces/${id}`,
);
// Repeated cleanup is allowed; unexpected failures are not.
if (!response.ok() && response.status() !== 404) {
throw new Error(
`Delete workspace ${workspaceId}: HTTP ${response.status()}`,
);
}
}
}
Playwright’s API testing support is useful for establishing preconditions and checking backend outcomes without navigating through unrelated UI flows. Keep the behavior under test in the browser: a workspace-renaming test can seed a workspace through an API, but should perform the rename through the UI.
Test-data endpoints should exist only in appropriately isolated test environments. For a remotely accessible environment, protect them with narrowly scoped authorization rather than exposing administrative functionality publicly.
Related Blogs
Compose the Fixtures
// e2e/fixtures/test.ts
import { randomUUID } from 'node:crypto';
import { test as base } from '@playwright/test';
import {
WorkspacesApi,
type Workspace,
} from '../api/workspaces-api';
import { WorkspacePage } from '../pages/workspace-page';
type TestFixtures = {
workspacesApi: WorkspacesApi;
workspace: Workspace;
workspacePage: WorkspacePage;
};
type WorkerFixtures = {
workerNamespace: string;
};
export const test = base.extend<
TestFixtures,
WorkerFixtures
>({
workerNamespace: [
async ({}, use, workerInfo) => {
const runId =
process.env.E2E_RUN_ID ?? `local-${randomUUID()}`;
const shard = workerInfo.config.shard?.current ?? 1;
const namespace = [
runId,
workerInfo.project.name,
`shard-${shard}`,
`slot-${workerInfo.parallelIndex}`,
].join(':');
await use(namespace);
},
{ scope: 'worker' },
],
workspacesApi: async ({ request }, use) => {
await use(new WorkspacesApi(request));
},
workspace: async (
{ workspacesApi, workerNamespace },
use,
testInfo,
) => {
const workspace: Workspace = {
id: randomUUID(),
name: 'Draft workspace',
};
await testInfo.attach('workspace-metadata', {
body: JSON.stringify({
workspaceId: workspace.id,
namespace: workerNamespace,
retry: testInfo.retry,
}),
contentType: 'application/json',
});
try {
await workspacesApi.seed(workspace, workerNamespace);
await use(workspace);
} finally {
await workspacesApi.remove(workspace.id);
}
},
workspacePage: async ({ page }, use) => {
await use(new WorkspacePage(page));
},
});
export { expect } from '@playwright/test';
The worker namespace records ownership, not test identity. parallelIndex identifies a worker slot and remains stable when that slot’s process is restarted; workerIndex identifies the process and changes on restart. Neither should be treated as globally unique across independent CI jobs.
Each workspace receives its own identifier. Consequently, multiple tests can use the same friendly workspace name without selecting or deleting each other’s records, provided the application operations remain scoped to the workspace ID.
The metadata attachment connects a failure to its backend resource without attaching credentials or the entire application response. Playwright exposes attachment APIs through TestInfo.
Cleanup Needs Both a Normal Path and a Recovery Path
Allocating the ID before seeding allows the fixture to attempt cleanup even when the seed request fails after the server has created the resource.
However, finally is not a distributed cleanup guarantee. A killed process, canceled machine, or delayed backend operation can still leave resources behind. For shared test environments, add an expiration policy or a cleanup service that removes old resources by ownership namespace.
Also keep cleanup failures visible. For fixtures that allocate several resources, release them in reverse dependency order and preserve the original failure when reporting additional cleanup errors.
A resource needs an owner, an identifier, and a cleanup policy, not merely a creation helper.
For more on API testing with Playwright, see Codoid’s Playwright API testing guide.
4. Keep Specifications Small but Explicit
With the supporting layers in place, the specification can focus on the behavior:
// e2e/specs/workspaces/rename-workspace.spec.ts
import { test, expect } from '../../fixtures/test';
test('persists a renamed workspace', async ({
page,
workspace,
workspacePage,
}) => {
await workspacePage.open(workspace.id);
await expect(workspacePage.heading).toHaveText(workspace.name);
await workspacePage.rename('Release planning');
await expect(workspacePage.heading).toHaveText('Release planning');
await page.reload();
await expect(workspacePage.heading).toHaveText('Release planning');
});
The test states its precondition, action, and persistence check. It does not need to know how the workspace was created or how it will be removed.
Notice that the page-object fixture does not automatically navigate. Navigation remains explicit because it helps the reader understand the scenario. Playwright fixtures should eliminate lifecycle repetition without hiding meaningful test steps.
Avoid turning the entire scenario into something like:
await workspaceFlows.verifyRenameWorks();
That may reduce the line count, but it also removes the test’s explanation of what “works” means.
5. Treat Authentication and Backend Isolation Separately
A new browser context isolates browser state. It does not create a new database, workspace, shopping cart, or account.
Playwright supports loading saved authentication state
into fresh contexts. Its authentication guidance distinguishes shared accounts for non-interfering tests from separate worker accounts for tests that change shared server-side state.
Choose the boundary according to what the tests mutate:
| S. No | Strategy | Appropriate use |
|---|---|---|
| 1 | Shared saved authentication state | Tests can safely use the same account concurrently |
| 2 | Account per worker | Account reuse is safe within a worker and mutable state is reset or independently scoped |
| 3 | Account or tenant per test | Tests change permissions, credentials, account settings, or other destructive state |
For worker authentication, create a worker-scoped account lease and authentication-state fixture, then have the test-scoped storageState fixture provide that state to each fresh context. Do not share a live Page merely to avoid logging in again.
A worker account prevents concurrent workers from changing the same account, but it does not reset changes between successive tests using that account. Reset those changes or allocate more narrowly.
Keep saved authentication state out of source control and ordinary report uploads; it can contain credentials sufficient to impersonate a test account.
There is another important distinction: the built-in request fixture is separate from the browser context, while page.request and context.request share that browser context’s cookie storage. Logging in through an independent API context does not automatically authenticate an already-created browser context. Transfer authentication state deliberately.
Use Setup Projects for Visible Prerequisites
For reusable prerequisites such as generating shared read-only authentication state, a setup project with project dependencies can make setup visible in reports and traces.
Do not interpret a setup project as “exactly once across the entire CI system.” Shard filtering selects primary tests, and their project dependencies also run. Independent shard invocations therefore need setup that tolerates repetition.
Database migrations against a shared environment, for example, usually belong in a coordinated deployment stage rather than an uncoordinated setup task on every shard.
6. Design for Parallel Execution Before Increasing Concurrency
Parallel execution is easier to introduce when tests already own their state.
Playwright runs test files in parallel by default, while tests within a file normally run in order. fullyParallel: true allows tests within files to run in parallel too. Workers are separate processes, and a failure causes the affected worker to be replaced.
Three controls are often confused:
- Workers control concurrent execution within one Playwright invocation.
- Shards split selected tests across separate invocations, typically on separate CI machines.
- Projects define configurations such as Chromium, Firefox, WebKit, devices, or environments. Multiple projects in one invocation do not each receive an additional independent allocation of the global worker limit.
For example, four simultaneously running shard jobs with two workers each provide an upper bound of approximately eight active test workers. Adding a separate browser dimension to the CI matrix creates more jobs and changes that calculation.
This is a capacity estimate, not a promise of proportional speedup. Startup costs, long tests, database contention, and runner limits still matter.
Shard Balance Depends on Test Structure
With fully parallel execution, Playwright can distribute individual tests across shards. Without it, sharding generally operates at file granularity. Test-level distribution helps avoid placing one large file entirely on one shard, but balancing test counts does not guarantee equal execution time.
Measure the slowest shard, not just total test duration.
Do Not Use Serial Execution to Conceal Dependencies
A sequence such as “create account,” “update account,” and “delete account” should usually be one test with explicit stages, or three independently provisioned tests.
In a serial group, a failure skips later tests, and retries rerun the group together. That behavior can be appropriate for a genuinely indivisible workflow, but it is a poor substitute for resource isolation.
For a truly exclusive external resource, use an explicitly coordinated strategy across every job that can access it. Ordering tests inside one process does not coordinate independent CI runs.
7. Make Configuration an Explicit Execution Policy
A configuration file should explain how the suite runs without changing the scenario’s meaning:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const ci = Boolean(process.env.CI);
const externalBaseURL = process.env.BASE_URL;
const baseURL =
externalBaseURL || 'http://127.0.0.1:3000';
export default defineConfig({
testDir: './e2e/specs',
outputDir: 'test-results',
fullyParallel: true,
forbidOnly: ci,
workers: ci ? 1 : undefined,
retries: ci ? 1 : 0,
failOnFlakyTests: ci,
timeout: 30_000,
expect: {
timeout: 5_000,
},
reporter: ci
? [['line'], ['blob']]
: [['list'], ['html', { open: 'never' }]],
use: {
baseURL,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
webServer: externalBaseURL
? undefined
: {
command: 'npm run start:e2e',
url: `${baseURL}/health`,
reuseExistingServer: !ci,
timeout: 120_000,
},
});
These are deliberate starting choices, not universal optimums.
- Start with conservative CI concurrency. Playwright recommends one worker on CI for stability and reproducibility, with sharding as a way to distribute execution more widely. Increase workers only after measuring the runner and application under load.
- Use retries as diagnostic evidence. Playwright marks a test that fails initially and passes on retry as flaky. failOnFlakyTests makes those outcomes fail the run, so retries can gather evidence without silently weakening the quality gate. Teams adopting this incrementally can initially track flakes before enforcing the gate.
- Choose trace retention intentionally. retain-on-failure records every attempt and keeps failed attempts. on-first-retry records only the first retry, which reduces recording work but does not capture the original failed attempt.
- Make application readiness meaningful. Playwright’s webServer can launch the app and wait for an endpoint. In this example, start:e2e must start the test deployment, and /health should report readiness only after required dependencies are usable. Disabling server reuse on CI also prevents accidentally accepting an unrelated existing process.
For more on load testing with Playwright, see Codoid’s Artillery Load Testing with Playwright guide.
8. Build CI Around Reproducibility and Failure Evidence
A reliable CI pipeline needs more than a browser-test command. It should install locked dependencies, check the test code, start the correct application, preserve diagnostic output, and retain the original test-job result.
Playwright transpiles TypeScript but does not perform full type checking. Run the TypeScript compiler separately, and ensure the selected tsconfig.json includes both the E2E sources and Playwright configuration. Linting should also catch missing awaits, for example through @typescript-eslint/no-floating-promises.
The following workflow assumes the repository provides lint, build, and start:e2e scripts. Each test job starts its own disposable local application. It runs Chromium across four shards and combines their blob reports afterward.
# .github/workflows/e2e.yml
name: End-to-end tests
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
test:
name: Chromium shard ${{ matrix.shard }}/4
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
CI: "true"
E2E_RUN_ID: >-
${{ github.repository }}-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- run: npx tsc --noEmit
- run: npm run lint
- run: npm run build
- run: npx playwright install --with-deps chromium
- name: Run shard
run: >-
npx playwright test
--project=chromium
--shard=${{ matrix.shard }}/4
- name: Upload shard report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: blob-${{ github.run_attempt }}-${{ matrix.shard }}
path: blob-report/
if-no-files-found: error
retention-days: 7
report:
name: Merge test reports
needs: test
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- run: npm ci
- uses: actions/download-artifact@v5
with:
pattern: blob-${{ github.run_attempt }}-*
path: all-blob-reports
merge-multiple: true
- name: Build HTML report
run: >-
npx playwright merge-reports
--reporter=html
./all-blob-reports
- name: Require every shard report
shell: bash
run: |
count=$(find all-blob-reports -maxdepth 1 \
-type f -name '*.zip' | wc -l)
test "$count" -eq 4
- name: Upload HTML report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ github.run_attempt }}
path: playwright-report/
if-no-files-found: error
retention-days: 7
Preserve Failures Without Losing the Report
The workflow does not use continue-on-error or append || true to the test command. A failing shard remains a failing job.
Report collection is allowed after ordinary test failures, and the merge job can produce diagnostic output even when a shard failed. Blob reports contain test results and attachments, including traces, so they are suitable for combining sharded runs.
Keep the test jobs as required checks. A report-generation job is not a replacement for their exit statuses.
Artifact names include the workflow attempt to avoid silently mixing separate attempts. Because the completeness check expects all four reports from that attempt, rerun the entire workflow when generating a new complete merged report.
For a scheduled cross-browser run, install all required browsers and remove the Chromium project filter. As the suite grows, move repeated type checking, linting, and building into prerequisite jobs where doing so improves cost without weakening reproducibility.
Protect the Execution Boundary
The example uses readable major-version action references. In a hardened repository, pin approved actions to full commit SHAs and update them through a controlled process. Keep credentials narrowly scoped, restrict token permissions, and do not expose privileged execution or secrets to untrusted pull-request code.
Treat traces and reports as potentially sensitive application data, not automatically harmless build output. Set access and retention policies accordingly.
9. Keep the Architecture Maintainable as It Grows
The architecture should make common changes local.
A changed button label should usually affect a page or component object. A new workspace-provisioning mechanism should affect the API client and fixture. A larger browser matrix should affect configuration and CI, not business assertions.
Watch for abstractions that break those boundaries. A page object that reads CI environment variables is taking on execution policy. A fixture that automatically performs an entire checkout is concealing scenario behavior. A worker-scoped mutable object is introducing shared state that reviewers must reason about.
For an existing suite, migrate incrementally. Establish the fixture import boundary, move paired setup and teardown into fixtures, isolate mutable resources, and then enable broader parallel execution. Do not treat a large folder reorganization as proof that the architecture has improved.
Make debugging part of normal development. A focused test can be run directly, repeated, or executed with a different worker count through Playwright’s CLI. These commands help investigate repeatability and concurrency sensitivity, although a successful repeated run is not proof that a test is deterministic.
# Run one specification.
npx playwright test e2e/specs/workspaces/rename-workspace.spec.ts \
--project=chromium
# Investigate repeatability with retries disabled.
npx playwright test e2e/specs/workspaces/rename-workspace.spec.ts \
--project=chromium --repeat-each=20 --retries=0
# Compare behavior under greater concurrency.
npx playwright test --project=chromium --workers=4 --retries=0
Track first-attempt pass rate, flaky outcomes, fixture setup time, the slowest shard, and the effort required to diagnose a failure. Those measures give the team a more useful maintenance picture than test count alone.
For more on automation best practices, see Codoid’s Code Review Best Practices for Automation Testing and Best Practices for Automation Testing with BDD.
Conclusion
Maintainable Playwright automation starts with explicit ownership.A test should own the behavior it verifies. A page object should own the application’s UI vocabulary. Playwright fixtures should own the lifetime of every resource they provide. Parallel execution should operate on independently scoped state, and CI should preserve both reproducibility and failure evidence.
The goal is not the shortest test file or the most elaborate framework. It is a suite in which a test remains understandable, and its result remains trustworthy, when it runs alone, alongside hundreds of other tests, after a worker restart, or across several CI machines.Codoid’s automation testing services can help you design and implement a maintainable Playwright test architecture tailored to your team’s workflow.
Need Help Structuring Your Playwright Test Suite?
Talk to a Playwright ExpertFrequently Asked Questions
- What is Playwright test architecture?
Playwright test architecture is the way a test suite is organized into distinct layers with clear ownership. Tests describe behavior, page objects describe UI interactions, fixtures own resource lifecycles, and configuration plus CI control execution. A well-designed architecture keeps each layer focused so that a change to a button label affects only a page object, while a change to a browser matrix affects only configuration and CI. This separation is what allows a suite to remain understandable and trustworthy as it grows from a handful of tests to hundreds running across multiple CI machines.
- Why does Playwright test architecture matter for maintainability?
Without a clear architecture, tests accumulate shared state, duplicated selectors, and hidden dependencies. Failures become difficult to diagnose, and parallel execution becomes unsafe. A deliberate architecture makes common changes local. When a control name changes, only the page object needs updating. When a new provisioning mechanism is introduced, only the API client and fixture are affected. When the browser matrix expands, only the configuration and CI workflow change. This reduces maintenance effort and keeps test intent visible to reviewers.
- What is the difference between fixtures and page objects in Playwright?
Page objects own the application's UI vocabulary. They expose operations such as rename() or open() and encapsulate locators and UI-specific synchronization. Fixtures own resource lifecycles. They create, wire, and clean up dependencies such as browser contexts, test data, API clients, and authentication state. A page object answers "how do I interact with this screen," while a fixture answers "who creates this resource and when is it removed." Both are necessary, and combining them into one layer usually makes both harder to maintain.
- What is the recommended scope for Playwright fixtures?
The built-in browser should use worker scope, since Playwright manages it and sharing it avoids unnecessary startup work. Browser context, page, and page objects bound to a page should use test scope so each test receives isolated resources. Mutable workspaces, orders, or documents should use test scope. Immutable worker metadata such as a run namespace should use worker scope. Reusable accounts or expensive shared services should use worker scope only when their sharing rules are explicit and safe. Start with test scope by default and promote to worker scope only when the sharing rules are clearly understood.
- How should test data be isolated in a Playwright test architecture?
Each test should own its data. Generate a unique identifier for every resource the test creates, seed it through an API before the test runs, and remove it afterward using a fixture. This prevents tests from selecting or deleting each other's records, even when running in parallel across workers and CI shards. A useful pattern is to allocate the resource identifier before seeding, so the fixture can still attempt cleanup if the seed request fails partway through. For shared environments, add an expiration policy or cleanup service to handle resources left behind by killed processes.
- How does Playwright test architecture support parallel execution?
Parallel execution is safe when tests already own their state. Playwright runs test files in parallel by default, and fullyParallel allows tests within a file to run in parallel as well. Workers are separate processes, so a failure replaces only the affected worker. Sharding splits selected tests across separate CI machines. Projects define configurations such as browsers or devices. Because each test owns its own data and does not depend on execution order, increasing workers or adding shards does not introduce shared-state failures. Measure the slowest shard rather than total test duration when tuning concurrency.
- What is the difference between workers, shards, and projects in Playwright?
Workers control concurrent execution within a single Playwright invocation. Shards split selected tests across separate invocations, typically on separate CI machines. Projects define configurations such as Chromium, Firefox, WebKit, devices, or environments. Multiple projects in one invocation do not each receive an additional independent allocation of the global worker limit, so adding a browser dimension to the CI matrix creates more jobs and changes the capacity calculation. Understanding these three controls prevents over-provisioning and keeps pipeline costs predictable.
Comments(0)