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:
- Unit tests for background/content script logic (Jest, no browser)
- Integration tests for popup UI (navigate directly to
chrome-extension://ID/popup.html) - E2E tests for content script effects (load target page, check DOM changes)
- Message passing tests via
chrome.runtime.sendMessagein 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.