Resource library

QA How-To

Playwright test.describe.configure: Examples and Best Practices

Explore Playwright test.describe.configure examples for parallel, default, serial, retries, timeouts, nested scopes, hooks, fixtures, and CI-safe tests.

24 min read | 2,472 words

TL;DR

Use `test.describe.configure` recipes according to dependency: parallel for isolated tests, default for ordered independent tests, and serial only for an unavoidable chain. Add scoped retries or timeouts in the same options object, then verify hooks, data, retries, and reports under the actual CI worker count.

Key Takeaways

  • Make browser and server data independent before using parallel mode inside a file.
  • Use default mode as a local fullyParallel opt-out, not as a hidden sequencing mechanism.
  • Prefer one journey test with steps over a serial chain whenever stages share one outcome.
  • Configure retries locally only when the group has a documented exception to project policy.
  • Set group timeout from an observed service expectation and keep assertion timeout separate.
  • Use nested scopes sparingly to express medium-grained concurrency that reports can explain.
  • Test every execution configuration under concurrency and forced failure before relying on it in CI.

These Playwright test.describe.configure examples show how to apply parallel, default, and serial execution modes without turning runner configuration into hidden test logic. They also cover scoped retries, per-test timeout, nested groups, worker-safe data, and failure-path verification.

Each recipe is a starting pattern. Replace public URLs and sample selectors with your application, then validate the assumption that matters most: independence for parallel and default tests, or explicit dependency and repeatable side effects for a serial group.

TL;DR

Situation Configuration Design requirement
Independent tests in one large file { mode: 'parallel' } Unique data and worker-safe hooks
Local opt-out from fullyParallel { mode: 'default' } Tests still pass alone
Unavoidable dependent chain { mode: 'serial' } Repeatable group and meaningful skips
One group needs another retry policy { retries: 1 } Evidence retained and exception documented
Known slow operation in a group { timeout: 60_000 } Measured expectation, no hard waits
Sibling groups parallel, tests ordered Outer parallel, inner default Clear nested scope and isolated groups

All three properties can appear in one options object. Configuration applies to every test in the enclosing file or describe, even if the call is placed after declarations. Put it first in the scope for readability.

1. Build a Baseline for the Playwright test.describe.configure Examples

