ScreenshotNeo

BlogHow-to

How to Collect JavaScript Coverage in Puppeteer

Collect JavaScript coverage in Puppeteer by starting coverage before the behavior you want to measure, then stopping it to inspect executed script ranges.

By the ScreenshotNeo team4 October 20267 min read

Puppeteer collects JavaScript coverage through the Coverage instance on a Page. Start collection before the navigation or interactions you want to measure, then call stopJSCoverage() when that work is complete. The result is an array of scripts with their text and ranges recorded as executed.

await page.coverage.startJSCoverage();
await page.goto('https://example.com');
const jsCoverage = await page.coverage.stopJSCoverage();

This measures collected script execution; a percentage of used bytes is not proof that tests cover every behavior or that the code is high quality. The workflow below uses Puppeteer’s documented API. See the Coverage class, startJSCoverage(), JSCoverageOptions, and stopJSCoverage() references for current API details.

1. Install Puppeteer and collect coverage

In a new project, install Puppeteer:

npm install puppeteer

Save the following as coverage.mjs and run node coverage.mjs. It launches Chromium, starts coverage before loading the page, waits for navigation, stops collection, prints each script URL and the recorded ranges, and closes the browser even if an error occurs.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.coverage.startJSCoverage();
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  const entries = await page.coverage.stopJSCoverage();

  for (const entry of entries) {
    console.log({
      url: entry.url,
      scriptBytes: entry.text.length,
      executedRanges: entry.ranges,
    });
  }
} finally {
  await browser.close();
}

Coverage must be active during the work of interest. If the page’s initial load is only part of the workload, perform the interactions before stopping coverage:

await page.coverage.startJSCoverage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.click('[data-testid="open-menu"]');
await page.fill?.('[name="search"]', 'coverage'); // Use locator.fill() if supported by your Puppeteer version.
const entries = await page.coverage.stopJSCoverage();

The optional-call example avoids throwing if that API is absent, but then it will not fill the field. For broad version compatibility, use a selector interaction such as page.type() where appropriate, or consult the API for the Puppeteer version installed. Interactions should reflect the real behavior whose code paths you want included.

2. Understand the returned entries and calculate used bytes

Each coverage entry includes a script URL, its source text, and ranges describing recorded execution. Puppeteer’s documented byte calculation sums the lengths of script text and the lengths of used ranges. The range calculation is end - start - 1.

function summarizeCoverage(entries) {
  let totalBytes = 0;
  let usedBytes = 0;

  for (const entry of entries) {
    totalBytes += entry.text.length;
    for (const range of entry.ranges) {
      usedBytes += range.end - range.start - 1;
    }
  }

  return {
    scripts: entries.length,
    totalBytes,
    usedBytes,
    percentUsed: totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100,
  };
}

console.log(summarizeCoverage(entries));

This percentage describes the share of collected script bytes represented by the returned used ranges. It depends on which page state and interactions were captured, what scripts were included, and the configured granularity. It is not a test-completeness score.

3. Configure JavaScript coverage

Pass an options object to startJSCoverage(). The documented defaults are shown here:

await page.coverage.startJSCoverage({
  resetOnNavigation: true,
  reportAnonymousScripts: false,
  includeRawScriptCoverage: false,
  useBlockCoverage: true,
});
Option Default What it controls
resetOnNavigation true Whether collection resets on navigation. Setting it to false does not guarantee coverage survives a navigation; Chrome may discard the previous execution environment and its data.
reportAnonymousScripts false Whether to include scripts without an associated URL, such as dynamically created eval or new Function code. Such entries generally use a debugger://VM URL unless the script supplies a //# sourceURL=... comment.
includeRawScriptCoverage false Whether to include raw V8 script coverage data alongside the normal result. Enable it when a downstream processor needs that data.
useBlockCoverage true Whether to collect block-level rather than function-level coverage. Block-level data is more granular; function-level data reports at a coarser level.

Leave defaults in place unless the report consumer requires a different granularity or additional script data. More included data can make reports larger and may affect processing time.

4. Handle navigation and multiple pages

Do not rely on resetOnNavigation: false to preserve coverage across page transitions. The browser may discard the old page’s JavaScript execution environment, including its coverage data. For reliable multi-page measurements, stop collection before leaving a page, save that result, start a fresh collection on the next page, then combine the reports in your own reporting step.

