I used to think Playwright test failures mostly came from flaky waits or unstable environments. But after digging into repeated CI failures, a different pattern stood out: the issues were rooted in how test setup and state were managed.
Playwright wasn’t the problem—test structure was. Once fixtures replaced duplicated setup, shared globals, and ad-hoc hooks, stability improved fast.
In this article, I’ll explain how Playwright fixtures actually work and how using them correctly makes test suites predictable, faster, and easier to scale.
What are Fixtures in Playwright?
Playwright Fixtures pertain to the concept of Test Fixtures.
A test fixture is a fixed state of a set of objects used as a base for running tests. It sets up the system for testing by yielding the initialization code, such as loading up a database with defined parameters from a site before running the test.
Why are Playwright Fixtures important?
Playwright fixtures are important because they control how tests are initialized, isolated, reused, and scaled, directly impacting reliability, execution speed, and maintainability as test suites grow.
- Ensure every test starts from a clean and predictable state with guaranteed setup and teardown
- Prevent state leakage when running tests in parallel across browsers and environments
- Eliminate repeated setup logic by centralizing common test initialization
- Make test dependencies explicit, improving readability and debugging
- Reduce flaky test failures caused by shared state or improper cleanup
- Optimize execution time by reusing expensive setup through worker-scoped fixtures
- Provide consistent behavior across local runs and CI pipelines
Read More: Playwright Selectors: Types
Types of Playwright Fixtures
In Playwright, fixtures are categorized based on how they are scoped, created, and executed rather than by explicit “fixture types” as a framework concept.
Built-in Playwright Fixtures
These fixtures are provided out of the box by Playwright Test and cover core browser and test execution needs. They are automatically available in every test without extra configuration.
Key built-in fixtures include:
- browser – A shared browser instance used across contexts.
- context – An isolated browser context created per test.
- page – A fresh page instance created from the context.
- request – APIRequestContext for API testing.
- browserName – Indicates the current browser under execution.
- testInfo – Metadata about the running test such as retries, status, and output paths.
Built-in fixtures are designed for safe parallel execution and reset automatically between tests, making them reliable defaults for most scenarios.
Custom Fixtures
Custom fixtures are user-defined extensions created using test.extend() to encapsulate reusable setup and teardown logic.
Common use cases:
- Authentication and logged-in sessions
- Test data setup and cleanup
- Initializing helpers or utilities
- Managing feature flags or environment state
Custom fixtures allow teams to move complex setup logic out of test files, improving readability and maintainability while keeping tests declarative.
Test-Scoped Fixtures
Test-scoped fixtures are created and destroyed for every individual test.
Characteristics:
- Maximum isolation between tests
- Safe for mutable state
- Slightly higher execution cost due to repeated setup
Typical examples:
- Creating a new logged-in page per test
- Seeding test data that must not be shared
- Initializing mocks or spies
Test-scoped fixtures are the default scope in Playwright and are preferred when correctness and isolation matter more than execution speed.
Read More: Cross Browser Testing using Playwright
Worker-Scoped Fixtures
Worker-scoped fixtures are created once per worker process and reused across multiple tests running in that worker.
Characteristics:
- Faster execution for heavy setup
- Shared state across tests in the same worker
- Requires careful handling to avoid test coupling
Typical examples:
- Database connections
- Preloaded authentication tokens
- Large test datasets
- Expensive service initialization
Worker-scoped fixtures are especially useful in large test suites running in parallel where repeated setup becomes a bottleneck.
Auto Fixtures
Auto fixtures are fixtures that execute automatically without being explicitly referenced in a test.
Key behavior:
- Enabled using auto: true
- Runs for every test within scope
- Useful for global setup or enforcement logic
Common use cases:
- Global cleanup routines
- Environment validation
- Attaching logs or traces
- Enforcing test preconditions
Auto fixtures should be used sparingly, as they can introduce hidden dependencies that make tests harder to reason about.
Extended Built-in Fixtures
These fixtures are created by extending existing Playwright fixtures such as page or context to customize their behavior.
Examples include:
- A page fixture that always starts in an authenticated state
- A context fixture with predefined permissions
- A page fixture with network mocking enabled
This approach is preferred over duplicating setup logic in multiple tests and ensures consistent behavior across the test suite.
Shared Fixtures (Centralized Fixtures)
Shared fixtures are custom fixtures defined in a separate fixtures file and imported across multiple spec files or projects.
Benefits:
- Consistent setup across large repositories
- Easier maintenance and refactoring
- Cleaner test files with minimal boilerplate
Shared fixtures are essential when scaling Playwright across teams, applications, or multiple environments.
Parameterized Fixtures
Parameterized fixtures accept configuration or runtime data to alter their behavior.
Common use cases:
- Role-based authentication (admin, user, guest)
- Region- or locale-specific setup
- Feature-flag-driven test execution
Parameterized fixtures reduce duplication by allowing a single fixture to support multiple test variations without branching logic inside tests.
Read More: How to start with Playwright Debugging?
Fixture Lifecycle
The Playwright fixture lifecycle follows a strict and predictable execution model that determines when fixtures are initialized, resolved, injected, and torn down. Understanding these steps is critical for debugging setup failures and designing reliable fixture hierarchies.
- Playwright parses the test file and identifies all fixtures required by the test based on the test function parameters.
- The fixture dependency graph is resolved, ensuring that fixtures required by other fixtures are initialized first.
- Worker-scoped fixtures begin setup once per worker process before any test in that worker starts.
- Test-scoped fixtures begin setup immediately before the individual test runs.
- For each fixture, setup logic executes until the await use(value) call is reached.
- The resolved fixture value is injected into the test or dependent fixture.
- Once all required fixtures are resolved, the test body starts execution.
- If a fixture throws an error during setup, the test is marked as a setup failure and the test body is skipped.
- After the test completes, teardown begins for test-scoped fixtures.
- Teardown logic executes after the use() call finishes, even if the test fails or times out.
- Teardown runs in reverse order of fixture initialization to ensure dependent resources are cleaned safely.
- Once all tests in a worker complete, worker-scoped fixture teardown executes.
- Final cleanup runs before the worker process exits, releasing shared resources.
This lifecycle guarantees deterministic setup, explicit dependency resolution, and reliable cleanup, making fixture-related failures easier to isolate and diagnose in both local and CI executions.
When to use Playwright Fixtures?
Playwright Fixtures are especially useful when:
- Repeating test setups across multiple test cases.
- Needing to establish consistent conditions (e.g., logins, API states) for various tests.
- Simplifying teardown processes after test execution.
Steps to Implement Fixtures in Playwright
Creating a Playwright fixture is straightforward and significantly aids in reusing functions.
Below are the detailed steps for implementing them.
Creation of Custom fixture in a test file
To create a new fixture file loginFixture.ts for a custom fixture, first import the Playwright test as baseTest.
import { test as baseTest } from '@playwright/test';
// Define custom fixture
const test = baseTest.extend<{loggedInPage: any;}>({
loggedInPage: async ({ page }, use) => {
// Go to the login page
await page.goto('https://www.bstackdemo.com/signin'); // Replace with login URL
// Fill in login details
await page.locator('id=username').click(); // click on the username field
await page.locator('#react-select-2-option-0-0').click(); // click the first option
await page.locator('id=password').click(); // click on the password field
await page.locator('#react-select-3-option-0-0').click(); // click the first option
await page.click('button[type="submit"]');
// Make the logged-in page available for tests
await use(page);
}
});
export { test };A variable test will be defined at this stage by extending the base test. Inside the extend method, the loggedInPage fixture will be used as the key, with an anonymous function as the value, where use will serve as the second parameter.
const test = baseTest.extend<{loggedInPage: any;}>({
loggedInPage: async ({ page }, use) => {
// }The next block features a simple test that navigates to https://www.bstackdemo.com/signin, selects the username and password, and logs into the application.
// Go to the login page
await page.goto('https://www.bstackdemo.com/signin'); // Replace with your login URL
// Fill in login details
await page.locator('id=username').click(); // click on the username field
await page.locator('#react-select-2-option-0-0').click(); // click the first option
await page.locator('id=password').click(); // click on the password field
await page.locator('#react-select-3-option-0-0').click(); // click the first option
await page.click('button[type="submit"]');
// Wait for navigation to complete after login
await page.waitForNavigation();This line will make the page available for all the tests.
await use(page);
Using Fixtures in the spec file
A new spec file named login.spec.ts will be created, with a reference to loggedInPage. Since this page is already logged in, it enables testing of other application functionalities without the need to repeat the login process.
import { test } from '../fixtures/loginFixture';
test('should navigate to the dashboard after login', async ({ loggedInPage }) => {
// Now we're logged in, so we can go to the dashboard
await loggedInPage.goto('https://www.bstackdemo.com/');
});Best Practices of Using Fixtures in Playwright
Here are some best practices when using Playwright fixtures.
1. Test on real devices
- Playwright fixtures are useful in creating and defining reusable and consistent steps for a process flow, such as Login, authentication, and API mocking.
- BrowserStack allows access to real device cloud platform with over 3500+ different device, browsers, and OS combinations and browsers, ensuring that Playwright Fixtures run in real user conditions rather than virtual environments.
- This helps early identification of device-oriented bugs.
For example, login fixtures can be used to automate all devices and browsers in a single instance, ensuring that the login functionality operates efficiently.
2. Cross-Browser and Cross-Platform Support
- BrowserStack enables the execution of Playwright tests across various devices, browsers, and operating system combinations.
- Adapting Playwright fixtures ensures that test steps remain flexible and reusable across different combinations.
- This allows for the same test steps to be executed in multiple browsers, including Chrome, Firefox, Safari, and Edge, across various versions.
3. CI/CD Integration
- BrowserStack integrates flawlessly with CI/CD tools such as GitHub Actions, Jenkins, and CircleCI. The tests can be run in a clean and isolated environment by configuring Playwright Fixtures.
- Automating the Playwright tests on BrowserStack after every build will help ensure the application is validated and tested in real-world scenarios.
For example: Automating the tests with BrowserStack through parallel testing in GitHub Actions will make sure that on every deployment, the test cases are run across real device.
Playwright Fixture Scope
Fixture scope defines how long a fixture exists and how frequently it is created during a test run. It directly affects test isolation, execution speed, and reliability, especially when tests run in parallel.
Test-scoped fixtures: Test-scoped fixtures represent resources that are created specifically for a single test and discarded immediately after that test completes.
- Created fresh for each individual test
- Teardown runs immediately after the test finishes
- Provide strong isolation and prevent shared state issues
- Suitable for mutable resources such as pages, sessions, cookies, and test data
- Higher execution cost when setup is expensive
Worker-scoped fixtures: Worker-scoped fixtures represent shared resources that live for the entire lifetime of a worker process and are reused across multiple tests.
- Created once per worker process
- Shared across all tests running in the same worker
- Teardown runs after all worker tests complete
- Ideal for expensive or read-only setup operations
- Requires careful state management to avoid cross-test interference
Scope behavior in parallel execution: Scope behavior determines how fixtures are instantiated when tests run concurrently across multiple workers.
- Test-scoped fixtures are isolated across all parallel tests
- Worker-scoped fixtures are isolated per worker, not per test
- Increasing worker count multiplies worker-scoped fixture instances
- Incorrect scope selection often leads to flaky tests in CI environments
Choosing the right fixture scope: Choosing the correct fixture scope is a design decision that balances isolation, performance, and maintainability.
- Use test scope when correctness and independence are critical
- Use worker scope when setup cost is high and state can be safely shared
- Avoid mixing scopes without clear ownership to prevent hidden dependencies
Fixtures vs Hooks
Fixtures and hooks both manage setup and teardown in Playwright, but they differ in how setup is declared, scoped, and executed. Fixtures are dependency-driven and explicit, while hooks are implicit and tied to file or block structure. This distinction becomes critical as test suites scale and run in parallel.
| Aspect | Playwright Fixtures | Playwright Hooks |
|---|---|---|
| Purpose | Provide explicit, reusable setup units injected into tests | Run lifecycle callbacks before or after tests |
| Declaration | Declared as test dependencies via function parameters | Declared using beforeEach, afterEach, beforeAll, afterAll |
| Visibility | Test dependencies are visible in the test signature | Setup logic is hidden inside hooks |
| Scope control | Support test and worker scopes | Limited to file or describe block scope |
| Parallel safety | Designed for parallel execution by default | Can accidentally share state across tests |
| Reusability | Easily shared across files and projects | Tightly coupled to test file structure |
| Execution behavior | Runs only when the fixture is required | Runs for every test in its scope |
| Failure reporting | Setup failures are clearly reported before test runs | Failures often appear as test failures |
| Dependency support | Fixtures can depend on other fixtures | No native dependency graph |
| Performance optimization | Worker-scoped fixtures reduce repeated setup | No equivalent mechanism for shared setup |
| Scalability | Suitable for large, modular test suites | Becomes hard to maintain at scale |
Fixtures are the preferred approach for reusable, scalable, and parallel-safe setup logic, while hooks are best limited to simple, file-specific initialization that does not need reuse or scope control.
How BrowserStack Automate Can Enhance Playwright Tests
BrowserStack Automate offers various functionalities for running the tests on multiple devices and parallelism.
Here are a few advantages of using BrowserStack Automate in Playwright tests:
- Reduce the time taken for execution – Parallel execution allows to run multiple test cases in parallel. With BrowserStack Automate, many number of tests can be run in parallel across different devices and browsers.
- Cross Browser and Cross device execution – BrowserStack Automate helps to run multiple tests simultaneously across various devices and OS combinations, thus giving the leverage of efficiency in testing the application.
- Scalability – BrowserStack is scalable to many devices and can spin up as many browser combinations as possible with the any additional maintenance.
- Cost efficient – BrowserStack Automate is cost-effective in the longer term in case of maintenance of the tests.
- CI/CD Integration – Tests can be integrated in the CI/CD process through BrowserStack, which is optimal for continuous testing and integration.
Useful Playwright Resources
- Playwright Automation Framework
- Playwright Java Tutorial
- Playwright Python tutorial
- Playwright Debugging
- End to End Testing using Playwright
- Visual Regression Testing Using Playwright
- Mastering End-to-End Testing with Playwright and Docker
- Page Object Model in Playwright
- Scroll to Element in Playwright
- Understanding Playwright Assertions
- Cross Browser Testing using Playwright
- Playwright Selectors
- Playwright and Cucumber Automation
Tool Comparisons: