Resource library

QA How-To

Playwright global setup: Examples and Best Practices

Use Playwright global setup examples for UI and API login, multiple roles, data seeding, cleanup, environment checks, CI, and secure test design patterns.

25 min read | 4,027 words

TL;DR

The best Playwright global setup examples use a setup project, save reusable authentication with storageState, seed idempotent reference data through the request fixture, and link cleanup through a teardown project. Choose each recipe based on state ownership, not convenience.

Key Takeaways

  • Start with a setup project and connect only the projects that need its output.
  • Use UI login when the login journey matters, and API login when the endpoint reliably sets browser-compatible cookies.
  • Create separate storage state files for each reusable role and protect them as credentials.
  • Seed only shared reference data globally, then create mutable records in isolated fixtures.
  • Write a run manifest when teardown needs explicit ownership of created resources.
  • Validate secrets and target environments before making setup calls.
  • Keep every recipe idempotent so retries and parallel CI invocations remain safe.

Playwright global setup examples should solve a specific prerequisite without turning the test runner into an invisible deployment system. The current default is a setup project that other projects depend on. It runs as normal Playwright Test code, so authentication, API calls, assertions, traces, retries, and reports remain visible.

The recipes below cover UI login, API login, multiple roles, test data, environment readiness, feature configuration, teardown, and the legacy globalSetup function. Each example includes the operational choices that determine whether it remains reliable in parallel CI.

Use the examples as patterns, not as a reason to share mutable state. A fast setup is valuable only when tests stay isolated and failures remain diagnosable.

TL;DR

Recipe Output Safe when
UI authentication Browser storage state file Account is reusable and tests isolate data
API authentication Storage state from API cookies Login endpoint sets browser-compatible cookies
Multiple roles One state file per role Role accounts do not conflict in parallel
Reference-data seed Deterministic API resource Tests treat it as read-only
Run-owned tenant Manifest plus external resource Teardown deletes only that run's resource
Health gate Assertion only Endpoint reports a meaningful ready state
Process globalSetup Environment value or Node resource Test fixtures and trace are not required

The project dependency pattern is the recommended starting point. A top-level globalSetup function remains supported for narrow runner-process work.

1. Playwright Global Setup Examples: A Complete Project Skeleton

A maintainable layout separates setup, teardown, ordinary specifications, and generated state:

playwright.config.ts
tests/
  setup/
    auth.setup.ts
    environment.setup.ts
    tenant.setup.ts
    tenant.teardown.ts
  specs/
    dashboard.spec.ts
playwright/
  .auth/
    user.json
    admin.json
  .run/
    tenant.json

Configure projects explicitly:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'environment setup',
      testMatch: /environment\.setup\.ts/,
    },
    {
      name: 'auth setup',
      testMatch: /auth\.setup\.ts/,
    },
    {
      name: 'chromium',
      testMatch: /specs\/.*\.spec\.ts/,
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['environment setup', 'auth setup'],
    },
  ],
});

Independent dependencies can run before chromium. If one fails, chromium does not start. When setup order matters, model the relationship rather than relying on array position. For example, auth setup can depend on environment setup.

Keep testMatch narrow. A setup file accidentally selected by the browser project may execute a second time with different fixtures or state. A dedicated setup directory plus explicit spec matching makes the graph understandable.

Generated auth and run directories should be excluded from version control. If a CI job uses a clean workspace, create directories during setup before writing files.

For the underlying lifecycle and decision criteria, read how to use Playwright global setup.

The generated directories have different lifetimes. Authentication state is reusable until the session expires, while a run manifest belongs to one invocation. Naming those lifetimes prevents a cleanup task from deleting credentials or a later run from consuming stale tenant data.

2. Example: Authenticate Through the User Interface

Use UI login when the sign-in journey itself, federated redirects, browser storage, or client-side initialization matters. The setup waits for a definitive authenticated state before saving storage.

// tests/setup/auth.setup.ts
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { test as setup, expect } from '@playwright/test';

