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.
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
- Open a synthetic host page you control.
- Wait for a user-visible signal that injection completed.
- Assert one critical interaction across the page and injected UI.
- Navigate the host page and verify that the UI is neither missing nor duplicated.
- Keep iframe and Shadow DOM traversal inside small helpers rather than spreading selectors across every test.
5. Split the test pyramid by failure type
| Layer | Best use |
|---|---|
| Unit | Message handlers, storage transforms, permission decisions, migration logic, and error mapping. |
| Component | Popup/options rendering, keyboard behavior, empty states, and visual variants with mocked Chrome APIs. |
| Loaded-extension E2E | Two or three critical flows that truly require a browser, real extension origin, or content-script boundary. |
| Manual exploratory | New permissions, unusual host pages, browser UI interactions, store packaging, and behavior not stable enough to automate. |
6. Common causes of flaky extension tests
- Sharing one persistent profile across unrelated tests.
- Assuming the MV3 service worker remains alive.
- Waiting a fixed number of milliseconds instead of waiting for an observable state.
- Testing a dev build while publishing a different bundle.
- Using production accounts, payments, or destructive endpoints.
- Asserting implementation details rather than the user-visible result.
Official references
- Playwright: Chrome extensions
- Chrome: end-to-end testing for extensions
- Chrome: Puppeteer extension-testing tutorial
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