ScreenshotNeo

BlogHow-to

How to Measure JavaScript Code Coverage in Puppeteer

Measure which JavaScript runs during a Puppeteer session, calculate a byte-based coverage percentage, and handle navigation, dynamic scripts, and reporting.

By the ScreenshotNeo team4 October 20268 min read

Measure JavaScript code coverage in Puppeteer by starting page.coverage.startJSCoverage() before the navigation or interaction you want to observe, exercising that flow, and calling page.coverage.stopJSCoverage(). The returned entries include script text and executed ranges. Divide the total bytes in those ranges by the total script-text bytes to get a byte-based used-code percentage.

This measures code observed during the scenarios you ran. It is not a test pass rate, branch coverage score, or proof that unobserved code is dead. Puppeteer describes the Coverage class as a way to gather information about parts of JavaScript and CSS used by a page. See the Puppeteer coverage guide and the Coverage API reference.

1. Install Puppeteer and run a basic coverage collection

In a new Node.js project, install Puppeteer:

npm install puppeteer

Save this as coverage.mjs and run it with node coverage.mjs. The example starts collection before navigation, leaves a place for the application flow, totals script bytes and executed range bytes, 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' });

  // Exercise the interactions or flows whose code you want to measure here.
  // Example: await page.click('button[data-action="open-menu"]');

  const entries = await page.coverage.stopJSCoverage();
  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;
    }
  }

  const percent = totalBytes === 0 ? 0 : (usedBytes / totalBytes) * 100;
  console.log(`Bytes used: ${percent.toFixed(2)}%`);
  console.log(`Used ${usedBytes} of ${totalBytes} script bytes`);
} finally {
  await browser.close();
}

The range calculation follows Puppeteer’s documented example. The zero-byte guard avoids division by zero if no script text was returned. To save the raw entries for later processing, serialize them before the browser closes; keep in mind that entries can contain full script text.

2. Choose the coverage window deliberately

Coverage records runtime activity within a collection session. Start it before the work that matters and stop it after the final interaction. Starting after navigation misses activity that happened during page initialization; stopping too early misses later interactions and asynchronous work.

  1. Create the page and set any relevant viewport, cookies, or test state.
  2. Start JavaScript coverage.
  3. Navigate to the target route, then wait for the page state your test actually needs.
  4. Exercise representative user paths, including conditional branches you want to observe.
  5. Stop coverage and process the returned entries.

For a single-page application, include route changes and UI interactions in the observed window. A page that appears visually ready may still load or execute code later, so synchronize on an application-specific selector or state when possible. Avoid using an arbitrary fixed delay as the only readiness condition when a reliable selector is available.

3. Understand the result and its limits

The simple percentage is usedBytes / totalBytes × 100, where total bytes are the lengths of returned script texts and used bytes are the summed lengths of reported executed ranges. It describes the scripts Puppeteer reported for that session and the ranges observed as executed.

  • Low coverage can mean the scenario did not exercise much of the loaded code. It can also reflect scripts or routes outside the captured flow.
  • High coverage does not establish that tests are correct, that every input is handled, or that all branches have been validated.
  • Byte weighting gives larger scripts more influence than smaller scripts. The result is not a count of functions or test cases.
  • Session scope matters: coverage only speaks to the navigation and actions observed in that collection.

Use the percentage as a comparison under a consistent setup—for example, before and after adding a test flow. For decisions about code removal, inspect unexecuted ranges and confirm they are not needed on other routes, under other user roles, or in less common states.

4. Configure JavaScript coverage

startJSCoverage() accepts options. The documented defaults are:

Option Default What it changes
resetOnNavigation true Resets coverage on navigation. Turning it off does not guarantee that coverage survives because Chrome may discard the previous page execution environment.
reportAnonymousScripts false Whether to include anonymous scripts, such as dynamically generated code.
includeRawScriptCoverage false Whether entries include V8 raw script coverage data for workflows that need it.
useBlockCoverage true Collects block-level ranges by default. Set to false for function-level coverage.

Example enabling anonymous scripts and retaining the default block-level granularity:

await page.coverage.startJSCoverage({
  reportAnonymousScripts: true,
  useBlockCoverage: true,
});

Anonymous scripts are excluded by default. When enabled, dynamically generated scripts can appear with names such as debugger://VM...; a //# sourceURL=... comment can supply a more useful name. See Puppeteer’s startJSCoverage reference and JSCoverageOptions reference for the option definitions in the version you use.

5. Collect coverage across multiple pages

For a journey with full page navigations, do not depend on resetOnNavigation: false to preserve all earlier data. Puppeteer explicitly notes that disabling reset does not guarantee survival across navigation. A dependable approach is to stop collection before leaving each page, start again on the next page, and merge or analyze the reports downstream.

