Files
FancyWordle/.agents/skills/playwright-best-practices/core/global-setup.md
T
2026-05-16 13:57:02 -04:00

13 KiB

Global Setup & Teardown

Table of Contents

  1. Global Setup
  2. Global Teardown
  3. Database Patterns
  4. Environment Provisioning
  5. Setup Projects vs Global Setup
  6. Parallel Execution Caveats

Global Setup

Basic Global Setup

// global-setup.ts
import { FullConfig } from "@playwright/test";

async function globalSetup(config: FullConfig) {
  console.log("Running global setup...");
  // Perform one-time setup: start services, run migrations, etc.
}

export default globalSetup;

Configure Global Setup

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

export default defineConfig({
  globalSetup: require.resolve("./global-setup"),
  globalTeardown: require.resolve("./global-teardown"),
});

Authentication in Global Setup: For authentication patterns using storage state in global setup, see fixtures-hooks.md. Setup projects are generally preferred for authentication as they provide access to Playwright fixtures.

Global Setup with Return Value

// global-setup.ts
async function globalSetup(config: FullConfig): Promise<() => Promise<void>> {
  const server = await startTestServer();

  // Return cleanup function (alternative to globalTeardown)
  return async () => {
    await server.stop();
  };
}

export default globalSetup;

Access Config in Global Setup

// global-setup.ts
import { FullConfig } from "@playwright/test";

async function globalSetup(config: FullConfig) {
  const { baseURL } = config.projects[0].use;
  console.log(`Setting up for ${baseURL}`);

  // Access custom config
  const workers = config.workers;
  const timeout = config.timeout;

  // Access environment
  const isCI = !!process.env.CI;
}

export default globalSetup;

Global Teardown

Basic Global Teardown

// global-teardown.ts
import { FullConfig } from "@playwright/test";
import fs from "fs";

async function globalTeardown(config: FullConfig) {
  console.log("Running global teardown...");

  // Clean up auth files
  if (fs.existsSync(".auth")) {
    fs.rmSync(".auth", { recursive: true });
  }

  // Clean up test data
  await cleanupTestDatabase();

  // Stop services
  await stopTestServices();
}

export default globalTeardown;

Conditional Teardown

// global-teardown.ts
async function globalTeardown(config: FullConfig) {
  // Skip cleanup in CI (containers are discarded anyway)
  if (process.env.CI) {
    console.log("Skipping teardown in CI");
    return;
  }

  // Local cleanup
  await cleanupLocalTestData();
}

export default globalTeardown;

Database Patterns

This section covers one-time database setup (migrations, snapshots, per-worker databases). For related topics:

Database Migration in Setup

// global-setup.ts
import { execSync } from "child_process";

async function globalSetup() {
  console.log("Running database migrations...");

  // Run migrations
  execSync("npx prisma migrate deploy", { stdio: "inherit" });

  // Seed test data
  execSync("npx prisma db seed", { stdio: "inherit" });
}

export default globalSetup;

Database Snapshot Pattern

// global-setup.ts
import { execSync } from "child_process";
import fs from "fs";

const SNAPSHOT_PATH = "./test-db-snapshot.sql";

async function globalSetup() {
  // Check if snapshot exists
  if (fs.existsSync(SNAPSHOT_PATH)) {
    console.log("Restoring database from snapshot...");
    execSync(`psql $DATABASE_URL < ${SNAPSHOT_PATH}`, { stdio: "inherit" });
    return;
  }

  // First run: migrate and create snapshot
  console.log("Creating database snapshot...");
  execSync("npx prisma migrate deploy", { stdio: "inherit" });
  execSync("npx prisma db seed", { stdio: "inherit" });
  execSync(`pg_dump $DATABASE_URL > ${SNAPSHOT_PATH}`, { stdio: "inherit" });
}

export default globalSetup;

Test Database per Worker

// global-setup.ts
async function globalSetup(config: FullConfig) {
  const workerCount = config.workers || 1;

  // Create a database for each worker
  for (let i = 0; i < workerCount; i++) {
    const dbName = `test_db_worker_${i}`;
    await createDatabase(dbName);
    await runMigrations(dbName);
    await seedDatabase(dbName);
  }
}

// global-teardown.ts
async function globalTeardown(config: FullConfig) {
  const workerCount = config.workers || 1;

  for (let i = 0; i < workerCount; i++) {
    await dropDatabase(`test_db_worker_${i}`);
  }
}

Environment Provisioning

Start Services in Setup

// global-setup.ts
import { execSync, spawn } from "child_process";

let serverProcess: any;

async function globalSetup() {
  // Start backend server
  serverProcess = spawn("npm", ["run", "start:test"], {
    stdio: "pipe",
    detached: true,
  });

  // Wait for server to be ready
  await waitForServer("http://localhost:3000/health", 30000);

  // Store PID for teardown
  process.env.SERVER_PID = serverProcess.pid.toString();
}