const userState = path.join(
  process.cwd(),
  'playwright/.auth/user.json',
);

setup('authenticate reusable user through UI', async ({ page }) => {
  const email = process.env.E2E_USER_EMAIL;
  const password = process.env.E2E_USER_PASSWORD;

  if (!email || !password) {
    throw new Error('E2E user credentials are required');
  }

  await mkdir(path.dirname(userState), { recursive: true });

  await page.goto('/login');
  await page.getByLabel('Email address').fill(email);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  await expect(page).toHaveURL(/\/dashboard$/);
  await expect(page.getByRole('heading', { name: 'Dashboard' }))
    .toBeVisible();

  await page.context().storageState({ path: userState });
});

A matching browser project sets storageState to that path. Every test gets a new context initialized from the saved cookies and local storage, not the original setup page.

Do not save state immediately after clicking. Authentication may set cookies over several redirects. The final URL and an authenticated page assertion provide evidence that storage is ready.

Use a dedicated automation account with least privilege. Tests that modify account-level preferences can still collide even though browser contexts are isolated. In that case, authenticate one account per worker or provision test users through fixtures.

The state file may impersonate the user until cookies expire. Add playwright/.auth to .gitignore, protect CI artifacts, and do not upload the file with public reports.

Keep permission checks out of the login helper unless authentication specifically owns them. A focused authorization test should prove that each role sees the correct controls after state reuse.

3. Example: Authenticate Through an API Endpoint

API login can reduce setup time when a supported endpoint sets the same session cookie used by the browser. APIRequestContext stores cookies from responses and can export storage state.

// tests/setup/api-auth.setup.ts
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { test as setup, expect } from '@playwright/test';

const userState = path.join(
  process.cwd(),
  'playwright/.auth/user.json',
);

setup('authenticate reusable user through API', async ({ request }) => {
  const email = process.env.E2E_USER_EMAIL;
  const password = process.env.E2E_USER_PASSWORD;

  if (!email || !password) {
    throw new Error('E2E user credentials are required');
  }

  await mkdir(path.dirname(userState), { recursive: true });

  const response = await request.post('/api/session', {
    data: { email, password },
  });

  expect(response.ok()).toBeTruthy();
  expect(await response.json()).toMatchObject({
    authenticated: true,
  });

  await request.storageState({ path: userState });
});

This works when the response sets cookies for the application origin. If authentication returns only a token in JSON, saving API request state will not magically insert that token into localStorage. Either use a supported token bootstrap path, add required local storage through a browser context, or use the UI flow.

Validate the result with one focused browser test:

import { test, expect } from '@playwright/test';

test('stored state opens an authenticated dashboard', async ({ page }) => {
  await page.goto('/dashboard');

  await expect(page.getByRole('heading', { name: 'Dashboard' }))
    .toBeVisible();
  await expect(page).not.toHaveURL(/\/login/);
});

Do not re-test this assertion in every specification. Its purpose is to prove the authentication contract. The wider suite should assert the business behavior it owns.

API login is not inherently better than UI login. It is better when the endpoint is stable, supported for test use, and semantically equivalent for the tests that consume its state.

Inspect cookie domain, path, secure, and same-site behavior when state works locally but fails against another host. The saved cookie must be eligible for the browser URL used by dependent tests.

4. Example: Prepare Multiple Reusable Roles

Applications often need standard-user and administrator projects. Save one file per role, then configure each project with the appropriate state.

// tests/setup/roles.setup.ts
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
import { test as setup, expect } from '@playwright/test';

const authDir = path.join(process.cwd(), 'playwright/.auth');

const roles = [
  {
    name: 'user',
    email: process.env.E2E_USER_EMAIL,
    password: process.env.E2E_USER_PASSWORD,
  },
  {
    name: 'admin',
    email: process.env.E2E_ADMIN_EMAIL,
    password: process.env.E2E_ADMIN_PASSWORD,
  },
] as const;

