javascriptroom blog

How to Detect and Test Chrome Extensions with Puppeteer: Check Content Scripts & Test Mode UI

Chrome extensions enhance browser functionality, from ad blockers to productivity tools, but ensuring they work reliably across websites and user interactions is critical. Manual testing is time-consuming, especially for edge cases like content script injection, background logic, or UI components (e.g., popups, options pages).

Puppeteer, a Node.js library developed by Google, simplifies this by letting you control Chrome (or Chromium) programmatically. With Puppeteer, you can automate testing of extension behavior, validate content script injection, and interact with extension UIs—all in a headless or visible browser environment.

This guide will walk you through detecting Chrome extensions in Puppeteer, testing content scripts, and validating UI components in "test mode." We’ll use practical examples and code snippets to make complex concepts easy to follow.

2025-12

Table of Contents#

  1. Prerequisites
  2. Detecting Chrome Extensions in Puppeteer
    • 2.1 Launching Puppeteer with Extensions
    • 2.2 Verifying Extension Load Status
    • 2.3 Accessing Background Pages/Service Workers
  3. 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
  4. Testing Extension UI in Test Mode
    • 4.1 Testing Popup UIs
    • 4.2 Testing Options Pages
    • 4.3 Testing Sidepanels (Manifest V3+)
  5. Advanced Tips for Reliable Testing
  6. Conclusion
  7. 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:

  1. Launch Puppeteer with the extension.
  2. Navigate to a test page matching the matches pattern.
  3. 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:

  1. Find the popup target (triggered by clicking the extension icon).
  2. Use Puppeteer’s Page methods 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() or page.waitForFunction() to wait for content scripts/popups to load:
    await page.waitForSelector('#extension-hello-btn', { timeout: 2000 });  
  • Debug with DevTools: Launch Puppeteer with devtools: true to 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.

References#