Or press ESC to close.

Testing Chrome Extensions with Puppeteer

Aug 17th 2025 15 min read
medium
javascriptES6
puppeteer24.16.2
jest30.0.5
browser
chrome139.0.7
extensions

Chrome extensions are powerful tools that enhance browser functionality, but testing them can be challenging. Unlike regular web applications, extensions operate in a unique environment with content scripts, background scripts, and popup interfaces. In this comprehensive guide, we'll explore how to use Puppeteer to automate testing of Chrome extension functionality, using a real-world example of the Firefly Inspector extension that highlights DOM elements.

Why Test Chrome Extensions?

Before diving into the technical implementation, let's understand why automated testing is crucial for Chrome extensions:

Setting Up the Test Environment

Prerequisites

First, ensure you have the necessary dependencies installed:

                
npm install --save-dev puppeteer jest
                

Our test setup needs to accomplish several key tasks:

Browser Launch Configuration

The foundation of extension testing is launching Chrome with the extension pre-loaded:

                
const puppeteer = require("puppeteer");
const path = require("path");

describe("Firefly Inspector Chrome Extension", () => {
  let browser;
  let page;
  const extensionPath = path.join(__dirname, "../firefly-inspector");

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: false,
      devtools: false,
      args: [
        `--disable-extensions-except=${extensionPath}`,
        `--load-extension=${extensionPath}`,
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-dev-shm-usage"
      ],
    });
  });
                

Key Configuration Points:

Extension Discovery and Verification

One of the trickiest parts of testing Chrome extensions with Puppeteer is figuring out how to detect when your extension has actually loaded. Extensions can be structured differently depending on whether they follow Manifest V2, Manifest V3, or are very minimal (like popup-only extensions). That means we need a strategy that can handle all of these cases.

The first thing we do is give the browser a little breathing room to finish loading the extension:

                
await new Promise((resolve) => setTimeout(resolve, 2000));
                

Since Puppeteer doesn't have a direct API for “find my extension,” we have to query the browser for all its current targets (a target is basically a page, background page, or service worker the browser is running):

                
const targets = await browser.targets();
                

Now we try to locate the extension. For Manifest V3 extensions, the background logic runs inside a service worker, so the most reliable way is to look for a target of type service_worker that starts with the chrome-extension:// URL scheme:

                
let extensionTarget = targets.find(
  (target) =>
    target.type() === "service_worker" &&
    target.url().startsWith("chrome-extension://")
);
                

But not all extensions use service workers. Manifest V2 extensions relied on background pages, and some simple extensions might only have a popup without any background process at all. That's why we add a fallback: if no service worker is found, we look for a target that points to a popup page:

                
if (!extensionTarget) {
  extensionTarget = targets.find(
    (target) =>
      target.url().startsWith("chrome-extension://") &&
      target.url().includes("popup.html")
  );
}
                

With this approach, we cover multiple extension architectures:

By checking all of these, our test can reliably detect the extension regardless of how it's built.

Alternative Extension ID Discovery

If the target-based approach fails, we can extract the extension ID from Chrome's extensions management page:

                
if (!extensionTarget) {
  const extensionsPage = await browser.newPage();
  await extensionsPage.goto("chrome://extensions/");
  
  // Enable developer mode to see extension details
  await extensionsPage.click("#devMode");
  
  // Extract extension ID by name matching
  const extensionCards = await extensionsPage.$("extensions-item");
  // ... ID extraction logic
}
                

This method is more reliable for complex extension structures but requires additional navigation steps.

Creating Test Pages

