skip to content

In Playwright, how does `test.use()` in a spec file interact with a project's `use` options?

level: middleimportance: must knowfreq 62%

answer

  1. Declaration, not a mid-test call
  2. More specific than the project block
  3. Merged key by key, not replaced
  4. File scope or describe scope only
  5. Applies in every project running the file

basics

~20 s

test.use() merges over the project's use block key by key for the tests in its scope, so the file wins on the keys it names and inherits the rest. It applies in every project that runs the file.

solid answer

~40 s

`test.use({ ... })` re-declares option values for a scope inside a spec file, and it is more specific than the project, so its keys win. The merge is key by key: a file that sets only `viewport` keeps every other option the project supplied, including whatever a device preset spread in. Placed at file scope it covers every test in the file; placed inside a `test.describe()` block it covers only that block, and a describe-level call overrides a file-level one. The important consequence is reach: the override applies in **every** project that runs the file, so a file that pins an option opts itself out of that axis of the matrix. `test.use()` belongs at file or describe scope — it is not a mid-test switch you can call inside a test body.

code

typescript · 17 lines
typescript
import { test, expect } from '@playwright/test';

test.use({ viewport: { width: 1400, height: 900 } });

test('staff table fits the wide layout', async ({ page }) => {
  await page.goto('/admin/staff');
  await expect(page.getByRole('table', { name: 'Staff' })).toBeVisible();
});

test.describe('collapsed sidebar', () => {
  test.use({ viewport: { width: 900, height: 900 } });

  test('navigation collapses on a narrow window', async ({ page }) => {
    await page.goto('/admin/staff');
    await expect(page.getByRole('navigation')).toBeVisible();
  });
});

go deeper

for a junior

Remember that test.use goes at the top of a spec file or inside a describe block, and that it changes options for the tests in that scope.

for a middle

Explain the merge: file and describe declarations layer over the project's use key by key, so unnamed keys still come from the project and the config root.

for a senior

Point out the blast radius — a file-level override applies in every project running that file, which can quietly flatten an axis of the matrix while still paying for the extra runs.

for a principal

Frame it as a convention question: decide when a variation is allowed to live in spec files at all versus when it must be a project the team can select and triage.

## The specificity chain Playwright resolves each test's options by layering declarations from least to most specific, and the later layer wins on the keys it declares: 1. The top-level `use` block in `playwright.config.ts`. 2. The `use` block on the project running this test. 3. `test.use({ ... })` at the top level of the spec file. 4. `test.use({ ... })` inside the `test.describe()` block containing the test. Every step is a **key-by-key merge, not a replacement**. A file that calls `test.use({ viewport: { width: 1400, height: 900 } })` keeps every other option the project handed it — everything a device preset spread into `use`, the top-level defaults, all of it. Only `viewport` changes. ## Where the call is legal `test.use()` is a declaration, not a statement that executes during a test. It is legal at the top level of a spec file and inside a `test.describe()` callback; calling it inside a `test()` body is a configuration error, because by then the options that built the fixtures have already been resolved. ```ts import { test, expect } from '@playwright/test'; test.use({ viewport: { width: 1400, height: 900 } }); // whole file test.describe('collapsed sidebar', () => { test.use({ viewport: { width: 900, height: 900 } }); // this block only test('staff nav collapses', async ({ page }) => { await page.goto('/admin/staff'); await expect(page.getByRole('navigation')).toBeVisible(); }); }); ``` Nesting follows the same rule as the chain above: the innermost declaration wins for the tests it encloses, and siblings outside the block still see the file-level value. ## The reach that catches people out A project's `use` says "for these tests in this variant". A file's `test.use` says "for these tests **in every variant that runs this file**". That difference is the whole trap: - Pin an option in a spec file and it is pinned in all three browser projects, not just the one you had in mind when you wrote it. - A file that hard-codes an option effectively removes itself from that axis of the matrix while still costing you a run in every project. - Nothing in the report flags this: the test still appears under each project name, it just is not varying along the dimension the project name implies. If the variation genuinely belongs to a subset of specs, that is fine and idiomatic. If you wrote it to work around one flaky combination, it is a silent hole in the matrix. ## Project options versus file options | Aspect | `use` on a project | `test.use()` in a file | |---|---|---| | Scope | Every test the project runs | The file, or one describe block | | Reach across the matrix | One variant | Every project running that file | | Visible in the report | Yes, as the project name | No | | Selectable from the CLI | Yes, via `--project` | No | | Best for | An axis you want to run and triage separately | A requirement local to specific specs | ## Three things `test.use()` cannot do The API only re-declares option values, which rules out a family of things people try to reach for from inside a spec file: - **It cannot change which project runs the file.** Project selection happens in the config and on the command line; a spec cannot opt itself into or out of a variant. - **It cannot change file filters.** `testDir`, `testMatch` and `testIgnore` are resolved before any spec is loaded, so they are config-only keys — excluding a file from one project is a change to that project, not to the file. - **It cannot report itself.** Nothing in the reporter says a file overrode an option, so the only record of the variation is the source of the spec. ## Practical guidance - Set the shared baseline once at the config root, the axis on the project, and reach for `test.use()` only when specific specs genuinely need something different. - Keep file-level `test.use()` calls at the very top of the file where a reader meets them before the tests. - Prefer a describe block over a file-level call when only some tests need the change — the narrower scope documents the intent. - When a matrix result looks suspiciously identical across projects, grep the spec files for `test.use` before blaming the config.

  • A spec file calls `test.use()` and the suite runs under three browser projects — which projects see the override?
    All three. `test.use()` attaches to the file, and every project whose file filters match that file runs those tests with the override applied. It is not a way to change behaviour in one project only; that requires either a project-level `use` value or excluding the file from the project.
  • What happens if `test.use()` is called inside a test body rather than at file or describe level?
    Playwright rejects it as a configuration error rather than applying it. Options are resolved before the test starts, because fixtures such as the page are built from them; by the time the body runs there is nothing left to reconfigure.

saying these in an interview costs you the question

  • Thinks test.use replaces the whole project use block
  • Calls test.use inside a test body to switch options
  • Believes the project value wins over the file value
  • Assumes a file override only affects one project
  • Thinks a describe-level call leaks to the rest of the file