Chrome Extension Testing: Complete Guide (2026)

Chrome Extension Testing: Complete Guide (2026)

To test Chrome extensions with Selenium, load the unpacked extension using ChromeOptions: options.add_argument(f"--load-extension=/path/to/extension"). For Playwright, use BrowserContext with args=["--load-extension=..."] and ignore_default_args=["--disable-extensions"]. Test the popup UI by navigating to chrome-extension://EXTENSION_ID/popup.html.

Key Takeaways

Load the extension as unpacked, not as .crx. Chrome restricts .crx installs for automation. Always load the unpacked source directory instead.

Get the extension ID from chrome://extensions. After loading, the ID is shown in the UI. In automation, extract it from the loaded extension URL or set a static ID in your manifest.

Test the popup by navigating directly to its URL. Chrome extension popups are just HTML pages. Navigate to chrome-extension://EXTENSION_ID/popup.html to test the UI directly without clicking the toolbar icon.

Background service workers require a different testing approach. You can't directly inspect background scripts — use chrome.runtime.sendMessage from content scripts or inject scripts into the extension context.

Content scripts run in page context. Test content script behavior by loading the target page and inspecting DOM modifications or listening for custom events the content script fires.

Why Chrome Extension Testing is Hard

Chrome extensions run in isolated contexts:

  • Popup: A mini HTML page that opens when you click the icon
  • Background service worker: Runs persistently, no DOM access
  • Content scripts: Injected into web pages, limited API access
  • Options page: A settings UI page

Testing spans multiple isolated contexts with different permissions. The testing complexity is high — but essential before publishing to the Chrome Web Store.

Loading Extensions in Selenium Python

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import os

# Path to your unpacked extension directory
extension_path = os.path.abspath("./my-extension")

options = Options()
options.add_argument(f"--load-extension={extension_path}")

# Disable extension popup prompts
options.add_argument("--disable-popup-blocking")
options.add_experimental_option("excludeSwitches", ["enable-automation"])

driver = webdriver.Chrome(options=options)
driver.get("https://example.com")

The extension loads automatically. You can verify it's loaded:

driver.get("chrome://extensions/")
# Extension should appear in the list

Getting the Extension ID

The extension ID is needed to navigate to popup/options pages:

Method 1: From Manifest (Set a Static ID)

Add a key to your manifest.json to get a stable ID:

{
  "name": "My Extension",
  "version": "1.0",
  "manifest_version": 3,
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
}

Method 2: Parse from the Extensions Page

import re
from selenium.webdriver.common.by import By

driver.get("chrome://extensions/")

# Enable developer mode to see IDs
driver.execute_script("""
  document.querySelector('extensions-manager').shadowRoot
    .querySelector('#toolbar').shadowRoot
    .querySelector('#devMode').click()
""")

# Find extension ID from the page
page_source = driver.page_source
extension_id = re.search(r'ID: ([a-z]{32})', page_source)
if extension_id:
    print(f"Extension ID: {extension_id.group(1)}")

Method 3: From Background Page URL

driver.get("chrome://extensions/")
# Navigate to background page and capture the ID from the URL

Testing the Popup UI

Extension popups are HTML pages — navigate directly to them:

extension_id = "abcdefghijklmnopqrstuvwxyz123456"
driver.get(f"chrome-extension://{extension_id}/popup.html")

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

# Test popup UI elements
toggle = wait.until(EC.element_to_be_clickable((By.ID, "enable-toggle")))
assert toggle.is_displayed()

# Click the toggle
toggle.click()

# Verify state changed
status_label = driver.find_element(By.ID, "status-label")
assert status_label.text == "Enabled"

Testing Content Script Effects

Content scripts modify web pages. Test by loading the page and checking DOM changes:

# Load a page where your content script is injected
driver.get("https://example.com")

import time
time.sleep(1)  # Wait for content script to run

# Check if content script added its elements
injected_button = driver.find_element(By.ID, "my-extension-button")
assert injected_button.is_displayed()

# Check if content script modified existing elements
modified_element = driver.find_element(By.CSS_SELECTOR, ".highlighted-text")
assert "highlight" in modified_element.get_attribute("class")

For content scripts that listen to page events:

# Trigger the event your content script responds to
driver.execute_script("""
    document.dispatchEvent(new CustomEvent('myapp:userAction', {
        detail: { action: 'purchase' }
    }));
""")

time.sleep(0.5)

# Check what the content script did in response
result = driver.execute_script("return window.__extensionResult")
assert result == "tracked"

Testing with Playwright Python

Playwright supports Chrome extensions with persistent contexts:

import asyncio
from playwright.async_api import async_playwright
import os

async def test_extension():
    extension_path = os.path.abspath("./my-extension")

    async with async_playwright() as p:
        # Must use persistent context for extensions
        context = await p.chromium.launch_persistent_context(
            user_data_dir="/tmp/test-user-data",
            headless=False,  # Extensions don't work fully in headless
            args=[
                f"--disable-extensions-except={extension_path}",
                f"--load-extension={extension_path}",
            ],
        )

        # Get extension ID from background page
        background = context.background_pages[0] if context.background_pages else None

        # Wait for background page to initialize
        await asyncio.sleep(1)

        # Get extension ID from background page URL
        if context.background_pages:
            bg_url = context.background_pages[0].url
            extension_id = bg_url.split("/")[2]

            # Navigate to popup
            popup_page = await context.new_page()
            await popup_page.goto(f"chrome-extension://{extension_id}/popup.html")

            # Test popup
            await popup_page.click("#enable-toggle")
            status = await popup_page.text_content("#status-label")
            assert status == "Enabled"

        await context.close()