const reports = [];

await page.coverage.startJSCoverage();
await page.goto('https://example.com/start');
await page.click('a[href="/next"]');

reports.push(await page.coverage.stopJSCoverage());
await page.coverage.startJSCoverage();
await page.waitForURL('https://example.com/next');
// Exercise the next page's relevant flow.
reports.push(await page.coverage.stopJSCoverage());

const allEntries = reports.flat();

Adapt the navigation wait to the app and Puppeteer version in use. If the interaction triggers navigation, ensure the click and navigation wait are coordinated so the click is not left waiting on a transition that the script has not observed. When merging, decide how to handle duplicate script URLs and changing script contents; summing all entries as-is can count a script more than once.

6. Export or convert the report

The entries can be inspected directly, summarized by URL, or passed to a reporting pipeline. Puppeteer’s coverage guide points to puppeteer-to-istanbul for converting Puppeteer output to a format consumable by Istanbul. Confirm compatibility and usage against the package documentation for your installed versions.

A minimal per-entry summary can help identify which scripts dominate the total:

const summary = entries.map((entry) => {
  const total = entry.text.length;
  const used = entry.ranges.reduce(
    (sum, range) => sum + range.end - range.start - 1,
    0,
  );
  return {
    url: entry.url,
    totalBytes: total,
    usedBytes: used,
    percent: total === 0 ? 0 : (used / total) * 100,
  };
});

console.table(summary);

Coverage entries contain script text, so treat stored reports as source-bearing artifacts. Avoid publishing them or sending them to an untrusted service if the page includes proprietary client code.

7. Troubleshooting

Symptom Likely cause Fix
No entries or zero total bytes Coverage started too late, the page had no reportable scripts, navigation failed, or the relevant content never loaded. Start before navigation, check the page response and browser console, and verify that scripts are present. Keep the zero-total guard in percentage calculations.
Coverage is unexpectedly low The captured flow did not exercise the application states or interactions that use the code. Add representative paths and wait for the intended state before stopping collection. Inspect entries and ranges instead of relying on one aggregate percentage.
Scripts disappear after navigation Navigation resets coverage by default, and disabling reset still cannot guarantee the browser retains the previous execution environment. Stop before navigation, restart afterward, and merge reports in your own reporting layer.
Eval or generated scripts are missing Anonymous scripts are omitted by default. Set reportAnonymousScripts: true; use sourceURL annotations in generated code when you control it and need identifiable names.
The number differs from a function or branch report The basic Puppeteer calculation is byte-based and the default collection is block-level. Compare like with like, record the chosen options, and use an Istanbul conversion workflow if that is the report format your team requires.
Totals seem inflated across routes The same script may appear in more than one page report. Define whether your report measures per-page observations or unique script versions, then deduplicate with an appropriate key and account for changed script content.

8. Performance, reliability, and cost

Coverage adds collection and processing work to an automated browser session, and retaining entries also retains script text. Keep collection scoped to the flows needed for the report, close the browser in a finally block, and avoid serializing large raw reports unless a downstream consumer needs them. Do not compare measurements from materially different browser, site-build, or scenario conditions as if they were equivalent.

Reliability depends on repeatable page state and explicit navigation handling. Use stable test data, deterministic authentication state, and waits tied to the application state under test. For multi-page journeys, collect page reports deliberately. The coverage API itself does not imply a monetary charge; the practical cost is browser runtime, compute, and report storage in your environment.

9. Or skip the browser setup

If your goal is to inspect how a page looks rather than measure which JavaScript executed, ScreenshotNeo is a website screenshot API and MCP server. It cannot produce Puppeteer JavaScript coverage; it captures a visual result. One GET request returns an image or PDF, and the API accepts familiar screenshot parameter names to ease switching.

See the ScreenshotNeo API documentation. Example using the supplied API pattern:

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
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(({ writeFile }) => writeFile('shot.webp', bytes));

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, failed loads, timeouts, and cache hits cost nothing, and response headers indicate the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

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

10. FAQ

Does Puppeteer coverage tell me whether my tests are good?

No. It shows observed code execution for the collected session. Test quality also depends on assertions, input coverage, and whether the scenarios check expected behavior.

Can I use JavaScript coverage and CSS coverage together?

Yes. The Coverage API has corresponding CSS start and stop methods, but CSS collection is separate from the JavaScript byte calculation shown here.

Should I always include anonymous scripts?

Only when dynamically generated scripts matter to the question you are measuring. Enabling them can add entries that are harder to map back to maintained source unless they have sourceURL names.

Why does the percentage change after a small application edit?

The ratio depends on both executed ranges and the total script text included. A build change, different loaded chunks, or a changed test flow can affect either side of the calculation.

References