globalSetup runs once before any browser workers spin up, in the main process, which means you can't pass JavaScript objects to tests: share data through files written to disk or environment variables set with process.env. The most common use is logging in once and saving the result with storageState, so every test in the suite starts authenticated without repeating the login flow. This guide covers the auth state pattern, multiple user roles with separate storage files, database seeding, global teardown, and the conditional setup pattern that skips re-login when a recent auth file already exists.
What global setup is
Playwright runs a globalSetup function once before the entire test suite starts, and a globalTeardown function once after everything finishes. These run in the main process, before any browser workers spin up.
Use global setup for:
- Authenticating and saving
storageState(most common use) - Seeding test database with baseline data
- Starting a mock server or test server
- Generating test data files
Basic configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
globalTeardown: require.resolve('./global-teardown'),
use: {
baseURL: 'http://localhost:3000',
},
});The most common use: saving auth state
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`${baseURL}/login`);
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL(`${baseURL}/dashboard`);
// Save cookies + localStorage for reuse
await page.context().storageState({ path: 'playwright/.auth/user.json' });
await browser.close();
}
export default globalSetup;// playwright.config.ts — use the saved auth in all tests
use: {
storageState: 'playwright/.auth/user.json',
},Every test now starts already authenticated. No login step. No session setup per test. Login happens once.
Multiple user roles
For apps with admin and regular user roles:
// global-setup.ts
async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
const browser = await chromium.launch();
// Save admin auth
const adminPage = await browser.newPage();
await loginAs(adminPage, `${baseURL}`, process.env.ADMIN_EMAIL!, process.env.ADMIN_PASSWORD!);
await adminPage.context().storageState({ path: 'playwright/.auth/admin.json' });
await adminPage.close();
// Save regular user auth
const userPage = await browser.newPage();
await loginAs(userPage, `${baseURL}`, process.env.USER_EMAIL!, process.env.USER_PASSWORD!);
await userPage.context().storageState({ path: 'playwright/.auth/user.json' });
await userPage.close();
await browser.close();
}
async function loginAs(page: Page, baseURL: string, email: string, password: string) {
await page.goto(`${baseURL}/login`);
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Log in' }).click();
await page.waitForURL(`${baseURL}/dashboard`);
}Then use different auth states per project:
// playwright.config.ts
projects: [
{
name: 'admin-tests',
use: { storageState: 'playwright/.auth/admin.json' },
testMatch: '**/admin/**/*.spec.ts',
},
{
name: 'user-tests',
use: { storageState: 'playwright/.auth/user.json' },
testMatch: '**/user/**/*.spec.ts',
},
],Database seeding in global setup
// global-setup.ts
import { Pool } from 'pg';
async function globalSetup() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// Seed baseline test data once
await pool.query(`
INSERT INTO users (email, password_hash, role)
VALUES ('testuser@example.com', $1, 'user')
ON CONFLICT (email) DO NOTHING
`, [hashedPassword]);
await pool.end();
}Global teardown
// global-teardown.ts
import { Pool } from 'pg';
async function globalTeardown() {
// Clean up test data after the full suite
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await pool.query("DELETE FROM users WHERE email LIKE '%@test.example.com'");
await pool.end();
// Stop any servers started in globalSetup
// (if you stored a reference on process or global)
}
export default globalTeardown;Sharing data between global setup and tests
Global setup runs in a separate process from the tests. You can't pass JavaScript objects directly. Options:
Files: Save data to disk in global setup, read it in tests.// global-setup.ts
import fs from 'fs';
async function globalSetup() {
const testData = { userId: '123', orderId: '456' };
fs.writeFileSync('playwright/.test-data.json', JSON.stringify(testData));
}// in tests
import testData from '../playwright/.test-data.json';
test('uses seeded data', async ({ page }) => {
await page.goto(`/orders/${testData.orderId}`);
});process.env.KEY = value. They're available in the test worker processes. Use for simple values (IDs, tokens).
Running global setup conditionally
Skip global setup when running individual tests locally:
// global-setup.ts
async function globalSetup() {
// Skip if auth file already exists and is recent
const authFile = 'playwright/.auth/user.json';
if (fs.existsSync(authFile)) {
const stats = fs.statSync(authFile);
const ageMinutes = (Date.now() - stats.mtimeMs) / 60000;
if (ageMinutes < 30) {
console.log('Auth state is recent, skipping login...');
return;
}
}
// Otherwise, log in and save
// ...
}This makes repeat local runs faster — the login step only happens when the saved auth is missing or stale.
→ See also: Handling Auth in Playwright with storageState (No Logging In Every Test) | Playwright Fixtures Explained: From Built-in to Custom | Test Isolation: Why Each Playwright Test Should Be Stateless