Chrome Extension Playwright Testing — Developer Guide

5 min read

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

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.