ScreenshotNeo

BlogHow-to

How to Use Puppeteer to Screenshot a Chrome Extension Page

Capture a Chrome extension page with Puppeteer, whether Puppeteer runs inside the extension or from Node.js. Includes runnable examples and troubleshooting.

By the ScreenshotNeo team4 October 20269 min read

There are two ways to use Puppeteer to screenshot a Chrome extension page, and the right code depends on where Puppeteer runs:

  • Inside the extension: use Puppeteer’s browser build with the experimental ExtensionTransport, which connects through Chrome’s restricted chrome.debugger API.
  • In Node.js: launch Chrome with the extension loaded, find the popup page’s target, convert it to a Puppeteer Page, and call the regular screenshot API.

The Node.js approach is usually the simpler fit for automated tests. The extension-internal approach is for code that must run in the extension itself. The extension transport supports one page at a time, so use Chrome’s tabs API to create or select tabs and connect to each needed page separately.

1. Puppeteer running inside the extension

This setup runs browser-compatible Puppeteer code from an extension page or another extension context. It is experimental and differs from Node.js Puppeteer: Chrome exposes DevTools Protocol access through chrome.debugger, and the transport attaches to one page at a time.

Prerequisites

  1. Build a browser-compatible bundle of your code with a bundler such as Rollup or webpack, as described in the Puppeteer Chrome Extensions guide.
  2. Request the Chrome extension permissions required by the extension APIs your code uses, including debugger access and tabs access. Follow Chrome’s current permission and user-consent requirements.
  3. Import the browser entry point from puppeteer-core, not the Node.js Puppeteer entry point.

Runnable capture pattern

import {
  connect,
  ExtensionTransport,
} from 'puppeteer-core/lib/puppeteer/puppeteer-core-browser.js';

async function captureTab(url) {
  // Create or identify the tab before attaching Puppeteer.
  const tab = await chrome.tabs.create({ url });
  if (tab.id === undefined) {
    throw new Error('Chrome did not return a tab ID');
  }

  const transport = await ExtensionTransport.connectTab(tab.id);
  const browser = await connect({ transport });

  try {
    const pages = await browser.pages();
    const page = pages[0];
    if (!page) {
      throw new Error('No page was available after connecting to the tab');
    }

    // Returns screenshot bytes (Uint8Array by default).
    return await page.screenshot({ type: 'png' });
  } finally {
    // Disconnect this Puppeteer connection when the capture is finished.
    await browser.disconnect();
  }
}

const imageBytes = await captureTab('https://example.com');
// Pass imageBytes to an extension-supported download, storage, or display flow.

The extension page must be bundled for the browser; the import above is the browser-specific entry point shown in Puppeteer’s guide. Check the exact export path against the Puppeteer version you install because this extension transport is experimental and may change.

Use the Chrome tabs API to open another URL or select another tab. Do not expect browser.newPage() to create another independently controlled page through this transport. Attach a separate connection to the chosen tab instead. See the official extension guide and ExtensionTransport API.

2. Puppeteer in Node.js testing an extension

For an automated test or build script, run Puppeteer in Node and load the unpacked extension when launching Chrome. Trigger the extension popup, wait for its page target, turn that target into a Page, then use the standard Page.screenshot() method.

Install

npm install puppeteer

Save the following as screenshot-popup.js. Pass the path to your unpacked extension as the first argument. Update the popup URL suffix if your extension uses a different popup file.

const path = require('node:path');
const puppeteer = require('puppeteer');

