Practical guide — verify API details against the current Playwright documentation.

Playwright setup

How to test a Chrome extension with Playwright.

The reliable starting point is a persistent context using Playwright’s bundled Chromium. From the extension service-worker URL, you can discover the generated extension ID and navigate directly to a popup or options page.

Last reviewed August 18, 2026 · Playwright APIs can change; the official documentation is the source of truth.

Current Playwright guidance says extension testing requires a persistent Chromium context. It also warns that branded Chrome and Edge removed the command-line behavior used to side-load extensions, so use Playwright’s bundled Chromium for this flow.

1. Launch the unpacked build

import { chromium } from "@playwright/test";

const extensionPath = "/absolute/path/to/unpacked-extension";
const context = await chromium.launchPersistentContext("", {
  channel: "chromium",
  headless: true,
  args: [
    `--disable-extensions-except=${extensionPath}`,
    `--load-extension=${extensionPath}`
  ]
});

Use the exact release build. Keep the user-data directory temporary unless the test explicitly covers upgrades or persisted state.

2. Discover the MV3 service worker and extension ID

let [worker] = context.serviceWorkers();
if (!worker) worker = await context.waitForEvent("serviceworker");

const extensionId = new URL(worker.url()).host;

Do not hard-code a generated ID unless your test environment deliberately configures one. Waiting for the worker also gives you an execution context for targeted lifecycle checks.

3. Open the popup as an extension page

const popup = await context.newPage();
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
await popup.getByRole("button", { name: "Save" }).click();

Direct navigation is usually more stable than trying to automate the browser toolbar. If the popup depends on the active tab, give the application a test seam that selects a known synthetic tab without changing production behavior.

4. Keep content-script tests bounded

  1. Open a synthetic host page you control.
  2. Wait for a user-visible signal that injection completed.
  3. Assert one critical interaction across the page and injected UI.
  4. Navigate the host page and verify that the UI is neither missing nor duplicated.
  5. Keep iframe and Shadow DOM traversal inside small helpers rather than spreading selectors across every test.

5. Split the test pyramid by failure type

LayerBest use
UnitMessage handlers, storage transforms, permission decisions, migration logic, and error mapping.
ComponentPopup/options rendering, keyboard behavior, empty states, and visual variants with mocked Chrome APIs.
Loaded-extension E2ETwo or three critical flows that truly require a browser, real extension origin, or content-script boundary.
Manual exploratoryNew permissions, unusual host pages, browser UI interactions, store packaging, and behavior not stable enough to automate.

6. Common causes of flaky extension tests

Official references

Next: force a service-worker restart, or return to the full MV3 release checklist.

Would a recorder help after the first Playwright test?

MV3 Replay is testing interest in a local visual workflow with extension-aware diagnostics and readable Playwright export. It is a concept, not a released product.

Review the concept