setup('authenticate reusable roles', async ({ browser }) => {
  await mkdir(authDir, { recursive: true });

  for (const role of roles) {
    if (!role.email || !role.password) {
      throw new Error('Missing credentials for role: ' + role.name);
    }

    const context = await browser.newContext({
      baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    });
    const page = await context.newPage();

    await page.goto('/login');
    await page.getByLabel('Email address').fill(role.email);
    await page.getByLabel('Password').fill(role.password);
    await page.getByRole('button', { name: 'Sign in' }).click();

    await expect(page).toHaveURL(/\/dashboard$/);
    await page.context().storageState({
      path: path.join(authDir, role.name + '.json'),
    });

    await context.close();
  }
});

The browser fixture uses the setup project's configured browser. Each role gets an isolated context that is explicitly closed.

Configure consumers:

{
  name: 'user chromium',
  use: {
    ...devices['Desktop Chrome'],
    storageState: 'playwright/.auth/user.json',
  },
  dependencies: ['roles setup'],
},
{
  name: 'admin chromium',
  use: {
    ...devices['Desktop Chrome'],
    storageState: 'playwright/.auth/admin.json',
  },
  dependencies: ['roles setup'],
}

If one test needs both roles at the same time, create two contexts in that test from the two files. Close both contexts after the collaboration scenario. Do not switch one page between identities by rewriting cookies mid-test.

The loop authenticates roles sequentially, which produces simple logs and avoids stressing identity rate limits. If role preparation becomes slow, split roles into independent setup projects only when concurrent sign-in is safe and each consumer declares the correct dependency. Keep file names tied to roles, never to array positions, and validate the expected identity after loading each state.

Audit state files with a small identity smoke assertion so a mislabeled credential cannot give the admin project a standard-user session.

5. Example: Seed Read-Only Reference Data

Reference data such as a country list, plan catalog, or test feature definition can be seeded once if every test treats it as read-only.

// tests/setup/catalog.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('upsert E2E plan catalog', async ({ request }) => {
  const seedToken = process.env.E2E_SEED_TOKEN;

  if (!seedToken) {
    throw new Error('E2E_SEED_TOKEN is required');
  }

  const response = await request.put('/api/test-support/catalogs/e2e', {
    headers: {
      authorization: 'Bearer ' + seedToken,
    },
    data: {
      plans: [
        { code: 'E2E-FREE', label: 'Free', seats: 1 },
        { code: 'E2E-TEAM', label: 'Team', seats: 10 },
      ],
    },
  });

  expect(response.ok()).toBeTruthy();
  expect(await response.json()).toMatchObject({
    key: 'e2e',
    planCount: 2,
  });
});

PUT or an explicit upsert endpoint communicates repeatability better than blind POST insertion. A retry should converge on the same catalog.

Do not seed orders, invoices, messages, or drafts globally when tests update them. Those records need per-test ownership. A browser test can reference plan code E2E-TEAM but should create its own subscription.

Protect test-support routes from production use. Strong controls include environment allowlists, scoped short-lived credentials, and server-side rejection outside designated test environments. A URL name containing "test-support" is not security.

When reference data changes, assertions should describe the intentional contract. Avoid snapshots of a large seed response that change whenever the backend adds an unrelated field.

Version the seed contract with the application. If tests require a new plan field, update the support endpoint and assertions together. Silent server defaults can make an old seed appear successful while the UI receives incomplete data.

6. Example: Create a Run-Owned Tenant and Manifest

Some suites need an isolated tenant for the entire invocation. Create it with a unique run ID, write a manifest for consumers and cleanup, and never guess which tenant belongs to the run.

// tests/setup/tenant.setup.ts
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { test as setup, expect } from '@playwright/test';

const manifestPath = path.join(
  process.cwd(),
  'playwright/.run/tenant.json',
);

setup('create run-owned tenant', async ({ request }) => {
  const runId = process.env.E2E_RUN_ID ?? randomUUID();
  const response = await request.post('/api/test-support/tenants', {
    data: {
      externalKey: 'pw-' + runId,
      displayName: 'Playwright run ' + runId,
    },
  });

  expect(response.ok()).toBeTruthy();

  const tenant = await response.json() as {
    id: string;
    externalKey: string;
  };

  await mkdir(path.dirname(manifestPath), { recursive: true });
  await writeFile(
    manifestPath,
    JSON.stringify({ runId, tenant }, null, 2),
    'utf8',
  );
});