Start with explicit project defaults so local examples have a clear policy to override. The configuration below enables fully parallel execution, one CI retry, and a 30-second test timeout.

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

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  timeout: 30_000,
  expect: { timeout: 5_000 },
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 4 : undefined,
  use: {
    baseURL: 'https://playwright.dev',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

A local configure call can alter mode, test retries, or test timeout for its scope. It cannot change expect.timeout, project browser options, worker count, or the global time limit. That separation helps reviewers distinguish scheduling policy from environment configuration.

Use focused commands while adopting a recipe:

npx playwright test tests/example.spec.ts --project=chromium --workers=4
npx playwright test tests/example.spec.ts --project=chromium --workers=1
npx playwright test tests/example.spec.ts --project=chromium --repeat-each=3

The multiworker run exposes collisions. The one-worker run helps compare behavior without concurrency. Repetition is a diagnostic sample, not proof of determinism. Keep trace and report evidence, and also run individual test titles to prove that ordered cases do not need predecessors.

2. Example: Run Independent Tests in One File in Parallel

Use file-level parallel mode when a file contains many independent cases and splitting it would harm feature organization. Each case receives its own Playwright page fixture and may execute in a different worker process.

// tests/docs-navigation.spec.ts
import { test, expect } from '@playwright/test';

test.describe.configure({ mode: 'parallel' });

test('opens the installation guide', async ({ page }) => {
  await page.goto('/docs/intro');
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

test('opens the locator guide', async ({ page }) => {
  await page.goto('/docs/locators');
  await expect(page.getByRole('heading', { name: 'Locators' })).toBeVisible();
});

test('opens the assertions guide', async ({ page }) => {
  await page.goto('/docs/test-assertions');
  await expect(page.getByRole('heading', { name: 'Assertions' })).toBeVisible();
});

This works because cases read public pages and share no mutable state. In an application suite, authentication files can be read by several contexts, but one shared user can still collide through server-side actions. Create per-worker or per-test accounts for mutations. Use unique record names, avoid fixed download paths, and make cleanup tolerate partial failure.

Do not use a module counter to allocate test data. Separate workers have separate module memory, so two workers can both choose the same value. Use a central allocator, a worker-indexed pool, or a unique identifier. If tests update a common feature flag or global setting, keep them in a constrained project rather than forcing them into parallel mode.

3. Example: Parallel Sibling Groups With Ordered Tests Inside

A useful middle ground runs feature groups concurrently but keeps tests within each group in default mode. The outer file scope is parallel, and each nested describe overrides itself to default.

// tests/reference-areas.spec.ts
import { test, expect } from '@playwright/test';

test.describe.configure({ mode: 'parallel' });

test.describe('test runner reference', () => {
  test.describe.configure({ mode: 'default' });

  test('opens the test API', async ({ page }) => {
    await page.goto('/docs/api/class-test');
    await expect(page.getByRole('heading', { name: 'Playwright Test' })).toBeVisible();
  });

  test('opens the test config API', async ({ page }) => {
    await page.goto('/docs/api/class-testconfig');
    await expect(page.getByRole('heading', { name: 'TestConfig' })).toBeVisible();
  });
});

test.describe('library reference', () => {
  test.describe.configure({ mode: 'default' });

  test('opens the page API', async ({ page }) => {
    await page.goto('/docs/api/class-page');
    await expect(page.getByRole('heading', { name: 'Page' })).toBeVisible();
  });

  test('opens the locator API', async ({ page }) => {
    await page.goto('/docs/api/class-locator');
    await expect(page.getByRole('heading', { name: 'Locator' })).toBeVisible();
  });
});

The two describes may occupy different workers. Tests inside each describe run in declaration order and retry individually. They must still pass when filtered alone. This pattern controls concurrency granularity, not data flow.

Use it when sibling areas have isolated resources but an external constraint makes within-area concurrency undesirable. If dozens of files repeat the nesting, separate files with normal file-level parallelism may be simpler. A configuration hierarchy should reduce mental load, not display every runner feature at once.

4. Example: Opt Out of fullyParallel for a Rate-Limited Scope

A repository may run most cases fully parallel while one group calls an account-scoped service with a strict request budget. Configure that group as default to avoid concurrent tests within the group.

// tests/search-service.spec.ts
import { test, expect } from '@playwright/test';

test.describe('account-scoped search service', () => {
  test.describe.configure({ mode: 'default' });

  test('finds a guide by title', async ({ request }) => {
    const response = await request.get('/api/search?q=locators');
    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    expect(JSON.stringify(body)).toContain('Locators');
  });

  test('returns no result for an unknown phrase', async ({ request }) => {
    const response = await request.get('/api/search?q=unlikely-unknown-phrase');
    expect(response.ok()).toBeTruthy();
  });
});

This only orders tests in this group. Other files and projects may call the same service at the same time. If the limit applies globally, mode: 'default' is insufficient. Use a dedicated project with an intentional worker cap, separate API credentials, or a service-side test quota.

Do not rely on the first case warming a cache for the second. Default mode preserves order, but Playwright replaces workers after failures. Filtered execution can also start directly at the second case. Each test must establish its own prerequisites.

Document the external limit next to the exception, including who owns it and how to know when it can be removed. Otherwise default-mode islands tend to outlive the constraint that created them.

5. Example: Replace a Serial Chain With One Journey Test

Before using serial mode, ask whether the stages describe one user outcome. If they do, one test with test.step often produces clearer failure reporting and avoids skipped test cases.

// tests/order-journey.spec.ts
import { test, expect } from '@playwright/test';

test('member completes a documentation journey', async ({ page }) => {
  await test.step('open the home page', async () => {
    await page.goto('/');
    await expect(page).toHaveTitle(/Playwright/);
  });

  await test.step('open the installation guide', async () => {
    await page.goto('/docs/intro');
    await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
  });

  await test.step('open the writing tests guide', async () => {
    await page.goto('/docs/writing-tests');
    await expect(page.getByRole('main')).toContainText('Writing tests');
  });
});

One retry repeats the whole journey, which matches its single business outcome. The report still identifies the failed stage. Test selection cannot accidentally run only an approval stage without creation. Cleanup can surround the journey through a fixture.

Use separate tests when each outcome matters independently and can receive its own setup. API setup often makes that practical: create the required order directly, then test approval UI without depending on a prior create test. Review Playwright APIRequestContext patterns for that separation.

This example is deliberately not a configure call. Senior engineering includes recognizing when runner configuration is not the best fix.

6. Example: Configure an Unavoidable Serial Group

When a third-party workflow cannot be seeded at intermediate states and each stage must remain a separately reported case, serial mode can express the dependency honestly. Keep the exception narrow.

// tests/dependent-docs-flow.spec.ts
import { test, expect, type Page } from '@playwright/test';

test.describe('dependent navigation flow', () => {
  test.describe.configure({ mode: 'serial', retries: 1, timeout: 45_000 });

  let page: Page;

  test.beforeAll(async ({ browser }) => {
    page = await browser.newPage();
  });

  test.afterAll(async () => {
    await page.close();
  });

  test('starts on the home page', async () => {
    await page.goto('https://playwright.dev');
    await expect(page).toHaveTitle(/Playwright/);
  });

  test('continues to documentation', async () => {
    await page.goto('https://playwright.dev/docs/intro');
    await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
  });
});

If the first test fails, the second skips. If either fails and a retry remains, the entire group starts again in a fresh worker. beforeAll creates a new page for each attempt and afterAll closes it. Real side effects must be safe to repeat or cleaned before retry.

Never make a serial group large merely to save login time. Storage state and fixtures can provide authenticated isolation with better retry and parallel behavior. Never mix unrelated assertions into the chain because one early failure hides every later result. Add a comment explaining the external limitation, then periodically test whether API seeding or a product test hook can remove it.

7. Example: Override Retries for One Integration Group

A scoped retry override is useful when one group has a justified policy different from the project. For example, local development may use no retries, while a CI-only external status check permits one retry and captures a trace.

// tests/remote-content.spec.ts
import { test, expect } from '@playwright/test';

test.describe('remote content checks', () => {
  test.describe.configure({
    mode: 'parallel',
    retries: process.env.CI ? 1 : 0,
  });

  test('home responds', async ({ request }) => {
    const response = await request.get('https://playwright.dev');
    expect(response.ok()).toBeTruthy();
  });

  test('documentation responds', async ({ request }) => {
    const response = await request.get('https://playwright.dev/docs/intro');
    expect(response.ok()).toBeTruthy();
  });
});

Retries are per test because the mode is parallel, not serial. Playwright categorizes a test that fails first and passes on retry as flaky in the report. Preserve that signal. Do not flatten flaky and passed results into one green count.

A retry can help distinguish a transient dependency from a reproducible application defect, but it cannot explain either. Capture response status, correlation identifiers, traces where a browser is involved, and relevant service health without leaking secrets. Use testInfo.retry inside a fixture only when retry-specific cleanup is truly required. Avoid changing assertions on retry, because that makes attempts test different behavior.

8. Example: Give Export Tests a Scoped Timeout

Longer-running report generation can receive a larger test timeout without slowing failure detection for the whole suite. The timeout applies separately to every test in the describe.

// tests/export.spec.ts
import { test, expect } from '@playwright/test';

test.describe('downloadable exports', () => {
  test.describe.configure({ mode: 'parallel', timeout: 60_000 });

  test('downloads the getting started source', async ({ page }) => {
    await page.goto('/docs/intro');
    await expect(page.getByRole('main')).toBeVisible();
  });

  test('downloads the test runner reference source', async ({ page }) => {
    await page.goto('/docs/api/class-test');
    await expect(page.getByRole('main')).toContainText('Playwright Test');
  });
});

The sample pages are fast, but the structure applies to exports. For a download, create page.waitForEvent('download') before clicking the trigger. Wait on a product signal instead of adding waitForTimeout. A larger test budget will not change a five-second assertion timeout from project config, so pass { timeout: ... } to the specific web-first assertion only when the UI expectation itself is validly slower.

Parallel exports can overload a report service. Before combining longer timeouts with parallel mode, confirm service capacity and isolate output files. A slow system plus high concurrency can create the very timeout the override was meant to handle. Use metrics or timestamps to identify queueing, generation, transfer, and UI readiness separately.

9. Example: Pair Parallel Mode With Worker-Safe Fixtures

A fixture can allocate a unique namespace from worker information, letting parallel tests share a provisioning pattern without sharing mutable records.

// fixtures.ts
import { test as base, expect } from '@playwright/test';

type WorkerFixtures = {
  dataNamespace: string;
};

export const test = base.extend<{}, WorkerFixtures>({
  dataNamespace: [async ({}, use, workerInfo) => {
    const namespace = `pw-${workerInfo.workerIndex}`;
    await use(namespace);
  }, { scope: 'worker' }],
});

export { expect };
// tests/namespaced.spec.ts
import { test, expect } from '../fixtures';

test.describe.configure({ mode: 'parallel' });

test('uses an isolated search term', async ({ page, dataNamespace }) => {
  await page.goto('https://playwright.dev');
  expect(dataNamespace).toMatch(/^pw-/);
});

test('uses the same worker contract independently', async ({ dataNamespace }) => {
  expect(dataNamespace.length).toBeGreaterThan(3);
});

workerIndex identifies a worker process across the run. If the worker restarts after failure, a new worker index can appear. Provisioning and cleanup must handle abandoned or replacement namespaces. parallelIndex is useful when a stable slot across worker replacement is the correct pool key. Choose based on resource semantics rather than copying one identifier mechanically.

A worker fixture reduces repeated setup within one worker but does not provide a singleton across all parallel workers. For test-level mutation, add a test-specific identifier to the namespace or allocate one record per test. See Playwright fixture typing guide for worker and test generic parameters.

10. Example: Verify Failure Behavior, Not Just Happy Runs

Execution configuration is incomplete until the team observes a controlled failure. Create a temporary local branch or purpose-built runner test, force one case to fail, and inspect which tests run, skip, retry, and repeat. Do not merge deliberate product-test failures.

For default mode, verify that only the failing case retries and later independent tests still execute in a fresh worker if necessary. For parallel mode, verify siblings continue according to runner policy and that concurrent cleanup does not remove another test's data. For serial mode, verify later cases skip and earlier cases repeat on a group retry.

Useful commands include:

npx playwright test tests/target.spec.ts --workers=4 --reporter=list
npx playwright test tests/target.spec.ts --workers=1 --reporter=list
npx playwright test tests/target.spec.ts -g 'specific case'
npx playwright test tests/target.spec.ts --repeat-each=3

Review HTML or trace evidence after the run. Timing alone does not prove parallelism because fast cases can overlap invisibly and worker availability can vary. For diagnostics, log testInfo.workerIndex, testInfo.parallelIndex, retry number, and a safe data namespace. Remove noisy logs when the architecture is understood, or attach them only on failure through an automatic fixture.

CI should exercise the actual project, shard count, and worker cap. A local laptop and hosted runner can expose different resource and scheduling behavior. Docker for Playwright can reduce environment drift, but it does not solve shared application data.

11. Review Playwright test.describe.configure Examples as Policy

Before adopting any recipe, identify the policy it expresses and the evidence that supports it. Use this decision sequence:

  1. Can every test set up and assert its own outcome? If no, first redesign setup.
  2. Can cases use isolated accounts, records, files, ports, and service quotas? If yes, parallel may be appropriate.
  3. Does only local ordering matter under fullyParallel? Use default, but preserve independence.
  4. Do stages truly share one outcome? Prefer one test with steps.
  5. Is separate serial reporting mandatory? Use a small serial group and make retries safe.
  6. Does one scope have a measured timeout or retry exception? Configure only that scope and document why.
  7. Does the same exception appear repeatedly? Move it to project or top-level configuration.

Keep mode, retries, and timeout in one configure object per scope when possible. Place it first. Avoid nested modes that require a diagram unless they materially improve scheduling. Test the failure path after every change.

The best example is not the one with the highest concurrency. It is the one whose scheduling, state, failure, retry, and report behavior match the test's real contract.

Interview Questions and Answers

Q: Show a file-level parallel configuration.

Use test.describe.configure({ mode: 'parallel' }) at the top of the spec. Every test must own its browser state and server data. I run it with the CI worker count and verify hooks do not create shared mutable resources.

Q: How do you run sibling describes in parallel but tests inside each in order?

Set the outer file scope to parallel. Inside each describe callback, call test.describe.configure({ mode: 'default' }). Sibling groups can be scheduled separately, while cases inside a group remain ordered and independently retryable.

Q: How do you opt out of fullyParallel?

Inside the affected describe, configure mode: 'default'. The override is local to that scope. It does not limit other files and is not enough for a globally rate-limited service.

Q: When would you combine retries and timeout in one call?

When one bounded integration group has both a documented retry exception and a measured longer service expectation. I use one options object, preserve flaky reporting and traces, and remove the override when the underlying constraint is resolved.

Q: Why prefer test.step over a serial group?

When stages form one user outcome, one test has the correct pass or fail unit. Steps preserve stage-level diagnostics without creating skipped cases or allowing partial filtering. Setup, cleanup, and retry behavior are easier to reason about.

Q: How do worker fixtures support parallel mode?

They can allocate worker-specific accounts or namespaces and reuse expensive setup within that worker. They are not global singletons, and workers can restart after failure. Mutable records often still need test-level uniqueness.

Q: How do you prove an execution-mode change is safe?

I run each test alone, run the full scope with real concurrency, repeat it, and introduce a controlled failure. I inspect retries, skips, hook counts, data cleanup, worker identifiers, traces, and final report semantics.

Q: What does a scoped timeout not change?

It does not change the assertion timeout, action timeout, navigation timeout, worker count, or global run timeout. It is the timeout for each test in the configured scope. I adjust only the layer whose expected behavior is actually slow.

Common Mistakes

  • Copying a parallel recipe while tests share one account or record.
  • Using a module counter for unique data across separate worker processes.
  • Believing default mode limits other files that call the same rate-limited service.
  • Keeping ordered tests that fail when selected individually.
  • Building a long serial group to avoid proper authentication or data setup.
  • Forgetting that successful serial stages repeat after a later failure.
  • Changing assertions or behavior on retry instead of diagnosing the first failure.
  • Raising test timeout while leaving the actual slow assertion unexplained.
  • Assuming a worker fixture is shared by the whole run.
  • Adding several nested overrides when separate files would be clearer.
  • Validating concurrency only with elapsed time instead of worker and failure evidence.

Conclusion

Playwright test.describe.configure examples are useful only when their dependency assumptions match the application. Parallel mode needs isolation, default mode needs independent cases despite ordering, and serial mode needs an honest reason for dependency. Scoped retries and timeouts need observable, temporary justification.

Adopt the smallest recipe that expresses the constraint. Run each case alone, exercise real concurrency, force a failure, and inspect the report before the configuration becomes part of a release pipeline.

Interview Questions and Answers

Give a practical parallel describe example and its main risk.

I configure `mode: 'parallel'` for independent cases in one feature file. Each test uses its own context, account, records, and output path. The main risk is hidden shared state in the application or hooks, not the browser fixture itself.

How would you model medium-grained concurrency?

I use an outer parallel file scope and nested default describe scopes. Sibling groups can run in separate workers while cases within a group remain ordered. I use it only when it is clearer than splitting the groups into files.

Show how you would configure a CI-only retry.

Inside the bounded group I use `retries: process.env.CI ? 1 : 0`. Project traces remain enabled on first retry, and the report preserves flaky status. The code comment or linked issue explains the external reason.

How would you handle a slow export group?

I set a measured per-test timeout on that describe, wait on download or backend readiness signals, and keep assertion timeout separate. I also check whether parallel exports overload the service before enabling parallel mode.

What makes a serial example production-safe?

The dependency is documented, the group is small, setup and teardown work for every retry attempt, and side effects are idempotent or cleaned. I also prove later tests skip and the entire group restarts after a failure.

Why might workerIndex be insufficient for permanent test data?

Workers can restart after failures, so a replacement gets another worker index. Provisioning must handle abandoned resources, and test-level mutation may need a more specific identifier. The pool key should reflect resource lifetime.

What would cause you to promote a local configure setting to config?

If many files repeat the same mode, retry, or timeout rule for the same target, it is suite policy rather than a local exception. Moving it to a project or top level improves discoverability and reduces drift.

How do you review nested configure scopes?

I map the effective policy at file and group levels, then check which cases can share workers and which retry unit applies. If that explanation is difficult, I simplify the hierarchy or split files. Scheduling should remain obvious from the source.

Frequently Asked Questions

What is a Playwright parallel describe example?

Place `test.describe.configure({ mode: 'parallel' })` at file level or inside the target describe. Ensure tests use isolated data and do not rely on module variables or one shared beforeAll resource.

How do I configure retries for one Playwright describe?

Inside the describe callback, call `test.describe.configure({ retries: 1 })`. In default and parallel modes each test retries independently, while a serial group retries from the beginning.

How do I set a timeout for all tests in one describe?

Call `test.describe.configure({ timeout: 60_000 })` inside that describe. The value applies separately to each test and does not change the assertion timeout.

Can I combine mode, retries, and timeout?

Yes. Put all needed properties in one options object, such as `{ mode: 'parallel', retries: 1, timeout: 45_000 }`. Keep the exception bounded and document why it differs from project policy.

How do I keep tests ordered in a fully parallel project?

Configure the local scope with `{ mode: 'default' }`. Its tests run in order, but they should still be independently selectable and must not depend on earlier tests.

What is a safe alternative to Playwright serial tests?

Use one test with `test.step` when stages form one journey, or create prerequisite state through a fixture or API for independent tests. Both approaches improve filtering and failure diagnosis.

Can parallel Playwright tests use beforeAll?

Yes, but the hook can run for each parallel worker group rather than once for the file globally. Its setup must be concurrency-safe, idempotent, and independently cleaned.

How should I test a test.describe.configure change?

Run cases individually, then with the real CI worker count, repeat the scope, and force a controlled failure. Inspect hook execution, data collisions, retries, skips, cleanup, traces, and reports.

Related Guides