End-to-End Testing for Chrome Extensions: Complete Guide (2026)
Why End-to-End Testing for Extensions?
Unit tests verify individual functions work correctly. End-to-end tests verify your entire extension works as users experience it — popup rendering, content script injection, service worker behavior, and cross-component communication all tested together.
E2E tests catch the integration bugs that unit tests miss: a popup that renders blank because the service worker hasn’t initialized storage, a content script that fails on specific page structures, or a message handler that drops messages under load.
Choosing a Testing Library
| Library | Extension Support | Headless | Best For |
|---|---|---|---|
| Puppeteer | Native (--load-extension) | Chrome only | Chrome-specific extensions |
| Playwright | Via launch args | Chromium | Cross-browser testing |
| Selenium | Via ChromeOptions | Via ChromeDriver | Existing Selenium infra |
| WebDriverIO | Native support | Via WebDriver | Comprehensive test suites |
Puppeteer Setup
const puppeteer = require('puppeteer');
const path = require('path');
async function setupBrowser() {
return puppeteer.launch({
headless: false, // Required for extensions
args: [
`--disable-extensions-except=${path.resolve('./dist')}`,
`--load-extension=${path.resolve('./dist')}`
]
});
}Playwright Setup
const { chromium } = require('playwright');
const path = require('path');
async function setupBrowser() {
return chromium.launchPersistentContext('/tmp/test-profile', {
headless: false,
args: [
`--disable-extensions-except=${path.resolve('./dist')}`,
`--load-extension=${path.resolve('./dist')}`
]
});
}Selenium Setup
const { Builder } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const path = require('path');
async function setupDriver() {
const options = new chrome.Options();
options.addArguments(`--load-extension=${path.resolve('./dist')}`);
return new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
}Maintaining a Fixed Extension ID
A consistent ID simplifies testing — you can hardcode URLs and allowlist origins:
- Upload to the Chrome Developer Dashboard
- Copy the public key
- Add to
manifest.json:
{
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
}With a fixed ID, extension page URLs become predictable:
chrome-extension://abcdefghijklmnopqrstuvwx/popup.html
chrome-extension://abcdefghijklmnopqrstuvwx/options.htmlTesting Extension Pages
test('options page saves settings', async () => {
const page = await browser.newPage();
await page.goto(`chrome-extension://${EXTENSION_ID}/options.html`);
// Change a setting
await page.click('#dark-mode-toggle');
await page.click('#save-btn');
// Verify toast notification
await page.waitForSelector('.toast-success');
const toast = await page.$eval('.toast-success', (el) => el.textContent);
expect(toast).toContain('Settings saved');
// Reload and verify persistence
await page.reload();
const isChecked = await page.$eval('#dark-mode-toggle', (el) => el.checked);
expect(isChecked).toBe(true);
});Testing Popups
Two approaches for popup testing:
Approach 1: Open Popup URL Directly
test('popup renders tab list', async () => {
const page = await browser.newPage();
await page.goto(`chrome-extension://${EXTENSION_ID}/popup.html`);
await page.waitForSelector('.tab-item');
const tabCount = await page.$$eval('.tab-item', (items) => items.length);
expect(tabCount).toBeGreaterThan(0);
});Approach 2: Use action.openPopup()
test('popup opens via action', async () => {
const worker = await getServiceWorker(browser);
await worker.evaluate(() => chrome.action.openPopup());
const popupTarget = await browser.waitForTarget(
(t) => t.type() === 'page' && t.url().endsWith('popup.html')
);
const popup = await popupTarget.page();
const heading = await popup.$eval('h1', (el) => el.textContent);
expect(heading).toBeDefined();
});Simulating Tab Context in Popup
Popups often behave differently based on the active tab. Pass context via URL params:
// In your popup.js
const params = new URLSearchParams(window.location.search);
if (params.has('tab')) {
activeTabId = parseInt(params.get('tab'));
}// In tests
await page.goto(
`chrome-extension://${EXTENSION_ID}/popup.html?tab=${testTabId}`
);Inspecting Service Worker State
Puppeteer
async function getServiceWorker(browser) {
const target = await browser.waitForTarget(
(t) => t.type() === 'service_worker'
);
return target.worker();
}
test('storage is initialized on install', async () => {
const worker = await getServiceWorker(browser);
const settings = await worker.evaluate(async () => {
return chrome.storage.local.get('settings');
});
expect(settings.settings).toBeDefined();
expect(settings.settings.theme).toBe('light');
});Selenium
test('service worker state via extension page', async () => {
await driver.get(`chrome-extension://${EXTENSION_ID}/debug.html`);
const result = await driver.executeAsyncScript(`
const callback = arguments[arguments.length - 1];
chrome.storage.local.get('settings', (data) => callback(data));
`);
expect(result.settings).toBeDefined();
});CI/CD Integration
GitHub Actions (Linux)
- run: xvfb-run --auto-servernum npm testHeadless Mode (Chrome 112+)
const browser = await puppeteer.launch({
headless: 'new', // New headless mode supports extensions
args: [
`--disable-extensions-except=${EXTENSION_PATH}`,
`--load-extension=${EXTENSION_PATH}`
]
});Test Organization
tests/
├── setup.js # Browser launch and teardown
├── popup.test.js # Popup UI tests
├── options.test.js # Options page tests
├── content-script.test.js # Content script injection
├─�� service-worker.test.js # Background logic
└── integration.test.js # Cross-component flowsWhat’s Next
E2E testing gives you the confidence to ship extensions knowing they work end-to-end. Start with popup and options page tests, add service worker validation, and run everything in CI.
Grow your well-tested extension 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.