Chrome Extension Puppeteer Testing — Developer Guide

4 min read

End-to-End Testing Chrome Extensions with Puppeteer

Overview

Puppeteer provides direct control over Chromium, making it a solid choice for testing Chrome extensions. While Playwright offers better cross-browser support, Puppeteer’s tight Chromium integration provides reliable extension testing capabilities.

Setup: Launching Chromium with Extension

Launch Chromium with your extension loaded using the --disable-extensions-except and --load-extension arguments:

const puppeteer = require("puppeteer");

async function launchWithExtension(extensionPath) {
  const browser = await puppeteer.launch({
    headless: false, // Extensions don't load in headless mode
    args: [
      `--disable-extensions-except=${extensionPath}`,
      `--load-extension=${extensionPath}`,
    ],
  });
  return browser;
}

Getting Extension ID Programmatically

To access extension pages, you need the extension ID. Retrieve it from the background service worker target:

async function getExtensionId(browser) {
  const targets = browser.targets();
  const extensionTarget = targets.find(
    (t) => t.type() === "service_worker" && t.url().includes("chrome-extension://")
  );
  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 scheme:

test("popup displays current state", async () => {
  const browser = await launchWithExtension("./dist");
  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.$eval("#status", (el) => el.textContent);
  expect(status).toBe("Enabled");

  await popupPage.screenshot({ path: "popup-enabled.png" });
});

Testing Content Scripts

Navigate to a target page and verify injected elements:

test("content script injects elements", async () => {
  const browser = await launchWithExtension("./dist");
  const page = await browser.newPage();

  await page.goto("https://example.com");

  // Wait for content script to inject
  await page.waitForSelector(".extension-injected-button");

  // Verify injection
  await page.click(".extension-injected-button");
  await page.waitForSelector(".extension-panel", { visible: true });
});

Testing Background Service Worker

Access the service worker target directly:

async function getBackgroundPage(browser) {
  const targets = browser.targets();
  const swTarget = targets.find(
    (t) => t.type() === "service_worker" && t.url().includes("background")
  );
  return swTarget?.worker();
}

Waiting Strategies

Use appropriate wait strategies for extension contexts:

// Wait for selector
await page.waitForSelector("#element");

// Wait for function with custom predicate
await page.waitForFunction(() => window.extensionReady === true);

// Wait for navigation in extension pages
await page.waitForNavigation({ waitUntil: "networkidle0" });

Mocking Chrome APIs

Mock Chrome APIs in your test environment:

await page.evaluateOnNewDocument(() => {
  chrome.runtime.sendMessage = (msg, cb) => {
    console.log("Mocked message:", msg);
    if (cb) cb({ response: "mocked" });
  };
});

CI Setup

Extensions require headful mode. Configure CI accordingly:

# Linux (xvfb-run)
xvfb-run npm test

# macOS/Windows - native headful supported
npm test

Puppeteer vs Playwright for Extensions

Feature Puppeteer Playwright
Chromium integration Excellent Good
Cross-browser Chrome/Chromium only All major browsers
Extension API Direct target access Browser context args
Community support Strong Growing

Test Helpers

Create reusable utilities for extension testing:

module.exports = {
  launchWithExtension,
  getExtensionId,
  getBackgroundPage,
  waitForExtensionReady,
};

Cross-Reference

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