Chrome extension E2E testing with Playwright helps QA engineers validate the real workflow users see: the extension loads, its popup opens, a page changes as expected, and failures produce useful evidence. The setup is different from a normal website test because the extension must be loaded when Chromium starts and its runtime ID is not something your test should guess.
This tutorial builds a small, repeatable workflow for a Manifest V3 extension. It follows current official Chrome and Playwright guidance and avoids brittle shortcuts such as hard-coding an extension ID or asserting only against hidden implementation state.
What you will test
Assume your unpacked extension lives in my-extension/. Its popup contains an Enable switch, and enabling it adds a visible marker to a sample page. The tutorial will:
- launch Playwright’s bundled Chromium with the extension loaded;
- discover the Manifest V3 extension ID from its service worker;
- open and test the popup page;
- assert a user-visible result on the target page;
- save screenshots and traces when a test fails; and
- plan one focused service-worker resilience check.
Chrome’s official E2E guidance recommends testing flows a user would perform and generally preferring visible behavior over internal state. That principle keeps the suite useful even when implementation details change.
Prerequisites
- Node.js installed
- a Manifest V3 extension in an unpacked folder
- Playwright Test installed in the project
- a safe local or test page the extension is allowed to access
Install Playwright Test and its browser:
npm init playwright@latest
npx playwright install chromium
Keep the extension build deterministic. If your source must be compiled, add a build command and point the fixture to the generated extension directory, not the source directory.
Step 1: Create a persistent Chromium fixture
Playwright’s official Chrome extension guide says extensions work in Chromium through a persistent browser context. It also recommends Playwright’s bundled Chromium because Google Chrome and Microsoft Edge removed the command-line flags used for this side-loading workflow.
Create tests/extension.fixture.ts:
import { test as base, chromium, type BrowserContext } from '@playwright/test';
import path from 'path';
type ExtensionFixtures = {
context: BrowserContext;
extensionId: string;
};
export const test = base.extend<ExtensionFixtures>({
context: async ({}, use) => {
const extensionPath = path.join(__dirname, '..', 'my-extension');
const context = await chromium.launchPersistentContext('', {
channel: 'chromium',
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`
]
});
await use(context);
await context.close();
},
extensionId: async ({ context }, use) => {
let [worker] = context.serviceWorkers();
if (!worker) {
worker = await context.waitForEvent('serviceworker');
}
const extensionId = worker.url().split('/')[2];
await use(extensionId);
}
});
export const expect = test.expect;
The empty user-data directory tells Playwright to create a temporary persistent profile. The service-worker URL has the form chrome-extension://EXTENSION_ID/..., so the fixture can retrieve the ID at runtime instead of embedding a machine-specific value.
Step 2: Test the popup like a normal page
Chrome documentation confirms that extension pages can be opened through their chrome-extension:// URLs. This is a practical way to test a popup when automating a toolbar click is not available or would add unnecessary fragility.
Create tests/popup.spec.ts:
import { test, expect } from './extension.fixture';
test('enables the extension from the popup', async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await expect(page.getByRole('heading', { name: 'Extension settings' }))
.toBeVisible();
const enableSwitch = page.getByRole('switch', { name: 'Enable extension' });
await expect(enableSwitch).toHaveAttribute('aria-checked', 'false');
await enableSwitch.click();
await expect(enableSwitch).toHaveAttribute('aria-checked', 'true');
});
Use role- and label-based locators when the popup exposes accessible names. These locators mirror user intent and make accessibility problems visible during test review. Replace the example names with the actual accessible labels in your extension.
Step 3: Validate behavior on a target page
A popup assertion proves the control changed, but not that the extension delivered its value. Add a second test against a controlled sample page:
import { test, expect } from './extension.fixture';
test('shows the enabled marker on the test page', async ({ page, extensionId }) => {
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await page.getByRole('switch', { name: 'Enable extension' }).click();
await page.goto('http://127.0.0.1:4173/extension-test-page');
await expect(page.getByTestId('extension-enabled-marker')).toBeVisible();
await expect(page.getByTestId('extension-enabled-marker'))
.toHaveText('Extension enabled');
});
Run the sample site locally in CI so the test does not depend on a changing public page. Verify that the manifest grants only the host access the test needs. A passing popup test plus a passing target-page assertion gives stronger evidence than checking storage alone.
Step 4: Add evidence for failures
Configure Playwright to retain a trace and screenshot on failure:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
screenshot: 'only-on-failure',
trace: 'retain-on-failure'
},
reporter: [['html', { open: 'never' }]]
});
Run the focused suite:
npx playwright test tests/popup.spec.ts
npx playwright show-report
For triage, capture the failing step, popup state, target-page state, console errors, and trace. Do not treat a screenshot alone as proof of the root cause; combine it with the action timeline and relevant logs.
Step 5: Cover Manifest V3 service-worker resilience
Manifest V3 extension service workers can stop after inactivity and restart when an event arrives. Chrome advises developers to persist important state rather than relying only on global variables. Playwright’s extension documentation also explains how its worker handle behaves across idle suspension.
Add a focused scenario for any critical workflow that depends on worker state:
- enable the feature and confirm the visible result;
- allow or deliberately trigger the worker lifecycle condition in a controlled test;
- perform the user action again; and
- verify that persisted settings and the visible result still work.
Keep this as a separate resilience test because it can take longer and needs clearer diagnostics. Avoid fixed-duration waiting as a synchronization strategy. Wait for observable events, UI states, or messages that prove the extension is ready.
Practical QA review checklist
- Build: Is the test loading the exact packaged directory intended for release?
- Browser: Is the bundled Chromium channel used as required by the current Playwright guidance?
- ID: Is the extension ID discovered dynamically?
- Locators: Do popup controls have stable accessible roles and names?
- Assertions: Does the test verify a visible user outcome, not only internal storage?
- Permissions: Does the test environment use the minimum host access needed?
- Isolation: Does each test get clean state, or is shared state explicitly reset?
- Evidence: Are traces, screenshots, reports, and console errors available on failure?
- Lifecycle: Is critical state resilient to Manifest V3 worker termination?
- Human review: Are permissions, UX, accessibility, and exploratory risks still reviewed manually?
Screenshot checklist
- The unpacked Manifest V3 extension folder and manifest file
- The persistent-context fixture with extension-loading arguments
- The runtime extension ID captured from the service-worker URL
- The popup page with its accessible Enable switch
- The controlled target page showing the extension’s visible result
- The Playwright HTML report with a passing popup and page-flow test
- A retained trace or failure screenshot used during triage
- The service-worker resilience test result and persisted state evidence
Common mistakes
- Launching a normal browser context: extension loading requires the persistent-context setup described by Playwright.
- Using installed Chrome by habit: use bundled Chromium for this side-loading workflow unless official guidance changes.
- Hard-coding the extension ID: derive it from the Manifest V3 service worker.
- Testing only storage: internal state can pass while the user-facing workflow is broken.
- Sharing dirty state: persistent profiles can leak settings unless the fixture creates clean temporary data or resets it explicitly.
- Ignoring worker lifecycle: global-only state may disappear when a Manifest V3 worker stops.
What this tutorial does not replace
One E2E flow does not replace unit tests, manual permission inspection, accessibility testing, security review, browser-version coverage, Web Store policy checks, or exploratory testing. Use this workflow as a reliable automation layer around your most important user journeys.
References
- Chrome for Developers: End-to-end testing for Chrome extensions
- Chrome for Developers: Extension service-worker lifecycle
- Playwright: Chrome extensions
- Playwright: Test assertions
FAQ
Can Playwright test a Manifest V3 Chrome extension?
Yes. Playwright documents loading an unpacked extension in its bundled Chromium with a persistent context and retrieving the extension ID from the service worker.
Why should the extension ID be discovered dynamically?
The runtime ID can differ across environments. Deriving it from the service-worker URL makes the test portable and avoids a fragile hard-coded value.
Should a Chrome extension E2E test inspect internal storage?
Only when it adds useful diagnostic coverage. The primary assertion should normally reflect user-visible behavior, following Chrome’s E2E testing guidance.
Do these tests replace manual Chrome extension testing?
No. They automate repeatable critical flows, while exploratory testing, permission review, accessibility, security, and release judgment remain important human activities.
Conclusion
Chrome extension E2E testing with Playwright becomes manageable when the setup is explicit: load the unpacked build in a persistent Chromium context, discover the extension ID, test the popup as a page, assert the visible target-page outcome, and retain strong failure evidence. Add one targeted Manifest V3 lifecycle scenario for critical state, and your QA suite will catch problems that simple popup checks often miss.
