Table of Contents#
- Prerequisites
- Detecting Chrome Extensions in Puppeteer
- 2.1 Launching Puppeteer with Extensions
- 2.2 Verifying Extension Load Status
- 2.3 Accessing Background Pages/Service Workers
- Testing Content Scripts with Puppeteer
- 3.1 Validating Content Script Injection
- 3.2 Testing DOM Manipulation by Content Scripts
- 3.3 Testing Messaging Between Content Scripts and Background
- Testing Extension UI in Test Mode
- 4.1 Testing Popup UIs
- 4.2 Testing Options Pages
- 4.3 Testing Sidepanels (Manifest V3+)
- Advanced Tips for Reliable Testing
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have the following set up:
- Node.js: Install Node.js (v14+ recommended) to run Puppeteer scripts.
- Puppeteer: Install via npm:
npm install puppeteer - Basic Chrome Extension Knowledge: Familiarity with extension structure (manifest.json, content scripts, background logic). We’ll use Manifest V3 (the latest standard) for examples.
- Sample Extension: For testing, create a simple extension or use an existing one. We’ll reference a sample extension with:
- A content script that injects UI elements into web pages.
- A background service worker (Manifest V3).
- A popup UI (popup.html) with interactive buttons.
Detecting Chrome Extensions in Puppeteer#
To test an extension, you first need to ensure Puppeteer loads it correctly. Here’s how to detect and verify extension behavior.
2.1 Launching Puppeteer with the Extension#
Puppeteer can load unpacked extensions using the --load-extension Chrome flag. Use puppeteer.launch() with the args option to specify the extension path:
const puppeteer = require('puppeteer');
async function launchWithExtension() {
const browser = await puppeteer.launch({
headless: false, // Set to 'new' for headless mode (Chrome 112+)
args: [
`--load-extension=${path.join(__dirname, 'path/to/your/extension')}`, // Path to unpacked extension
'--disable-extensions-except=path/to/your/extension', // Optional: Load only your extension
],
});
return browser;
} headless: false: Runs Chrome visibly for debugging (set to'new'for headless testing).--load-extension: Loads your unpacked extension from the specified directory.
2.2 Verifying Extension Load Status#
After launching, confirm the extension is loaded by checking Puppeteer’s targets (Chrome’s open pages/contexts). Extensions run in separate targets (e.g., background service workers, popups).
Use browser.targets() to list all targets and filter by extension URLs (format: chrome-extension://<extension-id>/...):
async function isExtensionLoaded(browser) {
const targets = await browser.targets();
const extensionTarget = targets.find(target =>
target.url().startsWith('chrome-extension://')
);
return !!extensionTarget; // Returns true if extension target exists
}
// Usage:
const browser = await launchWithExtension();
const loaded = await isExtensionLoaded(browser);
console.log('Extension loaded:', loaded); // Should log "true" 2.3 Accessing Background Pages/Service Workers#
Manifest V3 uses service workers instead of background pages. To verify the extension’s background logic (e.g., initialization, event listeners), access the service worker target:
async function getBackgroundServiceWorker(browser) {
const targets = await browser.targets();
const backgroundTarget = targets.find(target =>
target.type() === 'service_worker' && target.url().startsWith('chrome-extension://')
);
if (!backgroundTarget) throw new Error('Background service worker not found');
return await backgroundTarget.page(); // Get a Page object to interact with the service worker
}
// Example: Check if background initialized correctly
const backgroundPage = await getBackgroundServiceWorker(browser);
const isInitialized = await backgroundPage.evaluate(() => {
return typeof chrome.runtime !== 'undefined' && chrome.runtime.id; // Verify runtime is available
});
console.log('Background initialized:', isInitialized); // Should log "true" Testing Content Scripts with Puppeteer#
Content scripts run in the context of web pages, modifying DOM or interacting with page data. Use Puppeteer to validate their injection and behavior.
3.1 Validating Content Script Injection#
Content scripts inject into pages matching matches patterns in manifest.json. To test injection:
- Launch Puppeteer with the extension.
- Navigate to a test page matching the
matchespattern. - Check for content script-specific DOM changes or variables.
Sample Manifest V3 content_scripts Entry:
{
"content_scripts": [
{
"matches": ["<all_urls>"], // Injects into all pages
"js": ["content-script.js"],
"run_at": "document_idle"
}
]
} Content Script (content-script.js):
// Add a data attribute to the page body to signal injection
document.body.dataset.extensionInjected = 'true'; Test Script:
async function testContentScriptInjection(browser) {
const page = await browser.newPage();
await page.goto('https://example.com'); // Page matching "matches" pattern
// Check if content script injected the data attribute
const isInjected = await page.evaluate(() => {
return document.body.dataset.extensionInjected === 'true';
});
console.log('Content script injected:', isInjected); // Should log "true"
await page.close();
} 3.2 Testing DOM Manipulation by Content Scripts#
Content scripts often modify page UI (e.g., adding buttons, highlighting text). Test these changes by querying the DOM in the page context.
Content Script Example (adds a "Hello Extension" button):
// content-script.js
const button = document.createElement('button');
button.id = 'extension-hello-btn';
button.textContent = 'Hello Extension';
document.body.prepend(button); Test Script:
async function testContentScriptDOM(browser) {
const page = await browser.newPage();
await page.goto('https://example.com');
// Check if the button was added
const buttonExists = await page.$eval('#extension-hello-btn', btn => {
return btn.textContent === 'Hello Extension';
});
console.log('Button added by content script:', buttonExists); // Should log "true"
await page.close();
} 3.3 Testing Messaging Between Content Scripts and Background#
Content scripts communicate with the background via chrome.runtime.sendMessage. Test this flow to ensure messages are sent/received.
Background Service Worker (background.js):
// Listen for messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'PING') {
sendResponse({ type: 'PONG', data: 'Hello from background!' });
}
}); Content Script:
// Send "PING" to background and log response
chrome.runtime.sendMessage({ type: 'PING' }, (response) => {
console.log('Background response:', response.data); // Should log "Hello from background!"
}); Test Script:
async function testContentScriptMessaging(browser) {
const page = await browser.newPage();
await page.goto('https://example.com');
// Listen for content script logs (via page console)
page.on('console', msg => {
if (msg.text().includes('Background response:')) {
console.log('Test passed:', msg.text());
}
});
// Wait for message to be sent/received
await new Promise(resolve => setTimeout(resolve, 1000));
await page.close();
} Testing Extension UI in Test Mode#
Extensions have UIs like popups, options pages, and sidepanels. Puppeteer can interact with these by targeting their URLs.
4.1 Testing Popup UIs#
Popups are HTML pages shown when clicking the extension icon. To test them:
- Find the popup target (triggered by clicking the extension icon).
- Use Puppeteer’s
Pagemethods to interact with popup elements.
Sample Popup (popup.html):
<!DOCTYPE html>
<html>
<body>
<button id="popup-btn">Click Me</button>
<p id="popup-status"></p>
<script src="popup.js"></script>
</body>
</html> Popup Logic (popup.js):
document.getElementById('popup-btn').addEventListener('click', () => {
document.getElementById('popup-status').textContent = 'Clicked!';
}); Test Script:
async function testPopupUI(browser) {
// Step 1: Get the extension ID (needed for popup URL)
const targets = await browser.targets();
const extensionId = targets.find(t => t.url().startsWith('chrome-extension://')).url().split('/')[2];
// Step 2: Navigate directly to the popup URL (avoids needing to click the icon)
const popupPage = await browser.newPage();
await popupPage.goto(`chrome-extension://${extensionId}/popup.html`);
// Step 3: Click the button and check status
await popupPage.click('#popup-btn');
const statusText = await popupPage.$eval('#popup-status', el => el.textContent);
console.log('Popup status after click:', statusText); // Should log "Clicked!"
await popupPage.close();
} 4.2 Testing Options Pages#
Options pages let users configure the extension. Test them by navigating to their URL (defined in manifest.json):
{
"options_ui": {
"page": "options.html",
"open_in_tab": true
}
} Test Script:
async function testOptionsPage(browser, extensionId) {
const optionsPage = await browser.newPage();
await optionsPage.goto(`chrome-extension://${extensionId}/options.html`);
// Example: Fill a form field and save
await optionsPage.type('#api-key-input', 'test-key-123');
await optionsPage.click('#save-btn');
// Verify storage was updated (check background storage)
const backgroundPage = await getBackgroundServiceWorker(browser);
const storedKey = await backgroundPage.evaluate(() => {
return new Promise(resolve => {
chrome.storage.local.get('apiKey', (result) => resolve(result.apiKey));
});
});
console.log('Stored API key:', storedKey); // Should log "test-key-123"
await optionsPage.close();
} 4.3 Testing Sidepanels (Manifest V3+)#
Manifest V3 introduced sidepanels (persistent panels next to web pages). Test them similarly to popups:
{
"side_panel": {
"default_path": "sidepanel.html"
}
} Test Script:
async function testSidepanel(browser, extensionId) {
const sidepanelPage = await browser.newPage();
await sidepanelPage.goto(`chrome-extension://${extensionId}/sidepanel.html`);
// Check for sidepanel content
const hasContent = await sidepanelPage.$eval('h1', el => el.textContent === 'My Sidepanel');
console.log('Sidepanel loaded:', hasContent); // Should log "true"
await sidepanelPage.close();
} Advanced Tips for Reliable Testing#
- Use Stable Extension IDs: For CI/CD, pack the extension to get a fixed ID (avoids dynamic IDs from
--load-extension). - Handle Async Behavior: Use
page.waitForSelector()orpage.waitForFunction()to wait for content scripts/popups to load:await page.waitForSelector('#extension-hello-btn', { timeout: 2000 }); - Debug with DevTools: Launch Puppeteer with
devtools: trueto inspect extension contexts:puppeteer.launch({ devtools: true, ... }); - Isolate Tests: Use
browser.createIncognitoBrowserContext()to avoid storage/cache interference between tests.
Conclusion#
Puppeteer is a powerful tool for automating Chrome extension testing, from validating content script injection to interacting with UIs. By following this guide, you can ensure your extension works reliably across pages and user interactions. Start with simple checks (injection, DOM changes) and逐步 move to complex flows (messaging, UI interactions) to build a robust test suite.