ScreenshotNeo

BlogGuides

How to Read Puppeteer JavaScript Coverage Results

Learn what Puppeteer’s JavaScript coverage entries and ranges mean, calculate the documented byte ratio, and avoid common interpretation errors.

By the ScreenshotNeo team4 October 20269 min read

Puppeteer JavaScript coverage tells you which source ranges Chrome observed executing during a collection window. Each entry contains a script URL, its source text, and covered ranges. To calculate the documented aggregate ratio, add the lengths of the reported ranges, divide by the total source-text length, and multiply by 100. Treat that figure as a description of the scripts and browser activity captured in that run—not as a score for test quality or product completeness.

The key to reading the result is to know when collection started and stopped, which scripts and options were included, and what the denominator contains. This guide shows a complete collection run, explains the output, and covers navigation, anonymous scripts, comparisons, and troubleshooting.

1. Collect coverage around the behavior you want to measure

Start coverage before the navigation or interaction sequence whose JavaScript you want to inspect. Exercise the relevant page behavior, then stop collection and read the returned entries. Code that runs before collection starts, or falls into a category excluded by the selected options, should not be assumed to appear.

This runnable example launches Chromium, starts JavaScript coverage, visits a page, exercises a button if present, stops coverage, prints a per-script summary, and calculates the documented aggregate. It uses JavaScript coverage only; it does not mix CSS entries into the denominator.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();

    await page.coverage.startJSCoverage();
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });

    // Replace this with the interactions that represent the journey you measure.
    const button = await page.$('button');
    if (button) await button.click();

    const jsCoverage = await page.coverage.stopJSCoverage();
    let totalBytes = 0;
    let usedBytes = 0;

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

    const percentage = totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100;
    console.log(`JavaScript entries: ${jsCoverage.length}`);
    console.log(`Used bytes (documented range calculation): ${usedBytes}`);
    console.log(`Total source text length: ${totalBytes}`);
    console.log(`Covered ratio: ${percentage.toFixed(2)}%`);

    for (const entry of jsCoverage) {
      let scriptUsed = 0;
      for (const range of entry.ranges) {
        scriptUsed += range.end - range.start - 1;
      }
      const scriptPercent = entry.text.length === 0
        ? 0
        : (scriptUsed / entry.text.length) * 100;
      console.log({ url: entry.url, sourceLength: entry.text.length, used: scriptUsed, percentage: scriptPercent.toFixed(2) });
    }
  } finally {
    await browser.close();
  }
})();

Save this as coverage.js, install Puppeteer in the project with npm install puppeteer, and run node coverage.js. Use a page and interactions you are authorized to automate. Adjust the navigation wait condition and selectors for the application under study.

2. Read the fields in each JavaScript entry

A JavaScript coverage entry extends Puppeteer’s common coverage entry. The important fields are:

Field What it tells you How to use it
url The script’s associated URL. Group or identify scripts in a report. Anonymous scripts may be omitted unless enabled.
text The source text associated with the entry. Use this exact source version when interpreting range offsets or building an annotated report.
ranges Ranges of source positions observed as covered during collection. Use each range’s start and end with the entry’s own source text.
rawScriptCoverage Optional raw V8 coverage data. Available when raw script coverage is requested; useful when a downstream workflow needs that lower-level data.

The ranges are positions into the entry’s source text. They are not counts of statements, tests, features, or user journeys. Keep the source text and its version with any report that maps ranges back to code; applying offsets to a different build can highlight the wrong content.

3. Calculate and label the documented percentage

Puppeteer’s coverage example sums range.end - range.start - 1 for covered ranges and divides by entry.text.length accumulated across entries. The JavaScript-only calculation in the code above follows that documented arithmetic:

let totalBytes = 0;
let usedBytes = 0;
for (const entry of jsCoverage) {
  totalBytes += entry.text.length;
  for (const range of entry.ranges) {
    usedBytes += range.end - range.start - 1;
  }
}
const percentage = totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100;

Label this as the documented aggregate range ratio or byte-based coverage example, and say whether the entries are JavaScript only or combine JavaScript and CSS. Puppeteer’s published example can aggregate both JavaScript and CSS; if you use that approach, include both result arrays consistently and label the denominator accordingly. The example uses JavaScript string length as the denominator, so describe the result as the example’s source-length ratio rather than a universal measure of executable bytes.

The zero-total guard avoids producing NaN when no source text was returned. A report with no entries or no ranges is still a meaningful collection outcome to investigate; it is not automatically evidence of 0% coverage for the whole application.

4. Understand what changes the result

Collection window and interactions

Coverage reflects only activity observed between start and stop. If you stop immediately after navigation, event handlers and later states may not have run. Start before the journey, run the relevant clicks, form submissions, and state changes, and stop after those actions complete.

Block-level or function-level collection

useBlockCoverage defaults to true, which selects block-level collection. Setting it to false selects function-level collection. The granularity affects where execution is recorded, so hold it constant when comparing runs.

Anonymous scripts

reportAnonymousScripts defaults to false. Anonymous scripts can include dynamically created code from eval or new Function. When enabled, they may be represented with URLs beginning debugger://VM. Adding a //# sourceURL=... comment to generated code can give it a recognizable URL. Enable reporting when those scripts are part of the question you are investigating.