A test helper can read the manifest and build tenant-specific URLs. Do not store passwords or session cookies in this plain manifest.

In CI, E2E_RUN_ID should be unique per job or shard. randomUUID is a safe local fallback, but a pipeline identifier makes ownership easier to audit.

If the setup test retries after the tenant was created but before the manifest was written, a blind POST can leak the first tenant. Prefer an idempotency key or an upsert by externalKey. The example shows the data flow, but the endpoint contract must guarantee safe repetition for production-grade use.

For simple mutable records, a fixture remains better. A global tenant is justified only when tenant creation is costly and tests create isolated data within it.

7. Example: Clean Up With a Teardown Project

Link cleanup from the setup project:

// playwright.config.ts
{
  name: 'tenant setup',
  testMatch: /tenant\.setup\.ts/,
  teardown: 'tenant cleanup',
},
{
  name: 'tenant cleanup',
  testMatch: /tenant\.teardown\.ts/,
},
{
  name: 'tenant tests',
  testMatch: /tenant\/.*\.spec\.ts/,
  dependencies: ['tenant setup'],
}

Read the manifest and delete exactly that resource:

// tests/setup/tenant.teardown.ts
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { test as teardown, expect } from '@playwright/test';

const manifestPath = path.join(
  process.cwd(),
  'playwright/.run/tenant.json',
);

teardown('delete run-owned tenant', async ({ request }) => {
  const manifest = JSON.parse(
    await readFile(manifestPath, 'utf8'),
  ) as { tenant: { id: string } };

  const response = await request.delete(
    '/api/test-support/tenants/' +
      encodeURIComponent(manifest.tenant.id),
  );

  expect([204, 404]).toContain(response.status());
});

A 404 can be a successful idempotent cleanup result. Authentication and environment guards are omitted from the short snippet but must match the creation endpoint's controls.

Decide how to handle a missing manifest. In a strict pipeline, fail because ownership is unknown. In a best-effort janitor, log a nonsecret warning and let a scheduled cleanup job find expired resources by run metadata.

Project teardown runs after dependent projects. The --no-deps CLI option skips dependencies and teardowns, so it is not appropriate for a run that expects automatic cleanup.

Cleanup should have its own scoped credential and environment guard. If reading the manifest fails, report the exact path and run ID context without printing secret fields. A scheduled janitor can remove expired resources that survive canceled jobs, but it should use ownership metadata rather than a loose display-name search.

8. Playwright Global Setup Examples for Environment Readiness

A health endpoint should report more than process liveness. If tests need the database, queue, and a configured tenant service, the ready response should represent those dependencies.

// tests/setup/environment.setup.ts
import { test as setup, expect } from '@playwright/test';

setup('required services are ready', async ({ request }) => {
  await expect.poll(
    async () => {
      const response = await request.get('/api/ready');

      if (!response.ok()) {
        return 'http-' + response.status();
      }

      const body = await response.json() as {
        status?: string;
      };

      return body.status;
    },
    {
      message: 'Waiting for the E2E environment to become ready',
      timeout: 60_000,
      intervals: [500, 1_000, 2_000, 5_000],
    },
  ).toBe('ready');
});

expect.poll is a current Playwright Test assertion for polling a function. It is more expressive than a manual while loop or waitForTimeout. The intervals are explicit, and the failure appears as an assertion.

Keep the maximum wait aligned with deployment expectations. A ten-minute setup poll can hide a failed rollout and waste every CI worker. The deployment stage should perform its own rollout checks, while Playwright verifies the test-facing contract.

Avoid broad network-idle waits for readiness. A service can remain busy with analytics or streaming traffic while being ready for tests. Poll the specific state that matters.