const reports = [];

await page.coverage.startJSCoverage();
await page.goto('https://example.com/first');
reports.push(await page.coverage.stopJSCoverage());

await page.coverage.startJSCoverage();
await page.goto('https://example.com/second');
reports.push(await page.coverage.stopJSCoverage());

const allEntries = reports.flat();

This preserves separate per-page captures for later analysis. If the same script appears in multiple reports, decide how your report should combine duplicate URLs and ranges; simply concatenating entries can count the same script more than once in aggregate byte totals.

5. Send coverage to Istanbul

Puppeteer’s Coverage documentation points to puppeteer-to-istanbul as a path for producing output consumable by Istanbul. The conversion tool is downstream of collection: capture the entries first, then apply the converter’s instructions for your installed versions and desired output format. A single Istanbul configuration is not universal, so follow that package’s documentation for the reporting pipeline you use.

6. Troubleshoot common problems

Symptom Likely cause Fix
No entries or an empty result No eligible script executed while coverage was active, the page contains no JavaScript, or collection started after the behavior. Start coverage before navigation and interactions. Confirm the page loads scripts and complete the actions that exercise them before stopping.
Coverage appears to disappear after navigation The previous page’s JavaScript execution environment and coverage data may be discarded by Chrome. Stop before navigating away, save the entries, restart on the next page, and merge reports in your reporting layer.
Code created with eval is missing Anonymous scripts are excluded by default. Set reportAnonymousScripts: true. Use a sourceURL comment when a meaningful script URL is useful to downstream tools.
Reported percentage seems unexpectedly low or high The measurement reflects only the scripts and interactions captured, and it is a byte share of returned ranges. Check that coverage brackets the intended behavior; include required anonymous scripts; ensure duplicate scripts across pages are not double-counted; interpret the result as collected execution data, not test quality.
Ranges do not match function-level expectations useBlockCoverage defaults to block-level collection. Set useBlockCoverage: false if your analysis expects function-level granularity.
Raw V8 details are absent includeRawScriptCoverage defaults to false. Set it to true only if your downstream workflow consumes raw V8 coverage entries.
Browser process remains open after an error The script did not close the launched browser on exceptional paths. Put browser.close() in a finally block, as in the runnable example.

7. Performance, reliability, and cost considerations

  • Keep the measured window deliberate. Start immediately before the relevant navigation or behavior and stop when it finishes. This makes reports easier to interpret.
  • Use explicit navigation readiness. Choose an appropriate waitUntil condition for the application. Network idle can be unsuitable for pages with long-lived requests; use a readiness selector or an application-specific signal when needed.
  • Capture repeatable behavior. Run the same navigation and interaction sequence for comparable reports. Dynamic content and conditional loading can change which scripts execute.
  • Account for report size. Anonymous scripts and raw V8 data add detail. Enable those options only when needed, and avoid retaining large source-bearing reports longer than your workflow requires.
  • Make multi-page collection explicit. Stop and restart around navigations and preserve page-level context so a failed step does not silently erase which page a report represents.
  • Cost. Puppeteer is the browser automation library used in this workflow; the cited coverage API does not specify a coverage charge. Your execution cost depends on where and how you run Chromium, which is outside the coverage result itself.

8. Or skip the browser setup

If your task is to capture a screenshot rather than inspect executed JavaScript, ScreenshotNeo provides a website screenshot API and MCP server. It is not a JavaScript coverage collector. One GET request returns an image or PDF; see the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners, newsletter popups, and chat widgets are removed before the screenshot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers report the page verdict and billing status.
  • An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.
  • The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, no card required.

9. FAQ

Can I collect coverage without navigating?

Yes. Start coverage before the interactions you want to measure, then stop after them. Navigation is only needed if loading a page is part of the behavior under test.

Does a used-byte percentage tell me whether my tests are complete?

No. It reports recorded used ranges as a share of collected script text; it does not establish that important behaviors or edge cases were tested.

Can Puppeteer coverage measure CSS?

The API described here is JavaScript coverage. It does not report CSS coverage.

Can I use coverage to take a screenshot?

Coverage and screenshot capture answer different questions. Use Puppeteer coverage to inspect JavaScript execution; use a screenshot workflow when you need an image or PDF of a rendered page.