async function waitForServer(url: string, timeout: number) {
  const start = Date.now();

  while (Date.now() - start < timeout) {
    try {
      const response = await fetch(url);
      if (response.ok) return;
    } catch {
      // Server not ready yet
    }
    await new Promise((r) => setTimeout(r, 1000));
  }

  throw new Error(`Server did not start within ${timeout}ms`);
}

export default globalSetup;

Docker Compose Setup

// global-setup.ts
import { execSync } from "child_process";

async function globalSetup() {
  console.log("Starting Docker services...");

  execSync("docker-compose -f docker-compose.test.yml up -d", {
    stdio: "inherit",
  });

  // Wait for services to be healthy
  execSync("docker-compose -f docker-compose.test.yml exec -T db pg_isready", {
    stdio: "inherit",
  });
}

export default globalSetup;
// global-teardown.ts
import { execSync } from "child_process";

async function globalTeardown() {
  console.log("Stopping Docker services...");

  execSync("docker-compose -f docker-compose.test.yml down -v", {
    stdio: "inherit",
  });
}

export default globalTeardown;

Environment Variables Setup

// global-setup.ts
import dotenv from "dotenv";
import path from "path";

async function globalSetup() {
  // Load test-specific environment
  const envFile = process.env.CI ? ".env.ci" : ".env.test";
  dotenv.config({ path: path.resolve(process.cwd(), envFile) });

  // Validate required variables
  const required = ["DATABASE_URL", "API_KEY", "TEST_EMAIL"];
  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`Missing required environment variable: ${key}`);
    }
  }
}

export default globalSetup;

Setup Projects vs Global Setup

When to Use Each

Use Global Setup Use Setup Projects
One-time setup (migrations, services) Per-project setup (auth states)
No access to Playwright fixtures Need page, request fixtures
Runs once before all projects Can run per-project or have dependencies
Shared across all workers Can be parallelized

Setup Project Pattern

// playwright.config.ts
export default defineConfig({
  projects: [
    // Setup project
    {
      name: "setup",
      testMatch: /.*\.setup\.ts/,
    },
    // Test projects depend on setup
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
      dependencies: ["setup"],
    },
    {
      name: "firefox",
      use: { ...devices["Desktop Firefox"] },
      dependencies: ["setup"],
    },
  ],
});

For complete authentication setup patterns, see fixtures-hooks.md.

Combining Both

// playwright.config.ts
export default defineConfig({
  // Global: Start services, run migrations
  globalSetup: require.resolve("./global-setup"),
  globalTeardown: require.resolve("./global-teardown"),

  projects: [
    // Setup project: Create auth states
    { name: "setup", testMatch: /.*\.setup\.ts/ },
    {
      name: "chromium",
      use: {
        ...devices["Desktop Chrome"],
        storageState: ".auth/user.json",
      },
      dependencies: ["setup"],
    },
  ],
});

Parallel Execution Caveats

Understanding Global Setup Execution

┌─────────────────────────────────────────────────────────────┐
│  globalSetup runs ONCE                                      │
│  ↓                                                          │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│  │ Worker 1│  │ Worker 2│  │ Worker 3│  │ Worker 4│        │
│  │ tests   │  │ tests   │  │ tests   │  │ tests   │        │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘        │
│  ↓                                                          │
│  globalTeardown runs ONCE                                   │
└─────────────────────────────────────────────────────────────┘

Key implications:

  • Global setup has no access to Playwright fixtures (page, request, context)
  • State created in global setup is shared across all workers
  • If tests modify shared state, they may conflict with parallel workers
  • Global setup cannot react to individual test needs

When to Prefer Worker-Scoped Fixtures

Use worker-scoped fixtures instead of globalSetup when:

Scenario Why Fixtures Are Better
Each worker needs isolated resources Fixtures can create per-worker databases, servers
Setup needs Playwright APIs Fixtures have access to page, request, browser
Setup depends on test configuration Fixtures receive test context and options
Resources need cleanup per worker Worker fixtures auto-cleanup when worker exits

Common Parallel Pitfall

// ❌ BAD: Global setup creates ONE user, all workers fight over it
async function globalSetup() {
  await createUser({ email: "test@example.com" }); // Shared!
}

// ✅ GOOD: Each worker gets its own user via worker-scoped fixture
// Uses workerInfo.workerIndex to create unique data per worker

For worker-scoped fixture patterns (per-worker databases, unique test data, workerIndex isolation), see fixtures-hooks.md.

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Heavy setup in globalSetup Slow test startup Use setup projects for parallelizable work
Not cleaning up in teardown Leaks resources, flaky CI Always clean up or use containers
Hardcoded URLs in setup Breaks in different environments Use config.projects[0].use.baseURL
No timeout on service wait Hangs forever if service fails Add timeout with clear error
Shared mutable state Race conditions in parallel Use worker-scoped fixtures for isolation
Global setup for per-test data Tests conflict Use test-scoped fixtures