Chrome Extension Playwright Testing — Developer Guide
5 min readEnd-to-End Testing Chrome Extensions with Playwright
Overview
Playwright provides powerful E2E testing capabilities for Chrome extensions. Unlike Puppeteer, Playwright offers better cross-browser support and improved API for handling extension contexts.
Setup: Launching Chromium with Extension
Playwright can launch Chromium with your extension loaded using browser context arguments:
import { test, expect } from "@playwright/test";
async function launchWithExtension(extensionPath: string) {
const browser = await chromium.launch({
headless: false,
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
});
return browser;
}
Getting Extension ID Programmatically
To access extension pages, you need the extension ID. Get it from the service worker URL:
async function getExtensionId(browser: Browser): Promise<string> {
const targets = browser.targets();
const extensionTarget = targets.find(t => t.type() === "service_worker");
const extensionUrl = extensionTarget?.url() || "";
// URL format: chrome-extension://[id]/background_service_worker.js
return extensionUrl.split("/")[2];
}
Testing Popup Pages
Open the popup directly using the extension URL format:
test("popup displays current state", async ({ browser }) => {
const extId = await getExtensionId(browser);
const popupPage = await browser.newPage();
await popupPage.goto(`chrome-extension://${extId}/popup.html`);
// Interact with popup DOM
await popupPage.click("#toggle-button");
const status = await popupPage.textContent("#status");
expect(status).toBe("Enabled");
// Screenshot testing
await expect(popupPage.locator("#root")).toHaveScreenshot("popup-enabled.png");
});
Testing Content Scripts
Navigate to a target page and verify injected elements:
test("content script injects elements", async ({ page }) => {
// Extension must be loaded via browser context first
await page.goto("https://example.com");
// Wait for content script to inject
await page.waitForSelector(".extension-injected-button");
// Verify injection
const button = page.locator(".extension-injected-button");
await button.click();
// Verify state change
await expect(page.locator(".extension-panel")).toBeVisible();
});
Testing Background/Service Worker
Evaluate code directly in the service worker context:
test("background script handles messages", async ({ browser }) => {
const extId = await getExtensionId(browser);
// Create a page to communicate with background
const page = await browser.newPage();
await page.goto(`chrome-extension://${extId}/background.html`);
// Evaluate in service worker context
const bgPage = await browser.waitForTarget(
t => t.type() === "service_worker" && t.url().includes(extId)
);
const bg = await bgPage.worker();
// Test background function directly
const result = await bg.evaluate(() => {
// Access background scope
return "Background evaluated";
});
});
Testing Options Page
Navigate to and interact with the options page:
test("options page saves settings", async ({ browser }) => {
const extId = await getExtensionId(browser);
const page = await browser.newPage();
await page.goto(`chrome-extension://${extId}/options.html`);
// Fill and save settings
await page.fill("#api-key", "test-key-123");
await page.click("#save-button");
// Verify saved
await expect(page.locator(".success-message")).toBeVisible();
});
Extension Test Fixture
Create a reusable fixture for cleaner tests:
import { test as base } from "@playwright/test";
export const test = base.extend({
extensionId: async ({ browser }, use) => {
const extPath = path.resolve(__dirname, "./dist");
await launchWithExtension(extPath);
const id = await getExtensionId(browser);
await use(id);
},
});
CI Setup
Extensions require headful mode. Use xvfb on Linux:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx xvfb-maybe -- playwright test
env:
CI: "true"
Common Pitfalls
- Extension not loaded: Ensure
--disable-extensions-exceptand--load-extensionare both set - Wrong extension ID: ID changes between builds; always fetch dynamically
- Timing issues: Wait for extension to initialize before testing
- Service worker termination: Use
--disable-backgrounding-occluded-windowsto prevent sleep
Related Guides
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.