ScreenshotNeo

BlogHow-to

How to Preload a Chrome Extension for Browser Testing

Load an unpacked extension or CRX when Chrome starts, then wait for its worker and test it in an isolated browser profile.

By the ScreenshotNeo team4 October 20268 min read

To preload a Chrome extension for browser testing, provide it in Chrome’s launch options before the browser starts. With ChromeDriver, pass an unpacked extension directory using load-extension, or add a packaged .crx with addExtensions. With Puppeteer, use its extension-loading option. For unattended tests that need extensions, use Chrome’s new headless mode; Chrome’s guidance says the old headless mode does not support extension loading.

This guide covers Selenium with Java, Puppeteer with JavaScript, headless and CI setup, extension worker and popup access, test isolation, troubleshooting, and an API alternative for capturing screenshots of pages after your extension has changed them.

1. Choose the extension artifact and test tool

Use the artifact your build actually produces. An unpacked extension is a directory containing the extension files, including manifest.json. A packed extension is a .crx file. ChromeDriver supports both, but the options differ. Puppeteer’s documented workflow loads the extension directory at launch. See Chrome’s ChromeDriver extension instructions and Puppeteer extension testing tutorial.

Situation Use Key consideration
You build an unpacked extension directory ChromeDriver load-extension or Puppeteer extension loading Resolve the directory to an absolute path before launching Chrome.
You have a packaged CRX ChromeDriver addExtensions Pass the CRX file, not its containing directory.
You need extension behavior in CI/headless Chrome’s new headless mode Use --headless=new if your automation library does not already select it.
You need to test popup or worker behavior Wait for the extension context after launch Extension startup is asynchronous; do not assume it is ready immediately.

2. Load the extension with Selenium and ChromeDriver

Unpacked extension directory

Java example using Selenium’s ChromeOptions and ChromeDriver:

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.nio.file.Path;

public class ExtensionTest {
  public static void main(String[] args) {
    Path extensionDir = Path.of("build", "chrome-extension").toAbsolutePath();

    ChromeOptions options = new ChromeOptions();
    options.addArguments("load-extension=" + extensionDir);
    // For CI, enable Chrome's new headless mode if needed:
    // options.addArguments("--headless=new");

    ChromeDriver driver = new ChromeDriver(options);
    try {
      driver.get("https://example.com");
      System.out.println(driver.getTitle());
      // Add assertions for behavior the extension exposes to the page.
    } finally {
      driver.quit();
    }
  }
}

The directory must exist in the environment where ChromeDriver runs. In a build pipeline, build the extension first and point to the generated directory; do not point to a source folder unless it contains the complete loadable extension, including manifest.json.

Packaged CRX file

import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import java.io.File;

public class PackagedExtensionTest {
  public static void main(String[] args) {
    ChromeOptions options = new ChromeOptions();
    options.addExtensions(new File("build/my-extension.crx"));

    ChromeDriver driver = new ChromeDriver(options);
    try {
      driver.get("https://example.com");
      // Assert the extension's user-visible effect.
    } finally {
      driver.quit();
    }
  }
}

Choose one loading form that matches your artifact. Chrome’s ChromeDriver documentation shows the unpacked directory argument and CRX installation as separate approaches.

3. Load the extension with Puppeteer

Chrome’s Puppeteer tutorial launches Chrome with enableExtensions set to the extension directory, then waits for its service worker. The precise API can depend on your Puppeteer version, so check the version you have installed against the official tutorial.

import puppeteer from 'puppeteer';
import path from 'node:path';

const extensionPath = path.resolve('build/chrome-extension');
const browser = await puppeteer.launch({
  headless: false,
  pipe: true,
  enableExtensions: [extensionPath],
});

try {
  const workerTarget = await browser.waitForTarget(
    target => target.type() === 'service_worker'
      && target.url().startsWith('chrome-extension://'),
    { timeout: 10_000 }
  );

  const worker = await workerTarget.worker();
  if (!worker) throw new Error('Extension service worker did not become available');

  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  console.log(await page.title());
} finally {
  await browser.close();
}

The worker URL check above identifies an extension worker generically. If the browser has other extensions, narrow the match to the expected extension ID or another stable property of your test build. Use a finite timeout and include enough context in the failure message to diagnose startup problems. The official tutorial’s example dependency range is illustrative rather than a statement of the latest Puppeteer version.

4. Run extension tests in headless mode and CI

  1. Build the extension and confirm its output directory contains manifest.json.
  2. Make the path available to the test process. Resolve it to an absolute path to avoid dependence on the runner’s working directory.
  3. Launch Chrome with the extension-loading option for your automation library.
  4. For unattended extension tests, use Chrome’s new headless mode, --headless=new, unless your library already configures it. Chrome’s end-to-end testing guide says the old headless mode does not support loading extensions.
  5. Wait for the worker or other extension context you need, with a bounded timeout.
  6. Run assertions against the extension’s user-visible behavior where practical, then close the browser so the next test starts cleanly.

