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.