> ## Documentation Index
> Fetch the complete documentation index at: https://extension.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Playwright E2E testing for extensions

> Write repeatable end-to-end tests for browser extensions with Playwright. Cover extension flows, UI rendering, and integrations in CI and locally.

Validate extension behavior across browsers with repeatable end-to-end tests.

Extension.js projects can use Playwright to test extension flows, UI rendering, and integration behavior in continuous integration (CI) and local environments.

## Playwright testing capabilities

| Capability | What it gives you |
| - | - |
| Cross-browser runtime checks | Validate core flows on Chromium and Firefox engines |
| UI and interaction coverage | Test popup/options/content-script behavior with real browser contexts |
| CI-ready reports | Capture traces/screenshots/videos for failed tests |
| Regression safety | Catch integration bugs that unit tests usually miss |

## Why use it

* Catch runtime regressions that unit tests miss.
* Validate extension behavior on real browser engines.
* Verify multi-browser changes before release.

## Typical setup

Install Playwright test dependencies in your project:

<CodeGroup>
  ```bash npm theme={null}
  npm install -D @playwright/test
  ```

  ```bash pnpm theme={null}
  pnpm add -D @playwright/test
  ```

  ```bash yarn theme={null}
  yarn add -D @playwright/test
  ```

  ```bash bun theme={null}
  bun add -d @playwright/test
  ```

  ```bash deno theme={null}
  deno add -D npm:@playwright/test
  ```
</CodeGroup>

Create a `playwright.config.ts` and define browser projects and reporting.

## Recommended baseline

```ts theme={null}
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "e2e",
  retries: process.env.CI ? 2 : 0,
  reporter: [["html"], ["list"]],
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
  ],
});
```

## Automation contract (recommended)

For deterministic automation, do not parse terminal text. Use the metadata files that Extension.js generates:

* `dist/extension-js/<browser>/ready.json`
* `dist/extension-js/<browser>/events.ndjson` (newline-delimited JSON for watch/rebuild events)

`ready.json` (schema v2) includes stable fields intended for scripts/agents:

* `status`: `starting` | `ready` | `error` | `stopped`
* `command`: `dev` | `start` | `preview` | `build`
* `browser`
* `distPath`
* `manifestPath`
* `port`
* `pid`
* `browserPid` (the launched browser process, use it for teardown)
* `runId`
* `startedAt`
* `compiledAt`
* `errors`
* `runtime`: `"attached"` once the service worker is connected
* `executorAttachedAt`

Use this contract as the source of truth for readiness and failures. The full field reference, including `extensionId`, `profilePath`, `cdpPort`, and the error states, is in [ready.json](/docs/contracts/ready-json).

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/i7HWdgkEXZA?rel=0" title="Extension.js: Playwright end-to-end run" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>

## Running tests

<CodeGroup>
  ```bash npm theme={null}
  npx playwright test
  ```

  ```bash pnpm theme={null}
  pnpm playwright test
  ```

  ```bash yarn theme={null}
  yarn playwright test
  ```

  ```bash bun theme={null}
  bunx playwright test
  ```

  ```bash deno theme={null}
  deno run -A npm:playwright test
  ```
</CodeGroup>

## Canonical Playwright flow (AI-friendly)

1. Start Extension.js in no-browser mode.
2. Wait until `ready.json` reports `status: "ready"`.
3. Launch Playwright with the extension output from `distPath`.
4. Run tests and shut down.

One caveat on step 2: `status: "ready"` means compiled. If your test drives the extension through the act commands (`eval`, `storage`, `reload`, `open`), also wait until the contract carries `runtime: "attached"`. Playwright-launched browsers load `distPath` themselves, so plain UI tests need only `ready`.

### Development mode vs test mode

* `dev` is for watch-mode iteration (`extension dev --no-browser` + `extension dev --wait`).
* `start` is for production-style checks (`extension start --no-browser` + `extension start --wait`).

### Important distinction: run mode vs readiness gate

