When to use: One-time operations that must run before or after the entire test suite -- database seeding, environment health checks, creating shared auth state, starting external services. Runs once per
npx playwright testinvocation, not once per test or per worker.
globalSetup → runs ONCE before all tests in all projects
↓
setup projects → runs before dependent projects (has browser context)
↓
test projects → your actual tests
↓
teardown projects → runs after dependent projects (has browser context)
↓
globalTeardown → runs ONCE after all tests in all projects
Key distinction:
globalSetup/globalTeardown: No browser, no Playwright fixtures. Pure Node.js.- Setup projects with
dependencies: Has full browser context, can usepage,request, etc.
Use when: One-time non-browser work like database seeding, environment validation, or external service preparation. Avoid when: You need a browser (use a setup project instead) or per-test isolation (use fixtures).
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: './tests/global-setup.ts',
globalTeardown: './tests/global-teardown.ts',
testDir: './tests',
});// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
console.log('Global setup: seeding database...');
// Seed the test database
const { execSync } = await import('child_process');
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
execSync('npx prisma db seed', { stdio: 'inherit' });
// Store run metadata for tests to use
process.env.TEST_RUN_ID = `run-${Date.now()}`;
}
export default globalSetup;// tests/global-teardown.ts
import type { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
console.log('Global teardown: cleaning up...');
const { execSync } = await import('child_process');
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
}
export default globalTeardown;Use when: Verifying the test environment is healthy before running any tests. Fails fast if services are down.
Avoid when: Tests use webServer which already does a health check.
// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000';
const maxRetries = 10;
const retryDelay = 2000;
console.log(`Checking if ${baseURL} is reachable...`);
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(`${baseURL}/api/health`);
if (response.ok) {
console.log(`Environment is healthy (attempt ${i + 1})`);
return;
}
} catch {
// Connection refused or timeout -- retry
}
console.log(`Waiting for environment... (attempt ${i + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
throw new Error(`Environment at ${baseURL} is not reachable after ${maxRetries} attempts`);
}
export default globalSetup;Use when: Creating auth tokens or session cookies via API, without needing a browser. Avoid when: Login requires browser interaction (use a setup project instead).
// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000';
// Authenticate via API (no browser needed)
const response = await fetch(`${baseURL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'admin@example.com',
password: process.env.TEST_PASSWORD,
}),
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
}
const { token } = await response.json();
// Save the token as storageState for browser tests to pick up
const authDir = path.resolve(process.cwd(), 'playwright/.auth');
fs.mkdirSync(authDir, { recursive: true });
const storageState = {
cookies: [],
origins: [
{
origin: baseURL,
localStorage: [
{ name: 'auth_token', value: token },
],
},
],
};
fs.writeFileSync(
path.join(authDir, 'user.json'),
JSON.stringify(storageState, null, 2)
);
}
export default globalSetup;// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
globalSetup: './tests/global-setup.ts',
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
},
],
});Use when: Global setup generates values (IDs, tokens, URLs) that tests need. Avoid when: Each test should create its own data (the usual case).
Method 1: Environment variables (simplest):
// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
process.env.TEST_RUN_ID = `run-${Date.now()}`;
process.env.SEED_USER_ID = 'user-12345';
}
export default globalSetup;// tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';
test('dashboard shows seeded data', async ({ page }) => {
const userId = process.env.SEED_USER_ID;
await page.goto(`/users/${userId}/dashboard`);
await expect(page.getByRole('heading')).toBeVisible();
});Method 2: Shared file (for complex data):
// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
const SETUP_DATA_PATH = path.resolve(process.cwd(), 'test-data/setup-data.json');
async function globalSetup(config: FullConfig) {
const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000';
// Create test data via API
const res = await fetch(`${baseURL}/api/test/seed`, { method: 'POST' });
const seedData = await res.json();
// Write to shared file
const dir = path.dirname(SETUP_DATA_PATH);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(SETUP_DATA_PATH, JSON.stringify(seedData, null, 2));
}
export default globalSetup;// tests/helpers/setup-data.ts
import * as fs from 'fs';
import * as path from 'path';
const SETUP_DATA_PATH = path.resolve(process.cwd(), 'test-data/setup-data.json');
export function getSetupData(): { userId: string; orgId: string; apiKey: string } {
const raw = fs.readFileSync(SETUP_DATA_PATH, 'utf8');
return JSON.parse(raw);
}// tests/org-settings.spec.ts
import { test, expect } from '@playwright/test';
import { getSetupData } from './helpers/setup-data';
test('org settings page loads', async ({ page }) => {
const { orgId } = getSetupData();
await page.goto(`/orgs/${orgId}/settings`);
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
});Use when: Authentication requires browser interaction (form login, OAuth redirect, MFA). Avoid when: Auth can be done via API call (use Pattern 3 instead).
Important: globalSetup has no browser. For browser-based auth, use a setup project instead.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
// Setup project: runs first, has a browser, saves auth state
{
name: 'setup',
testMatch: /global\.setup\.ts/,
},
// Test projects: depend on setup, reuse saved auth state
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});// tests/global.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for navigation to confirm login succeeded
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Save signed-in state (cookies + localStorage)
await page.context().storageState({ path: authFile });
});Use when: Starting or configuring external services (mock servers, test containers, feature flags) before any tests run. Avoid when: Per-test or per-worker isolation is needed (use fixtures).
// tests/global-setup.ts
import type { FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
// Start a mock API server
const { createServer } = await import('../mocks/server');
const server = await createServer();
const port = await server.listen(0);
process.env.MOCK_API_URL = `http://localhost:${port}`;
// Configure feature flags for test environment
const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000';
await fetch(`${baseURL}/api/admin/feature-flags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.ADMIN_API_KEY}`,
},
body: JSON.stringify({
newCheckout: true,
darkMode: false,
betaFeatures: true,
}),
});
// Return a cleanup function (Playwright calls globalTeardown separately)
// For cleanup, use globalTeardown
}
export default globalSetup;// tests/global-teardown.ts
import type { FullConfig } from '@playwright/test';
async function globalTeardown(config: FullConfig) {
// Reset feature flags
const baseURL = config.projects[0]?.use?.baseURL || 'http://localhost:3000';
await fetch(`${baseURL}/api/admin/feature-flags/reset`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` },
});
}
export default globalTeardown;| Need | Use | Why |
|---|---|---|
| One-time DB seed | globalSetup |
No browser needed; runs once before everything |
| Browser-based login (shared state) | Setup project with dependencies |
Needs page and context (not available in globalSetup) |
| API-based auth token | globalSetup |
Simple HTTP call, no browser needed |
| Per-test unique data | Custom fixture via test.extend() |
Each test gets isolated data |
| Per-worker shared resource | Worker-scoped fixture ({ scope: 'worker' }) |
Shared within worker, isolated between workers |
| Health check before tests | globalSetup |
Fail fast if environment is down |
| Start mock server | globalSetup + globalTeardown |
One-time server lifecycle |
| Clean up after all tests | globalTeardown |
Runs once at the end regardless of pass/fail |
Do I need globalSetup?
│
├── Does the work need a browser (page, context)?
│ ├── YES → Use a setup project, not globalSetup
│ └── NO → globalSetup is appropriate
│
├── Does every test need unique/isolated data?
│ ├── YES → Use a fixture with test.extend()
│ └── NO → globalSetup for shared, read-only data
│
├── Is it per-worker (expensive resource, connection pool)?
│ ├── YES → Worker-scoped fixture
│ └── NO → globalSetup for truly global, one-time work
│
└── Is it cleanup?
├── After all tests → globalTeardown
├── After each test → Fixture teardown (after use())
└── After each worker → Worker-scoped fixture teardown
| Anti-Pattern | Problem | Do This Instead |
|---|---|---|
Browser login in globalSetup |
No browser context available; complex workarounds | Use a setup project with dependencies |
Creating per-test data in globalSetup |
All tests share the same data; not isolated | Use per-test fixtures for unique data |
globalSetup without globalTeardown |
Database or services left in dirty state | Always pair setup with teardown |
| Storing setup results in module-level variables | Workers are separate processes; variables don't share | Use environment variables or files |
Complex logic in globalSetup |
Hard to debug; runs outside normal test lifecycle | Keep it minimal: seed, verify, set env vars |
globalSetup that takes > 60 seconds |
Slows every test run, even for a single test | Move heavy work to a separate script or CI step |
Relying on globalTeardown for critical cleanup |
If the process crashes, globalTeardown might not run |
Design tests to be idempotent; use beforeAll in setup project |
Cause: Each worker is a separate process. process.env mutations in globalSetup propagate to workers, but only if set before workers spawn.
Fix: Set environment variables at the top level of globalSetup, before any async work that might delay:
// tests/global-setup.ts
async function globalSetup() {
// This works -- set before returning
process.env.TEST_RUN_ID = `run-${Date.now()}`;
}
export default globalSetup;If environment variables are still missing, write data to a file instead (Pattern 4, Method 2).
Cause: The path in globalSetup is relative to the config file, but the module resolution is wrong.
Fix: Use a path relative to the project root:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: './tests/global-setup.ts', // relative to config location
globalTeardown: './tests/global-teardown.ts',
});Cause: If the process is killed (SIGKILL, OOM) rather than exiting normally, teardown is skipped.
Fix: Design your setup to be idempotent. Global setup should handle a dirty state from a previous incomplete run:
// tests/global-setup.ts
async function globalSetup() {
// Always reset first, then seed -- handles dirty state
const { execSync } = await import('child_process');
execSync('npx prisma db push --force-reset', { stdio: 'inherit' });
execSync('npx prisma db seed', { stdio: 'inherit' });
}
export default globalSetup;Cause: The dependencies configuration requires the setup project to run before any dependent project.
Fix: This is expected behavior. To skip setup during focused debugging:
# Skip setup by running without dependencies
npx playwright test --project=chromium --no-deps tests/specific-test.spec.tsCause: The setup project or globalSetup that creates the file failed silently, or the path is wrong.
Fix: Add explicit error handling and verify the file exists:
// tests/global.setup.ts
import { test as setup, expect } from '@playwright/test';
import * as fs from 'fs';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.context().storageState({ path: authFile });
// Verify the file was created
if (!fs.existsSync(authFile)) {
throw new Error(`Auth state file was not created at ${authFile}`);
}
});- core/fixtures-and-hooks.md -- per-test and per-worker fixtures (preferred over globalSetup for most cases)
- core/configuration.md --
globalSetup,globalTeardown,webServerconfig - ci/projects-and-dependencies.md -- setup projects with
dependencies - core/authentication.md -- authentication patterns using setup projects
- core/test-data-management.md -- seeding and managing test data