Test Chrome Extensions with Puppeteer: Complete Automation Guide (2026)
Why Test Extensions with Puppeteer?
Puppeteer provides high-level control over a Chrome browser, making it perfect for end-to-end testing of Chrome extensions. Unlike unit tests that mock the Chrome API, Puppeteer tests run your extension in a real browser — catching integration issues, popup rendering bugs, and service worker failures.
Project Setup
Install Dependencies
npm init -y
npm install puppeteer jest --save-devpackage.json
{
"scripts": {
"test": "jest --testTimeout=30000"
}
}jest.config.js
module.exports = {
testTimeout: 30000,
testMatch: ['**/*.test.js']
};Launching Chrome with Your Extension
// helpers.js
const puppeteer = require('puppeteer');
const path = require('path');
const EXTENSION_PATH = path.resolve(__dirname, '../my-extension');
async function launchBrowser() {
const browser = await puppeteer.launch({
headless: false, // Extensions require headed mode
pipe: true,
args: [
`--disable-extensions-except=${EXTENSION_PATH}`,
`--load-extension=${EXTENSION_PATH}`
]
});
return browser;
}
async function getExtensionId(browser) {
const workerTarget = await browser.waitForTarget(
(target) => target.type() === 'service_worker' &&
target.url().includes('service-worker')
);
const url = new URL(workerTarget.url());
return url.hostname;
}
module.exports = { launchBrowser, getExtensionId, EXTENSION_PATH };Testing the Extension Popup
// popup.test.js
const { launchBrowser, getExtensionId } = require('./helpers');
describe('Extension Popup', () => {
let browser;
let extensionId;
beforeEach(async () => {
browser = await launchBrowser();
extensionId = await getExtensionId(browser);
});
afterEach(async () => {
await browser.close();
});
test('popup renders correctly', async () => {
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionId}/popup.html`);
const title = await page.$eval('h1', (el) => el.textContent);
expect(title).toBe('My Extension');
});
test('popup displays tab count', async () => {
// Open some tabs first
const page1 = await browser.newPage();
await page1.goto('https://example.com');
const page2 = await browser.newPage();
await page2.goto('https://example.org');
// Open popup
const popup = await browser.newPage();
await popup.goto(`chrome-extension://${extensionId}/popup.html`);
await popup.waitForSelector('#tab-count');
const count = await popup.$eval('#tab-count', (el) => el.textContent);
expect(parseInt(count)).toBeGreaterThanOrEqual(2);
});
test('popup button triggers action', async () => {
const page = await browser.newPage();
await page.goto(`chrome-extension://${extensionId}/popup.html`);
await page.click('#action-btn');
await page.waitForSelector('.result');
const result = await page.$eval('.result', (el) => el.textContent);
expect(result).not.toBe('');
});
});Testing the Service Worker
// service-worker.test.js
const { launchBrowser, getExtensionId } = require('./helpers');
describe('Service Worker', () => {
let browser;
let extensionId;
beforeEach(async () => {
browser = await launchBrowser();
extensionId = await getExtensionId(browser);
});
afterEach(async () => {
await browser.close();
});
test('service worker is active', async () => {
const workerTarget = await browser.waitForTarget(
(target) => target.type() === 'service_worker'
);
const worker = await workerTarget.worker();
expect(worker).toBeDefined();
});
test('service worker returns stored data', async () => {
const workerTarget = await browser.waitForTarget(
(target) => target.type() === 'service_worker'
);
const worker = await workerTarget.worker();
const result = await worker.evaluate(async () => {
await chrome.storage.local.set({ testKey: 'testValue' });
const data = await chrome.storage.local.get('testKey');
return data.testKey;
});
expect(result).toBe('testValue');
});
});Testing Content Scripts
// content-script.test.js
const { launchBrowser, getExtensionId } = require('./helpers');
describe('Content Script', () => {
let browser;
beforeEach(async () => {
browser = await launchBrowser();
});
afterEach(async () => {
await browser.close();
});
test('content script injects UI', async () => {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// Wait for content script to inject its element
await page.waitForSelector('#my-extension-widget', { timeout: 5000 });
const isVisible = await page.$eval('#my-extension-widget', (el) => {
return el.offsetParent !== null;
});
expect(isVisible).toBe(true);
});
test('content script modifies page', async () => {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
// Check if the content script made expected changes
const hasModification = await page.evaluate(() => {
return document.body.classList.contains('my-extension-active');
});
expect(hasModification).toBe(true);
});
});Opening Popup via Extension Action
test('popup opens via action click', async () => {
const workerTarget = await browser.waitForTarget(
(target) => target.type() === 'service_worker'
);
const worker = await workerTarget.worker();
// Trigger popup open programmatically
await worker.evaluate(() => chrome.action.openPopup());
// Find the popup target
const popupTarget = await browser.waitForTarget(
(target) => target.type() === 'page' && target.url().endsWith('popup.html')
);
const popup = await popupTarget.page();
const title = await popup.$eval('h1', (el) => el.textContent);
expect(title).toBeDefined();
});CI/CD Configuration
GitHub Actions
name: Extension Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install
- run: npm run build
- run: xvfb-run npm testPuppeteer requires a display server on Linux CI. xvfb-run provides a virtual display.
Tips
- Fresh browser per test — Launch and close a new browser for each test to avoid state leaks
- Increase timeouts — Extension loading is slower than regular page loads. Use 30s+ timeouts.
- Use
waitForSelector— Don’t assume elements are immediately available after navigation - Test in headed mode locally — Set
headless: falseduring development to see what’s happening
What’s Next
Puppeteer testing gives you confidence that your extension works as a complete system — not just individual functions. Start with popup tests, add service worker validation, and integrate into your CI pipeline.
Optimize your extension’s Chrome Web Store presence with ExtensionBooster.
Share this article
Build better extensions with free tools
Icon generator, MV3 converter, review exporter, and more — no signup needed.
Related Articles
I Built the Same Chrome Extension With 5 Different Frameworks. Here's What Actually Happened.
WXT vs Plasmo vs CRXJS vs Extension.js vs Bedframe. Real benchmarks, honest opinions, and the framework with 12K stars that's quietly dying.
5 Best Email Marketing Services to Grow Your Chrome Extension (2026)
Compare the top email marketing platforms for SaaS and Chrome extension developers. MailerLite, Mailchimp, Brevo, ActiveCampaign, and Drip reviewed.
15 Best Practices to Build a Browser Extension That Users Love (2026 Guide)
Master browser extension development in 2026. Manifest V3, security, performance, and UX best practices to build extensions users love.