A readiness check should never mutate the target. If it must repair configuration, separate that operation into a clearly named setup test with stronger credentials and ownership controls.

9. Example: Use globalSetup and Return Teardown

A top-level globalSetup function is still useful for a small Node-level session that tests access through process.env.

// tests/process-global-setup.ts
import type { FullConfig } from '@playwright/test';

export default async function globalSetup(
  config: FullConfig,
): Promise<() => Promise<void>> {
  const baseURL = config.projects[0]?.use.baseURL;

  if (typeof baseURL !== 'string') {
    throw new Error('A string baseURL is required');
  }

  const create = await fetch(new URL('/api/test-runs', baseURL), {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization:
        'Bearer ' + (process.env.E2E_SEED_TOKEN ?? ''),
    },
    body: JSON.stringify({ runner: 'playwright' }),
  });

  if (!create.ok) {
    throw new Error('Could not create test run: ' + create.status);
  }

  const data = await create.json() as { id: string };
  process.env.E2E_TEST_RUN_ID = data.id;

  return async () => {
    const remove = await fetch(
      new URL('/api/test-runs/' + encodeURIComponent(data.id), baseURL),
      {
        method: 'DELETE',
        headers: {
          authorization:
            'Bearer ' + (process.env.E2E_SEED_TOKEN ?? ''),
        },
      },
    );

    if (!remove.ok && remove.status !== 404) {
      throw new Error('Could not delete test run: ' + remove.status);
    }
  };
}

Configure it with globalSetup: require.resolve('./tests/process-global-setup'). This code has no page or request fixture, no setup trace, and no normal test report entry. That tradeoff is acceptable only when process-level behavior is the reason for choosing it.

The returned function retains data.id through closure. Tests receive the ID through process.env in the same runner invocation.

Wrap resource creation and cleanup in careful error handling. If creation succeeds but response parsing fails, preserve enough nonsecret context for a janitor to find the resource. If teardown fails, surface that failure instead of silently leaking a paid tenant or privileged session. Project dependencies remain preferable when trace and fixture support would make these operations easier to diagnose.

Keep the returned cleanup focused on resources created by that invocation, and make its not-found response an intentional documented outcome.

10. Validate Secrets and Block Unsafe Targets

Fail before navigation or API calls when required inputs are missing. One helper keeps error language consistent:

export function requiredEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error('Missing required environment variable: ' + name);
  }

  return value;
}

Guard destructive setup against an unexpected host:

export function assertSafeTarget(baseURL: string): void {
  const host = new URL(baseURL).hostname;
  const allowed = new Set([
    '127.0.0.1',
    'localhost',
    'qa.example.test',
    'staging.example.test',
  ]);

  if (!allowed.has(host)) {
    throw new Error('Refusing E2E setup for host: ' + host);
  }
}

Use both server-side and client-side protection. A local allowlist prevents common operator mistakes, while the service must still reject test-support credentials in production.

Do not print secret values while checking them. Do not attach storage state, full environment objects, authorization headers, or login response bodies to reports. Reports are often retained longer and shared more broadly than CI secrets.

Use short-lived, narrowly scoped tokens for seeding and cleanup. The browser account should not automatically possess infrastructure permissions. Separate credentials reduce the damage from a leaked trace or application bug.

Validate the resolved baseURL, not only the raw environment string, because configuration layers can override it. Apply the same policy to cleanup. A teardown pointed at a different host from setup cannot prove ownership and should stop before sending a destructive request.

11. Make Setup Reliable in CI, Shards, and UI Mode

Every separate Playwright invocation can perform setup. A matrix, shard, or retry at the job level creates another invocation. Choose unique resource ownership and idempotency accordingly.

When tests are filtered by file, grep, shard, or project, Playwright includes project dependencies for the selected primary tests. Passing --no-deps disables dependencies and teardown. Use that flag for deliberate diagnosis, not as a routine speed optimization.

UI mode does not automatically run the setup project in the same way as a normal full command. When authentication state expires, enable and run the setup project, then return to the dependent project. Document this workflow for developers.