Extensions interact with web content, so we need controlled test environments:

                
beforeEach(async () => {
  const testHtml = `
    <!DOCTYPE html>
    <html>
    <head>
      <title>Test Page</title>
    </head>
    <body>
      <h1>Firefly Inspector Test Page</h1>
      
      <div data-test="value1" id="element1">Element 1</div>
      <div data-test="value2" id="element2">Element 2</div>
      <div data-custom="custom1" id="element3">Element 3</div>
      <span data-test="value3" id="element4">Span element</span>
      <p data-test="" id="element5">Empty data-test</p>
      <div id="element6">No target attribute</div>
    </body>
    </html>
  `;

  const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(testHtml)}`;
  await page.goto(dataUrl);
  await new Promise((resolve) => setTimeout(resolve, 2000));
});
                

Why Use Data URLs?

The test page includes various element types to thoroughly test the extension's element detection logic.

Testing Extension Functionality

Now comes the core testing logic. Our example tests an extension that highlights DOM elements based on tag and attribute criteria:

Testing Element Highlighting

Once we've confirmed the extension is loaded, the next step is to test whether its core functionality works—in this case, highlighting elements on the page.

The test begins by opening the extension's popup page. Since extensions are loaded under a unique chrome-extension:// URL, we build the popup URL using the extensionId we captured earlier:

                
const extensionId = global.extensionId;
const popupUrl = `chrome-extension://${extensionId}/popup.html`;
const popupPage = await browser.newPage();
await popupPage.goto(popupUrl);
                

With the popup open, we can interact with its form fields just like we would in a normal Puppeteer test. Here we wait for the inputs to be available and then fill them in:

                
await popupPage.waitForSelector("#tagInput");
await popupPage.type("#tagInput", "div");
await popupPage.type("#attributeInput", "data-test");
                

The extension also requires a color value for the highlight. Instead of typing it manually, we directly set the value of the color input field using evaluate:

                
await popupPage.evaluate(() => {
  document.getElementById("colorInput").value = "#ff0000";
});
                

Finally, we simulate the user action that triggers the highlighting—clicking the button:

                
await popupPage.click("#highlightButton");
                

Because the highlighting logic may take a moment to apply changes to the DOM, we pause briefly before moving on:

                
await new Promise((resolve) => setTimeout(resolve, 2000));
                

At this point, the extension should have applied highlights to all matching elements on the main page, and we'sre ready to verify the results.

Verifying Results

After the extension has run, the real test is checking whether it did what we expected. We do this by looking at the main page and inspecting the elements that match our selector (div[data-test]).

Using page.evaluate, we grab all matching elements, then for each one, check whether the extension applied a visual highlight. In this case, the highlight is indicated by a box shadow, so we extract its computed style:

                
const highlightedElements = await page.evaluate(() => {
  const elements = document.querySelectorAll("div[data-test]");
  const highlighted = [];

  elements.forEach((element) => {
    const computedStyle = window.getComputedStyle(element);
    const boxShadow = computedStyle.boxShadow;

    if (boxShadow && boxShadow !== "none") {
      highlighted.push({
        id: element.id,
        hasHighlight: true,
        dataTest: element.getAttribute("data-test"),
        boxShadow: boxShadow,
      });
    }
  });

  return highlighted;
});
                

Now we can write our assertions. These tests serve multiple purposes:

                
expect(highlightedElements.length).toBeGreaterThan(0);
expect(highlightedElements.some((el) => el.id === "element1")).toBe(true);
expect(highlightedElements.some((el) => el.id === "element2")).toBe(true);

expect(highlightedElements.some((el) => el.id === "element5")).toBe(false);
                

By combining DOM inspection with computed styles, we're not just checking that the extension ran—we're verifying that it applied the correct styling rules to the right elements and respected the business logic for when not to highlight.

Handling Content Script Communication

Chrome extensions often rely on message passing between different components. Our test includes a fallback mechanism for direct content script testing:

                
const directResult = await page.evaluate(() => {
  function highlightElements(tag, attribute, color) {
    const selector = tag === "*" ? `[${attribute}]` : `${tag}[${attribute}]`;
    const elements = Array.from(document.querySelectorAll(selector));

    const elementsToHighlight = elements.filter((element) => {
      const value = element.getAttribute(attribute);
      return value && value.trim() !== "";
    });

    elementsToHighlight.forEach((element) => {
      element.style.setProperty(
        "box-shadow",
        `0 0 0 3px ${color}`,
        "important"
      );
    });

    return elementsToHighlight.length > 0;
  }

  return highlightElements("div", "data-test", "#ff0000");
});
                

This approach ensures that even if the extension's internal communication fails, we can still verify that the core functionality works correctly.

Performance Considerations

Extension testing can be slower than regular web testing due to:

Optimize your tests by:

Conclusion

Testing Chrome extensions with Puppeteer requires understanding the unique architecture of browser extensions and adapting traditional web testing approaches. Key takeaways:

By following these patterns and practices, we can build a robust testing suite that ensures our Chrome extension works reliably across different scenarios and user interactions. The investment in automated testing pays dividends in deployment confidence and user satisfaction.

Remember that extension testing is an evolving field, especially with Chrome's transition from Manifest V2 to V3. Stay updated with the latest Puppeteer releases and Chrome extension development best practices to keep your tests effective and maintainable.

For the complete code example, visit our GitHub page. Until next time 👋.