Your test passes locally. You push to CI and it fails — same browser, same code, nothing changed. Before you can fix it, you need to understand what Playwright is actually doing before, during, and after your test runs.

Most Playwright confusion isn't a skill gap — it's a mental model gap. Playwright isn't just a test runner that executes your code line by line. It's a structured execution engine with five distinct phases that surround your test code, each responsible for a different part of the process. Once you have this map, the weird behavior stops feeling random.


The 5-Phase Execution Map

playwright.config.ts
       ↓
Phase 1 — Preparation       (config + global setup)
       ↓
Phase 2 — Test Definition   (collect files, hooks, fixtures)
       ↓
Phase 3 — Execution         (your test code runs here)
       ↓
Phase 4 — Reporting         (artifacts + output)
       ↓
Phase 5 — Teardown          (global teardown + cleanup)

Phase 1: Preparation

What Playwright does before touching your test code:

  • Reads playwright.config.ts and validates all settings

  • Initializes the environment and resolves baseURL, outputDir, reporters

  • Runs your globalSetup file if defined

  • Configures each project (Chrome, Firefox, mobile viewports)

None of your test logic has been executed yet at this point.

What this means for you:

  • If your global setup is running but config changes aren't taking effect, the order matters — config loads first, then global setup, then projects.

  • If an environment variable or base URL isn't being picked up, playwright.config.ts is the first place to look, not your test file.

// playwright.config.ts
export default defineConfig({
  globalSetup: './global-setup.ts',
  use: { baseURL: process.env.BASE_URL ?? 'http://localhost:3000' },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
})

Phase 2: Test Definition

What Playwright does to build a map of all your tests:

  • Collects all test files matching testMatch in config

  • Registers describe blocks and their scope

  • Wires up hooks (beforeAll, beforeEach, afterEach, afterAll)

  • Resolves and validates all fixtures

This is a map-building step — nothing runs yet.

What this means for you:

  • If a fixture isn't available inside a test, it wasn't registered at this phase.

  • Fixtures need to be defined via test.extend(), not inside a helper function or hook.

  • If you're getting "fixture not found" errors, this is where the problem lives.

// fixtures.ts
import { test as base } from '@playwright/test'

export const test = base.extend({
  apiClient: async ({}, use) => {
    const client = new ApiClient()
    await use(client)
    await client.dispose()
  },
})

Phase 3: Execution

The heart of Playwright — and more layered than most engineers expect.

For each test, Playwright runs this sequence:

  • Set up fixtures (in dependency order)

  • Run beforeAll hooks (once per describe block)

  • Run beforeEach hooks

  • Execute your test steps and assertions

  • Collect screenshots, videos, traces if configured

  • Run afterEach hooks

  • Run afterAll hooks (once per describe block)

  • Tear down fixtures (in reverse order)

  • If retries are enabled: repeat the entire sequence

What this means for you:

  • If your test is retrying unexpectedly, check the retries key in playwright.config.ts — it might be set globally.

  • If beforeEach is running more times than expected, you may have hooks at both the file level and inside a describe block, and both are firing.

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,
})

Phase 4: Reporting

After all tests finish, Playwright hands off to the reporting phase:

  • Generates reports (HTML, JSON, JUnit — whatever you configured)

  • Merges results across shards or parallel workers

  • Writes artifacts — screenshots, videos, traces — to outputDir

What this means for you:

  • An empty report almost always means either the reporter isn't configured in playwright.config.ts,

  • or CI isn't preserving the output folder.

  • The reporter and outputDir keys in your config control this entirely.

  • Check both before debugging your test logic.

// playwright.config.ts
export default defineConfig({
  reporter: [['html', { outputFolder: 'playwright-report' }]],
  outputDir: 'test-results/',
})

Phase 5: Teardown

The mirror of Preparation — and the phase most engineers ignore.

  • Stops any web servers started via webServer in config

  • Runs your globalTeardown file if defined

  • Cleans up fixtures and temp directories

What this means for you:

  • If your browser process stays open after tests finish, or your dev server doesn't stop, the answer is in the Teardown phase.

  • Global teardown code belongs in the file referenced by globalTeardown in your config — not in an afterAll inside a test file.


Common Misconceptions

"Playwright runs tests the moment it reads the file."

False. Test files are collected in Phase 2 (Test Definition), but nothing executes until Phase 3. Syntax errors in test files surface in Phase 2, not during execution.

"beforeAll runs once per test."

No. beforeAll runs once per describe block scope. If you have nested describe blocks each with their own beforeAll, both fire.

"Retries re-run only the failed assertion."

No. The entire Phase 3 sequence repeats — fixtures are torn down and rebuilt, all hooks re-fire. This is why flaky network state or side effects can cause retries to behave unexpectedly.

"globalSetup is the same as beforeAll."

Not the same scope. globalSetup runs once for the entire test suite before any test file is touched (Phase 1). beforeAll runs per describe block in Phase 3.


Playwright runs your tests inside a structured five-phase lifecycle — and knowing which phase you're in tells you exactly where to look when something breaks.


Key Takeaways

  • Playwright has five execution phases: Preparation → Test Definition → Execution → Reporting → Teardown

  • Your test code only runs in Phase 3 — everything else is infrastructure

  • Config issues belong to Phase 1, fixture issues to Phase 2, retry issues to Phase 3

  • globalSetup/globalTeardown are Phase 1 and Phase 5 — not hooks inside tests

  • Empty reports and hanging processes are Teardown problems, not test logic problems


What's Next

Next in this series: Playwright Fixtures Demystified — what fixtures are, where they live in the lifecycle, and why teardown sometimes silently doesn't fire. Coming soon.