Chrome lists Puppeteer/Playwright, Selenium, and WebDriverIO as testing-library options, but loading APIs are library-specific. Do not copy ChromeDriver’s argument into another library without checking its documentation. See Chrome’s end-to-end testing guide.

5. Wait for extension workers, pages, and popups

Extension startup is asynchronous. For a Manifest V3 extension, wait for its service-worker target before trying to use worker functionality. If the worker does not appear before the timeout, fail with a useful message rather than continuing with a test that will fail unpredictably later.

To test a popup, Chrome documents opening it through action.openPopup() when the automation setup supports that API, or navigating to the popup page in another tab. Extension pages use the chrome-extension://<id>/... origin. Where possible, test the behavior a user sees; direct access to an extension page is useful when the test specifically needs that context.

Chrome also notes a Selenium-specific lifecycle caveat: ChromeDriver attaches a debugger to service workers, which can prevent them from stopping as they normally would. If the test is specifically about normal worker termination, account for this behavior or choose a testing approach that can observe the lifecycle you need.

6. Keep browser state isolated between tests

A fresh browser session or profile reduces state leakage from cookies, local storage, extension storage, permissions, and open tabs. Chrome’s Puppeteer tutorial cautions that reusing a browser can let one test affect another. ChromeDriver ordinarily creates a temporary profile; when a test deliberately needs a configured profile, ChromeDriver also supports a user-data-dir argument. See ChromeDriver capabilities and ChromeOptions.

  • Use a new browser per test or test group when tests mutate extension or website state.
  • Use a dedicated temporary profile if a test needs profile configuration; avoid sharing it across parallel workers.
  • Close the browser in a finally block so failed assertions do not leave processes or profiles behind.
  • Do not rely on an extension ID being stable unless your test setup explicitly makes it stable.

A fixed extension ID can matter when a test allow-lists an extension origin or opens an extension page by ID. Chrome links to a separate procedure for a consistent ID from its end-to-end testing guide; follow that procedure if your assertions require it.

7. Common problems and fixes

Symptom Likely cause Fix
Extension is missing after launch Wrong artifact type, incorrect path, or missing extension files For unpacked loading, pass the directory containing manifest.json. For a CRX, use the CRX installation option. Resolve and verify the path in the test runner environment.
Extension works locally but not in headless CI Old headless mode or a browser launch configuration that omits extension support Use --headless=new and confirm the automation library supports extension loading in the installed version.
Test intermittently cannot find the service worker The test checks too early or matches the wrong worker Wait for the expected extension worker with a bounded timeout. Match its ID or URL more specifically if other extensions are loaded.
Chrome rejects the load argument The path points to a file instead of an unpacked directory, or vice versa Match the option to the artifact: directory for load-extension, CRX file for addExtensions.
Tests pass alone but fail in a suite Shared profile or browser state is leaking between cases Use separate sessions or profiles and ensure each browser closes even after an assertion failure.
Popup page cannot be reached Wrong extension ID or popup path, or popup has not been opened in the expected context Resolve the ID from the running extension, confirm the popup path in its manifest, and use the documented popup-opening method supported by your automation library.
Worker termination assertion never passes under Selenium ChromeDriver’s debugger attachment can keep service workers alive Do not assume normal termination under this setup; use a strategy that accounts for the ChromeDriver lifecycle behavior.

8. Performance, reliability, and distribution notes

Extension tests add browser startup and extension initialization to the work each session must perform. Reuse may reduce repeated startup, but shared browser state can make tests interfere with one another. Choose session reuse only when the tests are designed to share a profile; otherwise, isolation is usually easier to reason about. The official sources do not provide a universal speed benchmark or a single best testing library.

Make failures diagnosable: log the resolved extension path, browser launch mode, and whether the expected worker appeared before timeout. Keep waits bounded, and assert an observable outcome on the page instead of treating successful browser launch as proof that the extension worked.

Loading an unpacked extension from a local directory is a development and testing workflow. Chrome says unpacked extensions should only be used to load trusted development code. It is not a distribution route. For distribution, Chrome documents the Chrome Web Store and self-hosting in managed environments, with policy constraints on self-hosting. See Chrome’s distribution guidance.

9. Or skip the browser setup

If your goal is to capture a page after your extension has changed it, run the browser test above and capture the resulting page, or use ScreenshotNeo for a website screenshot through one API request. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can ChromeDriver load an unpacked extension?

Yes. Pass the unpacked directory with the load-extension Chrome argument.

Can I load a CRX instead?

Yes. Use ChromeDriver’s addExtensions option with the CRX file.

Can I use a screenshot API to verify my extension is installed?

A screenshot API captures a website page; it does not replace browser automation for installing an extension or asserting its internal state. Use the browser test for those checks.