Chrome Extension Puppeteer Testing — Developer Guide
4 min readEnd-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
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.