Specnote
Back to the blog
playwright fixturesPlaywrighthow-toE2E testingSpecnote

What are Playwright fixtures? Fresh state every run, and CI

By WonyoungAugust 21, 20265 min
On 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.

Key takeaways

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 give every test a fresh environmentPlaywright 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.

NameWhat it gives you
pageA fresh page, created per test
contextA browser context with isolated cookies and storage
browserThe browser itself, shared across tests to save resources
browserNameWhich browser is running
requestAn API request context with no page attached

The page you have seen in every earlier post is one of these.

A test that asks for page
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.

fixtures.ts
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.

tests/order.spec.ts
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.

Do this after the suite has grown, not before. Designing the structure up front means guessing at situations that have not happened. Pulling something out once the same setup appears three or four times is about right.

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.

Tests run on every push and results are keptPush 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

Keep reading

If you enjoyed this post