Back to the blogWhat are Playwright fixtures? Fresh state every run, and CI
By WonyoungAugust 21, 20265 minOn this page
After a handful of tests, the same setup starts repeating: sign in, seed some data, navigate to a screen. Playwright fixtures pull that setup out into one place and hand each test only what it asks for. Once the suite is organised that way, it can run automatically on every push.
Playwright fixtures prepare the environment a test needs and hand it over. Each test gets a fresh page and empty storage, so nothing one test leaves behind reaches the next. Wire that into CI and the suite runs itself on every push.
When tests interfere with each other
The most common symptom: run them one at a time and they all pass; run them together and some fail.
The cause is usually state left behind by an earlier test — data it created, a session it kept, a screen it left open. Once tests interfere, results stop being trustworthy, because a failure could be the test itself or the one before it. If you grow the example from the getting started post into several tests, you hit this quickly.
Playwright blocks this structurally. Every test gets a fresh browser environment by default.
Playwright fixtures hand each test a fresh page and empty storage
What Playwright fixtures do
Fixtures prepare the environment a test needs and hand it over. The documentation frames the goal as giving a test "everything it needs and nothing else."
Several come built in.
| Name | What it gives you |
|---|---|
page | A fresh page, created per test |
context | A browser context with isolated cookies and storage |
browser | The browser itself, shared across tests to save resources |
browserName | Which browser is running |
request | An API request context with no page attached |
The page you have seen in every earlier post is one of these.
test('the dashboard renders', async ({ page }) => {
await page.goto('/dashboard');
});That page in the braces is not just a variable — it is a request for something the test needs. Write it and Playwright builds a page for this test alone. Whatever the previous test did does not reach it.
How this differs from beforeEach
beforeEach is the other way to organise setup: a block that runs before each test. Fixtures differ in a few ways.
- Setup and teardown live together. The code that builds a thing and the code that cleans it up sit in one place, which makes them easier to change.
- Only what is requested is built. A test that does not ask for a fixture never triggers it.
- They are reusable across files. Define once instead of copying into each spec.
- They compose. One fixture can depend on another.
Put simply, beforeEach says "every test in this file goes through this setup," while a fixture says "this test asks for this setup." Because the test does the asking, reading the test file tells you what it needs.
Writing your own
Setup you use often can be pulled into a fixture. A signed-in session is the usual example.
import { test as base } from '@playwright/test';
export const test = base.extend<{ loggedInPage: Page }>({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('me@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await use(page);
},
});Everything before use is setup, everything after is teardown. Now the test does not need to know how signing in works.
test('order history is visible', async ({ loggedInPage }) => {
await loggedInPage.goto('/orders');
});When the login screen changes, there is one file to fix. That holds whether you have five tests or fifty.
Test automation — running on every push
At this point the suite is organised, but anything you trigger by hand gets forgotten. It has to run on its own.
If you said yes to the CI workflow question during install, .github/workflows/playwright.yml already exists. Per the documentation, it works like this.
- It runs on pushes to the main branch and on pull requests
- It installs dependencies, downloads browsers and runs the suite
- It keeps the run artifacts for 30 days
That last point matters. When something fails, download the artifact, unzip it and open the report, and you can rewind exactly what happened on the runner.
Push code, CI runs the suite, results are kept for 30 days by default
Keep readingWhat is Playwright trace viewer? What is left when a test failsplaywright trace viewerPlaywright5 min
Closing the series — what tools do not do
Across this series we covered:
- Handing testing to AI, and where that boundary sits
- Recording clicks into code
- Pointing at elements so they survive UI changes
- Keeping enough of a failure to investigate it
- Install through first run
- Organising setup and running it automatically
With all of that in place, execution leaves your hands. And yet the same thing was left over at every step.
Deciding what to check.
A tool checks what it was told to check. It does not judge whether a change to the page was intended. That judgement needs a standard for what the correct state is, and that standard does not live in the code.
Execution is already solved, and solved for free. Playwright did it well. The gap that remains is the step before it.
Keep readingWhat Is Playwright MCP? 7 Strengths and 3 Ways to Chooseplaywright mcpplaywright8 min
Specnote takes the step before
Specnote turns confirm what was built, then test that scope into a product. AI lists what it built, a person removes what is wrong and adds what is missing, approves the list, and only that scope runs in a real browser.
If you can read and edit code, using Playwright the way this series describes is more precise and more flexible. What we aim at is the case where nobody has the time or the person to do that.
Technical details in this post were verified against the Playwright documentation on Fixtures and CI with GitHub Actions on August 21, 2026.
Frequently asked questions
What are Playwright fixtures?
They prepare the environment a test needs and hand it over. Pages and browser contexts come built in, and setup you repeat — like a signed-in session — can be defined as your own.
Is beforeEach wrong?
No. The difference is who asks. beforeEach applies to the whole file; a fixture is requested by the test that needs it. As the number of setup variants grows, fixtures stay easier to organise.
How do I stop tests from interfering with each other?
That is already the default — each test gets a fresh browser environment. When it still happens, the cause is usually state outside the browser, such as a shared database, and that has to be cleaned up in setup.
Do I have to set up CI?
Anything you run by hand gets forgotten. If the point of the suite is catching problems before release, it has to run on its own.
If I follow this whole series, is test automation finished?
Execution is. Deciding what to check, and redefining that scope every time the UI moves, is still there. That part is judgement, not tooling. > [!CTA] > Start by deciding the scope > > We list what AI built. Approve it, and that scope runs in a real browser on every release. > > Start free