asyncio.run(test_extension())

Testing Storage and Permissions

Extensions use chrome.storage API. Test storage changes:

# Check extension storage via injected script
storage_value = driver.execute_script("""
    return new Promise((resolve) => {
        chrome.storage.sync.get('settings', (data) => {
            resolve(data.settings);
        });
    });
""")
print(f"Extension settings: {storage_value}")

# Set storage for testing
driver.execute_script("""
    chrome.storage.sync.set({
        settings: { darkMode: true, notifications: false }
    });
""")

Testing Background Service Worker (MV3)

Manifest V3 uses service workers instead of background pages. They're harder to test directly:

# Trigger message from content script to background
driver.execute_script("""
    chrome.runtime.sendMessage(
        { action: 'getData', url: window.location.href },
        (response) => { window.__bgResponse = response; }
    );
""")

import time
time.sleep(0.5)

response = driver.execute_script("return window.__bgResponse")
assert response["status"] == "success"

Unit Testing Extension JavaScript

For business logic, unit test with Jest without a browser:

// __tests__/storage.test.js
import { saveSettings, loadSettings } from '../src/storage.js';

// Mock chrome.storage API
global.chrome = {
  storage: {
    sync: {
      set: jest.fn((data, callback) => callback && callback()),
      get: jest.fn((keys, callback) => callback({ settings: {} })),
    },
  },
};

test('saves settings correctly', async () => {
  const settings = { darkMode: true };
  await saveSettings(settings);
  expect(chrome.storage.sync.set).toHaveBeenCalledWith({ settings });
});

test('loads default settings when empty', async () => {
  const settings = await loadSettings();
  expect(settings.darkMode).toBe(false);
});

Testing on Different Chrome Versions

# .gitlab-ci.yml - Test on multiple Chrome versions
test-extension:
  parallel:
    matrix:
      - CHROME_VERSION: ["stable", "beta", "canary"]
  image: selenium/standalone-chrome:${CHROME_VERSION}
  script:
    - pytest tests/extension/

Common Chrome Extension Testing Challenges

Headless Mode Limitations

Chrome extensions don't fully work in headless mode. For CI:

options = Options()
# Use headless=new (Chrome 112+) which supports extensions better
options.add_argument("--headless=new")
options.add_argument(f"--load-extension={extension_path}")

Or use a virtual display (Linux CI servers):

# In CI before running tests
Xvfb :99 -screen 0 1920x1080x24 &
<span class="hljs-built_in">export DISPLAY=:99

Extension Popup Doesn't Open on Click

The browser toolbar button is outside the web page DOM. Navigate directly instead:

# Don't try to click the toolbar icon
# ❌ This won't work reliably
toolbar_button = driver.find_element(By.CSS_SELECTOR, ".extension-button")

# ✅ Navigate directly to popup URL
driver.get(f"chrome-extension://{extension_id}/popup.html")

Extension ID Changes Per Install

Use a fixed ID via manifest key, or dynamically discover the ID after loading.

Cross-Origin Restrictions

Content scripts can access page DOM but not all page JavaScript. Test what your content script can actually access:

# Content script can read DOM
text = driver.execute_script("return document.body.textContent")

# Content script cannot access page's JavaScript variables directly
# (unless the page exposes them in DOM or via custom events)

Full Test Suite Structure

tests/
  extension/
    test_popup.py          # Popup UI tests
    test_content_script.py # Content script injection tests
    test_options_page.py   # Settings page tests
    test_background.py     # Message passing tests
  unit/
    __tests__/
      storage.test.js      # Storage logic
      background.test.js   # Background worker logic
      content.test.js      # Content script logic
  conftest.py              # Shared fixtures (driver setup, extension loading)
# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import os

EXTENSION_PATH = os.path.abspath("./my-extension")
EXTENSION_ID = "your-extension-id-here"

@pytest.fixture(scope="session")
def driver_with_extension():
    options = Options()
    options.add_argument(f"--load-extension={EXTENSION_PATH}")
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(options=options)
    driver.extension_id = EXTENSION_ID

    yield driver

    driver.quit()

Alternative: AI-Powered Browser Testing

Manual Selenium setup for extension testing involves significant boilerplate. HelpMeTest can test the visible behavior of extension-enhanced pages with natural language:

Go to https://example.com
Verify the "Save with Extension" button appears in the top right
Click the "Save with Extension" button
Verify the confirmation toast appears
Go to the extension popup at chrome-extension://EXTENSION_ID/popup.html
Verify the saved item count increased to 1

This tests what users actually experience — the extension's effect on pages and its UI — without dealing with Chrome's internal APIs.

Summary

Chrome extension testing strategy:

  1. Unit tests for background/content script logic (Jest, no browser)
  2. Integration tests for popup UI (navigate directly to chrome-extension://ID/popup.html)
  3. E2E tests for content script effects (load target page, check DOM changes)
  4. Message passing tests via chrome.runtime.sendMessage in injected scripts

The hardest part is loading the extension reliably in CI. Use --headless=new or a virtual display. Fix the extension ID via manifest key for stable test URLs.

Read more