async function main() {
  const extensionPath = path.resolve(process.argv[2] || './my-extension');
  const browser = await puppeteer.launch({
    headless: true,
    enableExtensions: [extensionPath],
  });

  try {
    // Replace this predicate if your extension has multiple page targets.
    const popupTargetPromise = browser.waitForTarget(
      target => target.type() === 'page' && target.url().endsWith('/popup.html'),
      { timeout: 10000 },
    );

    // Open or trigger the extension popup using your test's chosen mechanism.
    // For example, your test can navigate to the extension's popup URL once
    // the extension ID is known, or interact with the extension action.
    const popupTarget = await popupTargetPromise;
    const popupPage = await popupTarget.asPage();

    await popupPage.screenshot({ path: 'popup.png', type: 'png' });
    console.log('Saved popup.png');
  } finally {
    await browser.close();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

The sample’s target wait only observes a popup; it does not open one. Your test must trigger the extension action or otherwise open the popup. In a test where the extension ID is known, you can navigate to its popup URL. If the ID is discovered dynamically, find the extension’s service worker or another extension target and derive the ID from its URL, then open the popup. Puppeteer’s extension guide documents loading extensions and accessing their targets.

Popup windows can close when they lose focus or the test interacts elsewhere. Keep the popup open while capturing, and make the target predicate specific enough to match the correct extension and page. The normal Puppeteer screenshot guide covers saving a screenshot to a path.

3. Choose the right screenshot output

Page.screenshot() returns a Uint8Array by default. With encoding: 'base64', it returns a base64 string. Supplying path writes the screenshot to a file in Node; a relative path is resolved from the process working directory. Without path, the method returns image data rather than saving a file.

Option Use Notes
path Save an image in Node Relative paths use the process working directory.
type Choose png, jpeg, or webp where supported Check the installed Puppeteer and Chrome version for format support.
quality Set lossy image quality Applies to JPEG and WebP; it is ignored for PNG.
fullPage Capture the full document Defaults to false. For a popup, the visible viewport is often the intended result.
clip Capture a rectangular area Specify the region in page coordinates; clipping can affect capture beyond the viewport.
encoding Choose returned data representation Defaults to bytes; use base64 when a text representation is needed.
omitBackground Request a transparent background Useful for pages whose background is transparent; inspect the result format and consumer.

For a full document screenshot in Node, for example, use await page.screenshot({ path: 'full.png', fullPage: true }). The documented default for captureBeyondViewport is false when no clip is supplied and true when a clip is supplied. See the ScreenshotOptions reference for the version you use.

4. cURL, Python, and Node.js alternatives for ordinary web pages

These examples are for capturing a publicly reachable website through ScreenshotNeo’s screenshot API. They do not capture a Chrome extension’s private popup or local extension page: the API captures a URL from its own service, while extension pages require the local Chrome context that has loaded the extension. See the ScreenshotNeo documentation for API options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

5. Troubleshooting

Symptom Likely cause Fix
Cannot resolve the browser entry-point import The Node entry point or an incompatible Puppeteer package version is being used, or the bundler cannot resolve the browser build. Install puppeteer-core, use the browser-specific import from the guide, and configure the bundler for browser code. Confirm the import path against your installed version.
Debugger permission or attach error The extension lacks the required permission, the user has not granted access, Chrome disallows attachment in that context, or another debugger is attached. Review Chrome’s current debugger permission and consent rules, ensure the tab is eligible, and detach conflicting debugging sessions before reconnecting.
tab.id is missing The tab operation did not return a usable tab identifier. Check the result of chrome.tabs.create or query the intended tab, and stop with a clear error when no ID is present.
browser.pages() returns no page The connection did not attach to the expected tab, or the target closed during attachment. Verify the tab still exists and reconnect to its current ID. In Node, wait for the correct target before converting it to a page.
Popup target wait times out The popup was never opened, the URL predicate is wrong, or the popup closed before Puppeteer found it. Trigger the action first, match the actual popup URL and extension ID, and use a timeout suited to your test startup.
Screenshot is blank or incomplete The page has not rendered yet, content is outside the viewport, or the capture requested only the visible viewport. Wait for a stable selector or page state, set fullPage: true when needed, and confirm the target page is the one you expect.
Image data is not saved No path was supplied, or the returned bytes were not written by the caller. In Node pass a path or write the returned bytes. In an extension, send the bytes through an appropriate extension download or storage flow.
Image looks clipped or dimensions differ The popup viewport is small, or the clip and viewport coordinates do not match the intended area. Set the page viewport before capture in Node where appropriate, and check clip coordinates and fullPage behavior.

6. Performance, reliability, and cost

Performance

  • Capture only the popup viewport when that is the artifact you need; full-page captures can require more rendering and produce larger files.
  • Wait for a meaningful readiness condition, such as a visible selector, instead of using a long fixed delay for every run.
  • Choose PNG for lossless output and JPEG or WebP when smaller lossy files suit the consumer. Avoid base64 unless the next step needs text encoding because it adds representation overhead.
  • Close the Node browser after the capture and disconnect extension transport connections when finished so automation sessions do not accumulate.

Reliability

  • Use a precise target predicate that checks the extension and popup URL; generic page-target matching can select the wrong tab.
  • Handle extension reloads and browser restarts by rediscovering targets rather than reusing stale IDs or pages.
  • Make screenshot setup deterministic: launch the same unpacked extension build, trigger the popup explicitly, wait for its content, then capture.
  • Keep in mind that extension-internal transport is documented as experimental and restricted to one page at a time.

Cost

Local Puppeteer itself does not charge per screenshot, but your workflow uses the machine and browser time needed to run Chrome. In CI, account for the compute and storage provided by that environment. ScreenshotNeo offers 1,000 screenshots per month free without a card; paid plans start at $5 for 3,000 screenshots. Its API is for website URLs, not extension-local pages.

7. Or skip the browser setup

For ordinary website URLs, ScreenshotNeo provides a one-request screenshot API. It does not replace Puppeteer for a local extension popup or extension-only state; use the DIY methods above for those captures.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for the other supported parameters. It removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed; and its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month without a card.

8. FAQ

Can Puppeteer capture a Chrome extension popup from outside the extension?

Yes. Run Puppeteer in Node, launch Chrome with the unpacked extension, open the popup, find its target, and call asPage() before taking the screenshot.

Can I use ScreenshotNeo to capture a local chrome-extension:// page?

No. ScreenshotNeo captures website URLs through its API; a local extension page needs the Chrome instance where that extension is installed and running.

Does Puppeteer return a file path from page.screenshot()?

It returns image data by default. In Node, provide path to save a file directly.

Can extension transport control multiple tabs?

The documented transport attaches to one page at a time. Create or select tabs with Chrome’s tabs API, then establish a connection for the page you need.

Official references