* `--no-browser` is the **run mode**: it starts the extension pipeline without launching a browser.
* A readiness gate (`extension dev --wait --browser=<browser>`) is the **synchronization step** that tells Playwright when the extension is ready.

`--no-browser` produces build output. The wait step confirms readiness before tests proceed. If your environment cannot run a second CLI process, poll `ready.json` directly.

Use this two-process pattern:

1. Process A: `extension dev --no-browser`
2. Process B: `extension dev --wait --browser=<browser> --output json`
3. Start Playwright only after the wait step exits successfully

Production-oriented variant:

1. Process A: `extension start --no-browser`
2. Process B: `extension start --wait --browser=<browser> --output json`
3. Start Playwright only after the wait step exits successfully

`--output json` prints one envelope on stdout, human copy moves to stderr. The older `--wait-format` alias still works but warns on stderr.

```ts theme={null}
import { spawn } from "node:child_process";
import { chromium } from "@playwright/test";

const browserName = "chromium";

const child = spawn(
  "pnpm",
  ["extension", "dev", "--no-browser", `--browser=${browserName}`],
  {
    stdio: "inherit",
    env: process.env,
  },
);

async function waitForReady() {
  return await new Promise<any>((resolve, reject) => {
    let stdout = "";
    let stderr = "";
    const wait = spawn(
      "pnpm",
      [
        "extension",
        "dev",
        "--wait",
        `--browser=${browserName}`,
        "--wait-timeout=60000",
        "--output=json",
      ],
      { stdio: ["ignore", "pipe", "pipe"], env: process.env },
    );
    wait.stdout.on("data", (chunk) => (stdout += chunk.toString()));
    wait.stderr.on("data", (chunk) => (stderr += chunk.toString()));
    wait.on("error", reject);
    wait.on("close", (code) => {
      if ((code ?? 1) !== 0) {
        reject(
          new Error(stderr || `wait command failed with code ${String(code)}`),
        );
        return;
      }
      const payload = JSON.parse(stdout.trim());
      resolve(payload.results[0]);
    });
  });
}

const ready = await waitForReady();

const context = await chromium.launchPersistentContext("", {
  headless: false,
  args: [
    `--disable-extensions-except=${ready.distPath}`,
    `--load-extension=${ready.distPath}`,
  ],
});

// run tests using context/pages...

await context.close();
child.kill("SIGTERM");
```

`headless: false` is required, not a preference. Playwright's default headless
mode runs Chromium's `headless_shell` binary, which loads no extensions at all.
A suite that flips it to `true` for CI still passes, because it silently tests a
browser with your extension missing. To run without a visible window, keep
`headless: false` and add `--headless=new` to `args`, which uses the full
Chromium headless mode that does support extensions.

## Practical guidance for extensions

* Keep test fixtures deterministic; extension startup can be sensitive to profile state.
* Prefer explicit waits on extension UI conditions over fixed timeouts.
* Run Chromium and Firefox projects in CI for cross-engine confidence.
* Capture traces/screenshots/videos on failure for faster debugging.
* Prefer `ready.json`/`events.ndjson` over stdout parsing for machine reliability.

## Common pitfalls

* Relying on fixed timeouts instead of state-based waits
* Running only one browser target in CI
* Skipping artifact upload for failed runs
* Coupling tests to local-only profile or environment assumptions

## Repository reference

The `playwright` template scaffolds a working setup you can copy from:

* `playwright.config.ts` and an `e2e/` folder, in [extension-js/examples/playwright](https://github.com/extension-js/examples/tree/main/examples/playwright)

## Next steps

* Set up [CI templates](/docs/workflows/ci-templates).
* Keep command workflows aligned with [dev](/docs/commands/dev) and [build](/docs/commands/build).

## See the template run

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube-nocookie.com/embed/k8PP2Gs1AsU?rel=0" title="Extension.js: Playwright E2E Sidebar template" loading="lazy" allow="encrypted-media; picture-in-picture; fullscreen" allowFullScreen />
</Frame>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.