Skip to content

feat(coverage): experimental istanbul coverage collection via tracing - #42684

Merged
Yury Semikhatsky (yury-s) merged 21 commits into
microsoft:mainfrom
yury-s:app-coverage
Sep 16, 2026
Merged

Yury Semikhatsky (yury-s) merged 21 commits into
microsoft:mainfrom
yury-s:app-coverage

Conversation

@yury-s

Copy link
Copy Markdown
Member

Summary

  • tracing.start({ coverage: true }) collects istanbul counters (window.__coverage__) from instrumented application code and stores them as coverage.json inside the trace file. Works in all browsers, langs: js, experimental.
  • In the test runner: use: { trace: { mode: 'on', coverage: true } }, plus a coverage reporter that merges the per test coverage out of the traces into coverage-final.json, lcov.info and an istanbul html report.
  • The counters are collected after every action, whatever is left is stashed in localStorage on pagehide and picked up from any same origin document, or from a page Playwright opens for an origin that has none left. tracing.flushCoverage() collects on demand.
  • Playwright configures no bundlers: the application is expected to be instrumented by vite-plugin-istanbul, babel-plugin-istanbul or the SWC plugin, the same contract component testing uses for mount.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread packages/playwright-core/src/client/tracing.ts Outdated
Comment thread packages/playwright-core/src/server/trace/recorder/tracing.ts Outdated
Comment thread packages/playwright-core/src/server/trace/recorder/tracing.ts Outdated
Comment thread packages/playwright-core/src/server/browserContext.ts Outdated
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

tracing.start({ coverage: true }) enables collection of istanbul
coverage from instrumented application code. A server-side
CoverageRecorder installs a binding and init script into the browser
context, accumulates counters relayed right before navigations, sweeps
all frames when a trace chunk is stopped and stores the result as
coverage.json inside the trace file. tracing.flushCoverage() syncs
pages that are about to be closed by in-page script; pages closed via
the API are swept automatically. The test runner enables collection
with trace: { coverage: true } in the config.
…rter

The 'coverage' reporter collects the coverage.json entries from the
per-test trace attachments, merges them and writes coverage-final.json,
lcov.info and a console summary, plus the istanbul html report when the
project has istanbul-lib-* installed. Aggregation covers the tests
whose traces are kept, so a full aggregate requires trace mode 'on'.
The statement, function and branch maps dominate the size of an
istanbul report, so only send them with the first report of each file
and report the hit counters alone afterwards. Files that were not hit
since the previous report are skipped, files that were never hit are
still reported once to keep them in the denominator. The merge adopts
the maps from the first report that carries them and zero fills the
counters that the reports omit.
Counters parked on pagehide used to go to sessionStorage, which dies
together with a page that closes itself. Stash them in localStorage
instead, where any same origin document picks them up, and relay the
stashes on every collect, not only when a new document boots. Stashes
are scoped to the recorder session so that the ones left by a previous
run in a persistent profile are discarded, and carry an id so that the
recorder drops the copies when several documents pick up the same
stash.
A page relays the stashes of its own origin, so the stash of a page
that closed itself is only picked up while another page of that origin
is still around. Track the origins of the closed pages and visit the
ones that have no page left in a page of Playwright's own, reusing the
origin traversal of browserContext.storageState.
Collect the counters of the acted upon page at the tracing
instrumentation point that captures the after-action snapshot, so that
an action is the most that can be lost when a page goes away without a
chance to stash its counters, and so that the counters can later be
attributed to the actions that produced them.

With the pages swept this often, the stash in the page storage is only
picked up by Playwright itself, so the coverage script no longer needs
a binding to push it. Origins are noted as they are swept, not only
when a page closes, to also harvest the ones left behind by a
navigation.
Collecting after each action used to hang off the snapshot capture,
which is gated on the snapshot metainfo of the method, and the page
close case fell outside that gate and reached into tracing from
page.ts. Both now go through instrumentation: onAfterCall collects for
the page the action ran in, and a new onPageWillClose collects before
the page is gone, which also excludes the pages Playwright opens for
itself.

