Test Chrome Extensions with Puppeteer: Complete Automation Guide (2026)

AppBooster Team · · 5 min read
Automated testing code on a monitor

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

package.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 test

Puppeteer requires a display server on Linux CI. xvfb-run provides a virtual display.


Tips

  1. Fresh browser per test — Launch and close a new browser for each test to avoid state leaks
  2. Increase timeouts — Extension loading is slower than regular page loads. Use 30s+ timeouts.
  3. Use waitForSelector — Don’t assume elements are immediately available after navigation
  4. Test in headed mode locally — Set headless: false during 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