Raw V8 coverage

includeRawScriptCoverage defaults to false. Set it to true only when your report consumer needs the optional rawScriptCoverage field. Keep this setting consistent across runs whose results you compare.

resetOnNavigation defaults to true. Setting it to false does not guarantee that coverage survives navigation: Chrome may discard the old page execution environment and its coverage. If you need data across pages, stop coverage before navigating, start it again on the next page, and merge the separate reports yourself.

Check the API reference for the Puppeteer version installed in your project before relying on defaults; option behavior and defaults are version-sensitive.

5. Compare runs on the same basis

A percentage change is interpretable only when the measurement setup is comparable. Before comparing results, check these axes:

  • Collection window: same navigation, interactions, and start and stop points.
  • Script population: same script URLs and same treatment of anonymous scripts.
  • Options and granularity: same block or function setting, raw coverage setting, and navigation strategy.
  • Denominator: same source build and aggregation method; state whether CSS is included.
  • Page state: same relevant content, account state, feature flags, and route where those affect loaded scripts.

Use per-script entries to explain an aggregate movement. A changed percentage can come from a different set of scripts or a different journey, not only from more or less execution in the same code.

6. Troubleshoot common results and errors

Symptom Likely cause Fix
No entries or unexpectedly empty output Coverage started too late, stopped too early, navigation failed, or no JavaScript was returned in the measured page. Start before navigation, confirm the page loaded, inspect the URL and page errors, and stop only after the target behavior.
Expected generated code is missing Anonymous scripts are excluded by default. Set reportAnonymousScripts: true; use a sourceURL annotation in generated code when you control it.
Coverage disappears after navigation Chrome discarded the previous execution environment; resetOnNavigation: false cannot guarantee retention. Stop before navigating, start coverage on the next page, and merge the reports.
Offsets do not line up with the source file The report is being applied to a different source version, transformed bundle, or source map representation. Retain and annotate against the entry’s text or the exact generated asset used for that run.
Percentages are negative, above 100%, or otherwise surprising The ranges may be counted more than once, mixed collection sets may be combined, or incompatible denominators/source versions may be used. Follow the documented range arithmetic on one result set, inspect ranges per entry, and disclose aggregation choices. Do not silently clamp the result.
Zero division or NaN The accumulated source text length is zero. Guard for zero as in the sample, then investigate why there were no usable source entries.
Results differ after a Puppeteer upgrade Defaults, Chrome behavior, or collected script population may differ by version. Pin versions for repeatable reports and verify current option defaults in the installed-version API reference.

7. Performance, reliability, and cost

Coverage collection adds instrumentation and stores report data until it is stopped, so keep the window focused on the behavior you intend to measure. For large applications, process entries after stopping and avoid retaining multiple full source reports longer than necessary. A stable browser version, pinned Puppeteer version, deterministic test data, and repeatable interactions improve run-to-run comparisons.

The JavaScript coverage API itself does not define a universal runtime or storage cost; those depend on the site, browser, and amount of collected source. The useful operational practice is to collect only the needed pages and flows, label the exact denominator, and treat failures or missing coverage as collection issues to diagnose rather than forcing a percentage.

8. Turn coverage entries into a navigable report

If you need line-oriented output consumable by Istanbul tooling, Puppeteer’s coverage documentation points to puppeteer-to-istanbul. Keep the original Puppeteer entries with the converted report so readers can see which files and source versions formed the result.

9. Capture a visual reference for the same browser journey

JavaScript coverage explains what code ran; a screenshot can preserve what the browser showed during that same run. ScreenshotNeo is a website screenshot API and MCP server for developers, made by Yorker Media. It is separate from Puppeteer’s coverage output, but can be useful when a coverage report needs a visual reference for a page state.

For the DIY path, use your existing Puppeteer page and save a screenshot when the relevant state is visible:

await page.screenshot({ path: 'page-state.png', fullPage: true });

That captures a browser view; it does not change or explain the coverage entries. If you need a screenshot in a separate request, ScreenshotNeo accepts a URL and can return PNG, JPEG, WebP, or PDF. See the ScreenshotNeo website and API documentation.

10. Or skip the browser setup

One GET request captures a URL. The following examples save the returned image; replace the target URL as needed. Find the available capture parameters in the ScreenshotNeo docs.

cURL

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

Python

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)

Node.js

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

ScreenshotNeo accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free plan: 1,000 screenshots a month, no card required.

11. FAQ

Does 80% coverage mean 80% of my product is tested?

No. It means the documented aggregate of covered ranges relative to the source text in that collection. It does not measure feature completeness, assertion quality, or all possible user journeys.

Should I include CSS in the JavaScript percentage?

No, if the figure is labeled JavaScript coverage. Puppeteer’s example can combine JS and CSS, but a combined result should say so and use both populations consistently.

Why are some script URLs shown as debugger://VM...?

Those can be anonymous scripts reported without an ordinary source URL, such as dynamically generated code. Anonymous reporting is off by default.

Where should I verify option defaults?

Use the API reference matching the Puppeteer version installed in your project, since the documentation and defaults can be version-specific.

Sources