> ## 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:

<PackageManagerTabs command="install -D @playwright/test" />

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` includes stable fields intended for scripts/agents:

* `status`: `starting` | `ready` | `error`
* `command`: `dev` | `start` | `preview`
* `browser`
* `distPath`
* `manifestPath`
* `port`
* `pid`
* `runId`
* `startedAt`
* `compiledAt`
* `errors`

Use this contract as the source of truth for readiness and failures.

## 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.

### 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> --wait-format=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> --wait-format=json`
3. Start Playwright only after the wait step exits successfully

```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",
        "--wait-format=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");
```

## 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

Your repository includes a Playwright configuration at:

* `playwright.config.ts`

## Next steps

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