A useful CI command remains simple:

npx playwright test --project="user chromium"

The dependency graph supplies role setup. Avoid separate shell scripts that run setup first and tests second unless the pipeline needs to persist an artifact between distinct jobs. Two commands can drift in flags and environment, while project dependencies preserve the relationship in configuration.

For CI artifacts and browser installation, see GitHub Actions for Playwright. For worker-owned test data, use typed Playwright fixtures rather than growing the global graph.

12. Review Setup With an Operational Checklist

Before merging a recipe, verify:

  • The project name and testMatch select only intended setup files.
  • Every dependent project declares the exact prerequisite it needs.
  • Authentication waits for final cookies or storage.
  • Generated state directories exist and are ignored by Git.
  • Shared accounts do not own mutable test data.
  • Seed endpoints are idempotent and protected.
  • A unique run ID identifies created external resources.
  • Teardown deletes only resources in its manifest.
  • Retries cannot multiply records.
  • Errors name the failed prerequisite without revealing secrets.
  • Traces and reports are enabled where they add diagnostic value.
  • Local, UI mode, shard, and matrix behavior are documented.
  • Destructive setup rejects production targets.
  • Per-test fixtures still own ordinary mutable records.

A setup project should be small enough to run alone and understand from its report. Split unrelated health, authentication, and tenant operations so a failure says what broke.

Do not optimize solely for fewer API calls. One shared account or record can make a suite fast in serial execution and unreliable under load. Isolation, ownership, and diagnostic clarity are the primary design criteria.

Interview Questions and Answers

Q: Why are setup projects preferred for Playwright global setup examples?

They use the regular Playwright Test runner, so fixtures, assertions, traces, retries, reporters, and configuration work normally. Dependent projects wait for them and are skipped when setup fails. This makes prerequisites observable rather than hidden in a process hook.

Q: When is API authentication equivalent to UI authentication?

It is equivalent only when the supported login endpoint creates the same cookie or storage contract that browser sessions use. APIRequestContext can save response cookies through storageState. A token returned only in JSON requires a separate supported bootstrap mechanism.

Q: How would you prepare two user roles?

I save one storage state file per role, preferably from isolated browser contexts or separate setup tests. Consumer projects reference the matching file. A collaboration test can create two contexts from those files and close both afterward.

Q: What data is appropriate for global seeding?

Stable reference data that all tests read but do not mutate is appropriate. Orders, carts, drafts, messages, and other mutable records should be created per test or worker. This prevents parallel tests from overwriting shared state.

Q: How do you make tenant setup safe to retry?

I use a unique run ID and an idempotency key or upsert contract. I write the created tenant ID to a manifest only after validating the response. Cleanup reads that manifest and deletes only the owned resource.

Q: How do you wait for environment readiness?

I poll a meaningful readiness endpoint with expect.poll and a bounded timeout. The endpoint should represent dependencies that tests need, not just process liveness. I fail with a clear message instead of adding a fixed sleep.

Q: What security risks exist in global setup?

Storage state can impersonate users, seed tokens can mutate environments, and reports can leak diagnostic data. I use least-privilege secrets, gitignore generated state, block production targets, and avoid logging cookies, tokens, or full environment values.

Q: How does setup behave with shards?

Filtering and sharding select primary tests, then their project dependencies run for that Playwright invocation. Separate shards are separate invocations, so setup may repeat. Resources must be idempotent or uniquely owned per shard.

Common Mistakes

  • Copying a setup example without defining who owns the created state.
  • Using API login when the endpoint does not place authentication into cookies or compatible storage.
  • Saving all roles into one overwritten storage state file.
  • Sharing one mutable account across tests that update preferences or permissions.
  • Blindly inserting seed records on every retry.
  • Generating a unique resource without recording it for cleanup.
  • Deleting resources by a broad prefix instead of an exact run manifest.
  • Returning from UI login before final cookies are set.
  • Assuming a setup project's process.env mutation reaches all test workers.
  • Running filtered tests with --no-deps and expecting prerequisites.
  • Uploading auth state with traces or reports.
  • Polling for readiness with long fixed sleeps.
  • Putting application deployment and database migration inside an opaque setup test.
  • Allowing destructive test-support calls against production.
  • Reusing one giant setup project for unrelated suites.

