---
url: https://huldoser.github.io/test-inventory/frameworks/playwright.md
description: >-
  Scan Playwright tests without running them, interpret the inventory, and check
  Playwright syntax and limitations.
---

# Playwright

This guide covers **Playwright 1.42 or newer**. You need Node.js 22.12 or newer to run test-inventory and an
existing JavaScript or TypeScript test suite. The scanner reads your source without executing your tests or starting
a browser. These are minimum supported versions; [CI comparisons](/guide/runners#checked-in-ci) list tested versions.

[Choose another framework](/frameworks/) · [Use an AI client](/guide/agents)

## Install

```sh
npm install --save-dev test-inventory
```

## Scan your suite

Run from the folder containing your project's `package.json`:

```sh
npx test-inventory --framework playwright --pretty --output inventory.json
```

To select files explicitly, use quoted globs. This example matches the `trading-web` fixture used below;
adjust the globs to your suite's layout:

```sh
npx test-inventory "tests/**/*.spec.ts" --framework playwright --output inventory.json
```

The command replaces `inventory.json` once the new output is complete. It does not change your test files.

## Check the result

Print the counts and diagnostics from the saved file:

```sh
node -e "const i = require('./inventory.json'); console.log(i.summary); console.log(i.diagnostics)"
```

For matching tests, `summary.fileCount` and `summary.testCount` should be greater than zero. Review diagnostics
even when the command exits with 0: errors can mean tests are missing. If the result is empty or unexpected,
use [troubleshooting](/guide/troubleshooting).

The inventory describes source declarations, not execution results. Conditional skips are not proof that a run
skipped the test. Loops and tables are one record by default; supported cases can be expanded with `--expand-cases`.
Expected-to-fail tests execute and check for failure; they are not disabled tests.

Tests created by unreadable helpers can be missing without a diagnostic. A dependency hash does not prove complete
coverage of runtime dependencies. Read the [shared limits](/guide/limits) before using scans to make CI decisions.

## Find skipped tests

```sh
npx test-inventory --framework playwright --state skip --state fixme --fields fullName,state,condition,reason --pretty
```

This uses the same file selection as the first scan. If you supplied globs, `--root` or `--framework-version`,
repeat those options here.

This selects source declarations marked skip or fixme; it is not a report of a test run.
See [query recipes](/guide/recipes) for other inventory queries.

## Example

`trading-web` tests the trading app's UI with page objects and fixtures. Its specs import `test` from a fixtures file
that extends Playwright's:

```ts excerpt
// fixtures/index.ts
export const test = base.extend<TradingFixtures>({
  signedInPage,
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  // ...
});

export { expect } from '@playwright/test';
```

The scan follows that import, sees `base.extend()` on Playwright's `test`, and reads the specs that use
it like any other:

```ts excerpt
// tests/orders/order-lifecycle.spec.ts
import { test, expect } from '../../fixtures/index';

// One order moves through every status, so each step depends on the one before.
test.describe('order lifecycle', { tag: '@orders' }, () => {
  test.describe.configure({ mode: 'serial' });

  test('places a limit order for 100 NVDA', async ({ orderTicket, blotter }) => {
    // ...
  test('shows a partial fill', async ({ blotter }) => {
    await expect(blotter.status('NVDA')).toHaveText(/Partially filled \(\d+\/100\)/);
  });
```

```sh
npx test-inventory "tests/**/*.spec.ts" --framework playwright --output inventory.json
```

The describe's record has the mode set by `describe.configure()`, and its tests inherit the mode and the tag:

```json
{
  "id": "2b2853e11e8f8699",
  "relativeFilePath": "tests/orders/order-lifecycle.spec.ts",
  "lineStart": 4,
  "title": "order lifecycle",
  "isSerial": true,
  "tags": ["@orders"]
}
```

```json
{
  "fullName": "order lifecycle > shows a partial fill",
  "suiteId": "2b2853e11e8f8699",
  "isSerial": true,
  "tags": ["@orders"]
}
```

### Gherkin features with playwright-bdd

[playwright-bdd](https://github.com/vitalets/playwright-bdd) generates Playwright tests from `.feature` files. Generate
the files first, then scan them:

```sh
npx bddgen
npx test-inventory ".features-gen/**/*.spec.js" --framework playwright --output inventory.json
```

Records point at the generated files. The generated comments identify their source feature files.

## Playwright projects and declaration counts

`trading-web` runs the same tests in Chromium and WebKit. Its config defines two projects:

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

export default defineConfig({
  // ...
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});
```

Scan its declarations once:

```sh
npx test-inventory "tests/**/*.spec.ts" --framework playwright --pretty
```

The output contains one record for this test, with both project names. This is an excerpt of that record:

```json
{
  "fullName": "order lifecycle > shows a partial fill",
  "projects": ["chromium", "webkit"]
}
```

Running the test in two browsers does not double the inventory's `testCount`. The inventory counts source
declarations; a runner report can contain a separate result for each project. A test's `projects` lists the
projects whose file-selection settings include it. Read [project configuration](/frameworks/playwright#configuration-and-limits)
for the settings the scanner can resolve.

## Configuration and limits

The scan finds `playwright.config.*` in the test file's folder and its ancestors. A config elsewhere that
reaches the file, such as `apps/web/playwright.config.ts` with `testDir: '../../e2e'`, is not found from it.
It reads file and project selection, run settings, modes and `forbidOnly`. Other settings, such as `workers`
and config `use`, are not interpreted as test record settings.

A config chosen with the runner's `--config` is not inferred from the files. A config built by a call the scan
cannot follow, such as `getPlaywrightConfig({ startCommand: 'pnpm start' })`, cannot be worked out.

For a project's `grep`, the test's text contains its project, file, config `tag`, describe and test titles and tags.
Option tags are placed at the end; Playwright places a describe's tags after its title. Only a pattern depending
on that order can distinguish them. A test excluded by `grep` remains in the inventory, with that project missing
from its `projects`.

Calls to `testInfo.setTimeout()` and `testInfo.slow()` are not read. Nor are calls in an imported function passed
to a hook by name, such as `test.beforeEach(slowOnCI)`.

A loop that registers the same title path twice gives a `duplicate-title` error and `notLoaded` tests, as
Playwright refuses the file.

### States set in helper functions

In Playwright, a `test.skip()`, `test.fixme()`, `test.fail()` or `test.describe.configure()` call inside a plain
function applies to the tests, hooks and describes that call that function by name, also through helpers that call
other helpers. A function called from the file or a describe may also be imported from another file:

```ts
function skipWithoutMarketData() {
  test.skip(!process.env.MARKET_DATA_URL, 'needs the market data feed');
}

test('streams AAPL quotes', async ({ page }) => {
  skipWithoutMarketData(); // state: skip, isConditional: true
});
```

When the file exports the function, or nothing in the file calls it by name, the scan can't tell which tests it
affects, and a `state-in-helper` warning says the call was not applied. A function the file declares and passes by
name to a hook or a test, as in `test.beforeEach(skipWithoutMarketData)`, is read as if its code were written there.

### Fixtures, snapshots and dependency hashes

The hash does not follow `webServer` commands or `teardown` projects. Nondefault snapshot paths from
`snapshotPathTemplate`, `snapshotDir` or a matcher's `pathTemplate` give snapshot tests a
`dependency-not-followed` warning at that setting.

A test's fixtures are those requested by the test, surrounding hooks, conditions such as
`test.skip(({ account }) => ...)` and `test.use()` values in reachable code, including helpers taking the test
object, plus the fixtures those request. Taking the fixtures object whole, an unreadable fixture function (such as
a hook returned by a factory or held in a `let`), or a fixture from an installed package includes every fixture
of the test object. An automatic fixture, or an override of built-in fixtures or options such as `page`, `context`,
`storageState` or `baseURL`, enters every test's hash. `base.extend(fixtures)` without an object literal takes
all fixtures.

A screenshot without a name, or with a runtime name, takes all files in the spec's `-snapshots` folder. A known
name takes every matching prefix, so `holdings.png` also takes `holdings-by-sector-chromium-linux.png`.
These broader dependencies can change a hash even when the changed code does not affect that test.

::: details Exact file selection

For Playwright: the files under each project's `testDir`
that `testMatch` matches and `testIgnore` doesn't, matched against the full path in any case, without those that
`.gitignore` files in the test folder leave out when no `testDir` is set or `respectGitIgnore` is on.

:::

## Supported syntax

Playwright 1.42 and newer.

### Where test comes from

| Form                       | Example                                                                                                                                                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Named import, also renamed | `import { test } from '@playwright/test'`, `import { test as base } from '@playwright/test'`                                                                                                                                   |
| Default import             | `import pwTest from '@playwright/test'`                                                                                                                                                                                        |
| Namespace import           | `import * as pw from '@playwright/test'` then `pw.test(...)`                                                                                                                                                                   |
| CommonJS                   | `const { test } = require('@playwright/test')`, `require('@playwright/test').test(...)`                                                                                                                                        |
| Other packages             | `playwright/test`, `@playwright/experimental-ct-react` and the other component testing packages, and `playwright-bdd`, whose generated files import `test` from it                                                             |
| Derived                    | `base.extend({...})`, chained `.extend().extend()`, `mergeTests(a, b)`                                                                                                                                                         |
| Kept in a variable         | `const later = test.skip`: keeps `skip`, `fixme`, `fail`, `only` or `fail.only`. Conditional and logical choices, also written inline, keep only the modifiers every possible test function shares.                            |
| Local fixtures file        | `import { test } from '../fixtures'`, or a path alias, `#` import or workspace package, followed through re-exports, `extend` chains and functions that build it, across files                                                 |
| Returned by a function     | `brokerTest('live')('sends an order', ...)`, also kept in a variable, where `brokerTest` is declared in the file or a local file and returns a test object or describe function                                                |
| Destructured members       | `const { describe, beforeEach } = test`, also nested, as in `const { describe: { serial } } = test`: each works as `test.describe` or `test.beforeEach` does. A test object built with a destructured `extend` is not followed |

### Calls

| Call                                                                                                                                          | Read as                                                                                                                                                   |
| --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test(title, body)`, `test(title, details, body)`                                                                                             | A test. The body may be a function passed by name.                                                                                                        |
| `test.only`, `test.skip`, `test.fixme`, `test.fail` with a title and body                                                                     | A test with that state.                                                                                                                                   |
| `test.fail.only`                                                                                                                              | Expected to fail, with `isOnly`. Playwright 1.49 and newer.                                                                                               |
| `test.skip()`, `test.skip(condition, description)`, `test.skip(({ browserName }) => condition, description)`; the same for `fixme` and `fail` | At file or describe level, a state for every test in the scope. In a test body or hook, a state for that test or the tests the hook runs for.             |
| `test.skip(description)` with a string, from Playwright 1.65; the same for `fixme` and `fail`                                                 | The same, with the string as the reason; an empty string applies nothing. Before 1.65 the string is only a condition, with a `skip-without-body` warning. |
| `test.describe(title, callback)`, with details, or anonymous `test.describe(callback)`                                                        | A describe.                                                                                                                                               |
| `test.describe.only`, `.skip`, `.fixme`, `.serial`, `.serial.only`, `.parallel`, `.parallel.only`                                             | A describe with that state or mode.                                                                                                                       |
| `test.describe.configure({ mode, lock })`                                                                                                     | The mode of the describe or file. `lock`, from Playwright 1.64, adds locks to every test in it.                                                           |
| `test.info().annotations.push({ type, description })`                                                                                         | An annotation of the test.                                                                                                                                |
| `test.use`, `test.slow`, `test.setTimeout`, `test.step`, `test.step.skip` (1.50), `test.expect`, `test.info`, `test.abort` (1.60), hooks      | Known; not tests.                                                                                                                                         |
| Any other chain, such as `test.only.skip`                                                                                                     | An `invalid-chain` error: Playwright fails to load the file and runs no tests at all.                                                                     |

### Details

`{ tag, annotation, lock }`: `tag` is a string or an array of strings starting with `@`; `annotation` an object or
an array of `{ type, description }`; `lock` a string or an array of strings, from Playwright 1.63. Tags are also read
from `@words` in titles.

The [shared syntax rules](/reference/syntax#in-every-runner) also apply.

## Next

* [Shared limits](/guide/limits): imports, generated tests, conditions and dependency coverage.
* [Key concepts](/guide/concepts): counts, states, IDs and hashes.
* [Output reference](/reference/output) and [diagnostics](/reference/diagnostics): interpret every field and warning.
* [CLI](/guide/cli), [library](/guide/library) and [MCP](/guide/agents): use the same inventory through each interface.
* [Compare runners](/guide/runners): differences from runner collection and execution reports.
