# Global Setup and Teardown

> **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 test` invocation, not once per test or per worker.

## Quick Reference

```
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 use `page`, `request`, etc.

## Patterns

### Pattern 1: Basic Global Setup and Teardown

**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).

```ts
// playwright.config.ts
import { defineConfig } from "@playwright/test"

export default defineConfig({
  globalSetup: "./tests/global-setup.ts",
  globalTeardown: "./tests/global-teardown.ts",
  testDir: "./tests",
})
```

```ts
// 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
```

```ts
// 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
```

### Pattern 2: Environment Health Check in Global Setup

**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.

```ts
// 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
```

### Pattern 3: Authentication State in Global Setup (Without Browser)

**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).

```ts
// 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
```

```ts
// 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",
      },
    },
  ],
})
```

### Pattern 4: Passing Data from Global Setup to Tests

**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):**

```ts
// 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
```

```ts
// 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):**

```ts
// 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
```

```ts
// 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)
}
```

```ts
// 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()
})
```

### Pattern 5: Global Setup with `storageState` (Browser-Based Auth)

**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.

```ts
// 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"],
    },
  ],
})
```

```ts
// 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 })
})
```

### Pattern 6: Global Setup for External Services

**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).

```ts
// 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
```

```ts
// 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
```

## Decision Guide

| 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-Patterns

| 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 |

## Troubleshooting

### Global setup runs but environment variables are not available in tests

**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:

```ts
// 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).

### Global setup fails with "Cannot find module"

**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:

```ts
// 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",
})
```

### Global teardown doesn't run after test failure

**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:

```ts
// 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
```

### Setup project runs every time, even when only running one test file

**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:

```bash
# Skip setup by running without dependencies
npx playwright test --project=chromium --no-deps tests/specific-test.spec.ts
```

### `storageState` file doesn't exist when tests start

**Cause**: 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:

```ts
// 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}`)
  }
})
```

## Related

- [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- per-test and per-worker fixtures (preferred over globalSetup for most cases)
- [core/configuration.md](../core/configuration.md) -- `globalSetup`, `globalTeardown`, `webServer` config
- [ci/projects-and-dependencies.md](projects-and-dependencies.md) -- setup projects with `dependencies`
- [core/authentication.md](../core/authentication.md) -- authentication patterns using setup projects
- [core/test-data-management.md](../core/test-data-management.md) -- seeding and managing test data