Conclusion

These Playwright global setup examples cover the most useful production patterns: UI and API authentication, multiple roles, read-only reference data, run-owned resources, readiness checks, project teardown, and a narrow process-level hook. The recommended setup project keeps each prerequisite visible and connected to only the tests that require it.

Choose a recipe by state lifetime and ownership. Protect authentication files, make server operations repeatable, use manifests for cleanup, and keep mutable records in fixtures. A setup that can safely run twice is much more valuable than one that happens to run once locally.

Interview Questions and Answers

Why do you prefer project dependencies for global setup?

They make setup a normal test project with fixtures, assertions, traces, retries, and reporting. The dependency graph is explicit, and failed setup prevents dependent tests from running. That is easier to operate than a hidden process hook.

How would you choose between UI and API login?

I use UI login when redirects, browser initialization, or the login journey matters. I use API login when a supported endpoint creates the same browser-compatible session cookies. I verify the saved state with a focused browser test.

How do you support multiple roles?

I create one protected storage state file per role using isolated contexts or setup tests. Projects load only the state they require. When one test needs both roles, it creates two contexts and closes both.

What makes data seeding safe for parallel execution?

Global seeds are read-only and idempotent. Mutable records get unique per-test or per-worker ownership. Run-wide resources use unique IDs, manifests, and exact cleanup.

How would you implement cleanup after dependent projects?

I connect a teardown project with the setup project's teardown property. Setup records exact resource IDs, and cleanup deletes only those IDs. Deletion accepts an already-absent resource when that is a valid idempotent outcome.

What is a reliable environment readiness check?

It polls a documented endpoint that represents the dependencies tests actually need. I use expect.poll with bounded time and useful intervals. I avoid fixed sleeps and do not let the test stage mask a failed deployment indefinitely.

How do you protect secrets used by setup?

I load them from the CI secret store, validate presence without printing values, and use least-privilege scopes. I gitignore storage state and prevent it from entering public artifacts. Destructive endpoints also reject production targets server-side.

Why can a setup example fail only in shards?

Separate shards can repeat setup and operate concurrently. A design that assumes one shared mutable account, file, or database row can collide. I use shard-aware run IDs, idempotent endpoints, and isolated mutable data.

Frequently Asked Questions

What is a good Playwright global setup example for login?

Create a setup project, sign in through UI or a supported API, wait for authentication to complete, and save storageState to a gitignored file. Configure dependent browser projects to load that file.

Can Playwright authenticate through an API in global setup?

Yes. APIRequestContext stores cookies set by responses and can write them with request.storageState(). This works when the API login creates browser-compatible cookies, not when it only returns an unused JSON token.

How do I create multiple authentication states in Playwright?

Authenticate each role in an isolated browser context or setup test and save a separate state file. Point user and admin projects to their respective files.

How do I seed a database before Playwright tests?

Prefer a protected test-support API through the request fixture and use deterministic upsert semantics. Seed shared read-only reference data globally, while mutable records should be created per test.

How do I clean up data after Playwright tests?

Link a teardown project from the setup project's teardown property. Record exact run-owned resource IDs in a manifest and make deletion idempotent.

Can Playwright global setup wait for a service?

Yes. A setup project can use expect.poll against a meaningful readiness endpoint with explicit intervals and a bounded timeout. Avoid fixed sleeps and generic network-idle waits.

Does global setup run for every Playwright shard?

Each shard is a separate runner invocation, and dependencies for selected primary tests run in that invocation. Design setup to repeat safely or use unique shard-specific ownership.

Where should Playwright auth files be stored?

Use a generated directory such as playwright/.auth and add it to .gitignore. Treat files as credentials, avoid public artifacts, and regenerate them when sessions expire.

Related Guides