Name the trace entry trace.coverage, so that merging the chunks of
several contexts into the trace of a test keeps every one of them
instead of silently dropping all but the first. Do not collect for a
chunk that is being discarded, and write no entry when there is
nothing to report. Sweep the pages and their frames concurrently, and
keep the coverage stash out of browserContext.storageState.
Keeps onAfterCall synchronous, drops the api call wrapper around
flushCoverage and the visitOrigins comment.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

The recorder only noted origins it happened to see during a pull or
at page close, missing origins passed through in between.
Frames and multi-context cases join the plumbing and reporter tests,
the option-off case moves to the library suite.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment thread docs/src/api/class-tracing.md Outdated
Comment thread tests/library/browsertype-connect.spec.ts Outdated
Comment thread packages/playwright-core/src/server/instrumentation.ts
Comment thread packages/playwright-core/src/server/trace/recorder/tracing.ts Outdated
Comment thread packages/playwright-core/src/server/coverageRecorder.ts Outdated
}

async flush(progress: Progress) {
await progress.race(Promise.all(this._context.pages().map(page => this.collectFromPage(page))));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like safeNonStallingEvaluateInAllFrames

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to ignore exceptions on individual frames (if any) and proceed with serializing the rest

Comment thread packages/injected/src/coverageScript.ts Outdated
The recorder lives from start to stop, and uninstall unhooks the
pagehide listener in the live documents.
Only the hits after the start count, the maps still go out with the
first report.
Uninstall already unhooks the previous instance in every live document.
A failed stash is recorded in the page storage and thrown by the next
take, the error is reported when the chunk stops. The two tests that
were failing on Firefox and WebKit no longer click an empty body or
navigate inside the action whose pull races the navigation.
@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

1 failed
❌ [playwright-test] › fixture-errors.spec.ts:746 › should not continue with scope teardown after fixture teardown timeout @ubuntu-latest-node26

7 flaky ⚠️ [chromium-library] › library/browsercontext-page-event.spec.ts:173 › should work with Ctrl-clicking `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:356 › screencast › should work for popups `@realtime-time-library-chromium-linux`
⚠️ [chromium-library] › library/chromium/chromium.spec.ts:213 › should intercept service worker requests (main and within) `@chromium-ubuntu-22.04-node24`
⚠️ [chromium-library] › library/video.spec.ts:736 › screencast › should work with video+trace `@chromium-ubuntu-22.04-node24`
⚠️ [firefox-library] › library/browsercontext-cookies-third-party.spec.ts:257 › third party 'Partitioned;' cookies `@firefox-ubuntu-22.04-node20`
⚠️ [firefox-library] › library/browsercontext-cookies-third-party.spec.ts:470 › top level 'Partitioned;' cookie and same origin iframe `@firefox-ubuntu-22.04-node20`
⚠️ [webkit-page] › page/page-set-input-files.spec.ts:38 › should upload a folder `@webkit-ubuntu-22.04-node20`

51858 passed, 1247 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

2 failed
❌ [chrome] › mcp/cli-route.spec.ts:85 › unroute removes all routes @mcp-macos-latest-chrome
❌ [firefox] › mcp/cli-session.spec.ts:54 › idle timeout shuts the session down @mcp-windows-latest-firefox

8575 passed, 1446 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

🟢 All three failures are pre-existing flakes — this PR is clear

Hi, I'm the Playwright bot and I triaged the latest CI runs here.

Nothing red points at the coverage work. Each of the three failures has the same test failing on a SHA this branch can't be responsible for — a release-1.63 push, an unrelated dependabot PR, or plain main pushes. Worth noting the earlier library/coverage.spec.ts reds are gone: the newest "tests 1" run is green on that file.

Details

Pre-existing flake / infra

  • [playwright-test] › fixture-errors.spec.ts:746 › should not continue with scope teardown after fixture teardown timeout (ubuntu-latest-node26) — in the results DB this test has failed 3 times total with the identical expect(received).not.toContain("in fixture teardown") error: once on the release-1.63 branch (run 34886337379, sha 0a8e4af1, no PR), once on dependabot PR chore(deps): bump azure/login from 3.0.0 to 3.0.1 in the github-actions group across 1 directory #42372 (run 32671492278, sha 45fcf3a8), and once here. It passes in ~630 runs per bot otherwise. A timing-sensitive teardown-ordering race, not something this diff reaches.

  • [chrome] › mcp/cli-route.spec.ts:85 › unroute removes all routes (mcp-macos-latest-chrome) — timedOut, not an assertion failure. Same test timed out on a main push (run 33773231320, sha bfb42e59, mcp-windows-latest-firefox) with no PR involved. Two of its neighbours in the same file (route with content-type, route-list shows no routes when empty) have each timed out exactly once on this bot as well, which reads as MCP session start-up slowness on macOS rather than anything test-specific.

  • [firefox] › mcp/cli-session.spec.ts:54 › idle timeout shuts the session down (mcp-windows-latest-firefox) — the noisiest of the three: 12 of 28 runs failed on this bot, 16 passed, on SHAs across the repo, while mcp-ubuntu-latest-firefox (14/14) and mcp-macos-latest-firefox (15/15) are clean. A Windows/Firefox-only timing flake that predates this branch.

Not reached by the diff

This PR is confined to tracing/coverage collection, the injected coverage script, and a new coverage reporter. None of the three failing tests exercise tracing, coverage, or reporter selection.

Triaged by the Playwright bot - agent run

@github-actions

Copy link
Copy Markdown
Contributor

🟢 All three failures are pre-existing flakes — this PR looks clear

Hi, I'm the Playwright bot and I triaged the failing CI here.

Each of the three failures has the same test failing on SHAs this PR can't be responsible for — main, release-1.63, and other people's PRs — and none of them touch coverage, tracing, or the reporter surface this PR changes.

Details

The PR adds istanbul coverage collection behind tracing.start({ coverage: true }) plus a coverage reporter. Nothing in the diff reaches fixture teardown timing, MCP routing, or MCP session lifetime, and all three failures reproduce without it.

Pre-existing flake / infra

  • [playwright-test] › fixture-errors.spec.ts:746 › should not continue with scope teardown after fixture teardown timeout @ubuntu-latest-node26 — a timing-sensitive test (1000ms test timeout, asserts the second fixture teardown never runs). It failed with the byte-identical error — Expected substring: not "in fixture teardown"Tearing down "fixture2" exceeded the test timeout of 1000ms — on release-1.63 sha 0a8e4af1 (run 34886337379, no PR) and on the dependabot PR chore(deps): bump azure/login from 3.0.0 to 3.0.1 in the github-actions group across 1 directory #42372 sha 45fcf3a8 (run 32671492278). Across the results DB it's 3 failures in 4471 results, 1 of 632 on this exact bot, so it's a rare timing flake rather than anything this PR introduced.

  • [chrome] › mcp/cli-route.spec.ts:85 › unroute removes all routes @mcp-macos-latest-chrome — bare Test timeout of 30000ms exceeded. with no assertion error, i.e. it never got as far as checking anything, and it was the only failure on that bot in the run. Same test previously timed out on sha bfb42e59 (run 33773231320), unrelated to this branch. It's rare (2 failures in ~2140 results), so the evidence is a single prior occurrence, but it's on a SHA this PR can't touch, and the PR changes no routing code.

  • [firefox] › mcp/cli-session.spec.ts:54 › idle timeout shuts the session down @mcp-windows-latest-firefox — the clearest one: 19 failures in 168 results, all on mcp-windows-latest-firefox, including main at sha 9ae53771 (this PR's own merge base), bb78937f, 49d290ae, 18205280, a0241629, plus unrelated PRs test: evaluate() with falsy result #42728, fix(firefox): use bypassServiceWorker flag for storage state pages #42741, fix(network): preserve dates in response headers #42734, feat(firefox): roll to r1546 #42735, fix(mcp): do not close browser while tool call is running #42696. Broken on that bot independently of this PR.

Triaged by the Playwright bot - agent run

@yury-s
Yury Semikhatsky (yury-s) merged commit 05c126b into microsoft:main Sep 16, 2026
42 of 45 checks passed
@yury-s
Yury Semikhatsky (yury-s) deleted the app-coverage branch September 16, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants