supercov 4.8.1 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/cli.md CHANGED
@@ -632,11 +632,14 @@ what that run's tests did with it:
632
632
  - A helper, fixture or support file, or source outside what is measured (a
633
633
  simulator kept out of `SUPERCOV_SOURCE_ROOTS`): left out of a run only when
634
634
  that run's test files import it, directly or through other files. Supercov
635
- reads the imports of JavaScript, TypeScript and Python tests, with pytest's
636
- `conftest.py` files and `pytest_plugins`; a computed import, a change to
637
- `sys.path`, or tests in another language count every helper as loaded. A
638
- JavaScript or TypeScript helper that only gained functions, which code
639
- written before cannot have called, leaves every run in.
635
+ reads the imports of JavaScript, TypeScript, Python and Ruby tests, with
636
+ pytest's `conftest.py` files and `pytest_plugins`, and what `.rspec`
637
+ requires; a computed import, a glob of support files, a change to
638
+ `sys.path` or `$LOAD_PATH`, or tests in another language count every helper
639
+ as loaded. A JavaScript, TypeScript or Python helper that only gained
640
+ functions, which code written before cannot have called, leaves every run
641
+ in. In Python that is a plain `def`: a decorator runs when the module
642
+ loads.
640
643
  - A test runner's settings: `playwright.config.*`, `vitest.config.*`,
641
644
  `jest.config.*` and `ava.config.*` belong to their runner, and a run that
642
645
  did not use that runner keeps. Of Playwright's settings, only those that
@@ -110,16 +110,19 @@ By test kind
110
110
  directory has any, so a directory's `All` never stands above its kinds
111
111
  unexplained.
112
112
 
113
- A request to your app server that names no test -- a webhook from a mock
114
- Stripe server Supercov does not measure -- is credited to the test running when
115
- the server handled it, if that test was the only one running then. Where a
116
- server serves one worker's tests, as a server started per worker does, only
117
- that worker's tests count, so the request is credited while other workers run
118
- theirs. A mock that runs under Supercov passes the test's identity on in its
119
- own requests, and its webhook is credited exactly. The run's `Attribution`
120
- line counts tests credited by time as `with server work credited by time`, and
121
- each one's `runs <id> test` page says so. Work done while tests ran at once on
122
- the same server, or while none ran, stays no test's.
113
+ A request to your app server, Node, Python or Ruby, that names no test -- a
114
+ webhook from a mock Stripe server Supercov does not measure -- is credited to
115
+ the test running when the server handled it, if that test was the only one
116
+ running then. Where a server serves one worker's tests, as a server started per
117
+ worker does, only that worker's tests count, so the request is credited while
118
+ other workers run theirs. A mock that runs under Supercov passes the test's
119
+ identity on in its own requests, and its webhook is credited exactly. The run's
120
+ `Attribution` line counts tests credited by time as `with server work credited
121
+ by time`, and each one's `runs <id> test` page says so. A server the suite runs
122
+ in its own process -- a pytest live server, Capybara's Puma under a Rails
123
+ system test -- is judged by which of the suite's tests that process was
124
+ running. Work done while tests ran at once on the same server, or while none
125
+ ran, stays no test's.
123
126
 
124
127
  A test that made assertions and has no covered line to its name is counted in
125
128
  a warning on the summary. `npx supercov runs latest tests without-evidence`
@@ -224,6 +224,45 @@ npx supercov -- uv run pytest
224
224
  npx supercov -- python -m unittest
225
225
  ```
226
226
 
227
+ ### A Python server under browser tests
228
+
229
+ A Playwright suite that starts a Python server, through `webServer`, a fixture
230
+ or a test, measures the server when you run it under Supercov:
231
+
232
+ ```sh
233
+ npx supercov -- npx playwright test
234
+ ```
235
+
236
+ Each request runs as the test that sent it. Supercov's Playwright runtime names
237
+ the test in a header on the page's own requests and the request fixture's, and
238
+ in a cookie on the app's host; the server reads it in the standard library's
239
+ HTTP server, in any WSGI server running Flask or Django (gunicorn, waitress,
240
+ their own development servers) and in any ASGI server running Starlette,
241
+ FastAPI or Django (uvicorn, hypercorn). A request that names no test, such as
242
+ a webhook from a mock, is credited by time as it is for a Node server; see the
243
+ [coverage model](coverage-model.md). A Python process a test starts belongs to
244
+ that test.
245
+
246
+ The server's coverage is a Python run of its own. In a project with no
247
+ JavaScript source that run is the run. In one with a Node server or front end
248
+ too, the browser run is published as usual and the server's run beside it.
249
+ `runs latest` is the browser run, and its summary names the server's run
250
+ (`companions` in JSON); the server's run names the browser run it took its
251
+ tests from. The pull-request comment Action reports each server's run in a
252
+ section of its own. Each run's figures stand alone, and `supercov merge`
253
+ merges each language's runs separately.
254
+
255
+ A server the suite runs in its own process works without Playwright: a
256
+ pytest-django `live_server`, Django's `LiveServerTestCase`, or a Flask app in
257
+ a thread that a Selenium or pytest-playwright suite drives. Its requests name
258
+ no test, and each is credited to the test that was running in that process
259
+ when it came, and said to be.
260
+
261
+ The server runs in Supercov's copy of the project. A virtual environment
262
+ inside the project is used as it is, and a package installed editable from the
263
+ checkout is measured too. `SUPERCOV_SOURCE_ROOTS` applies to both languages:
264
+ when you set it, list the Python directories as well.
265
+
227
266
  ## Ruby
228
267
 
229
268
  | Runner | Attribution | Current requirement |
@@ -280,6 +319,26 @@ npx supercov -- ruby -Itest test/shapes_test.rb
280
319
  npx supercov -- bin/rails test
281
320
  ```
282
321
 
322
+ ### A Ruby server under browser tests
323
+
324
+ A Playwright suite that starts a Ruby server, a Rails, Sinatra or other Rack
325
+ app under Puma, WEBrick or another server, measures it the way it measures a
326
+ [Python server](#a-python-server-under-browser-tests): each request runs as the
327
+ test that sent it, a request that names no test is credited by time, a Ruby
328
+ process a test starts belongs to that test, and the server's coverage is a Ruby
329
+ run of its own, published as the run or beside the browser run. Gems installed
330
+ into the project (`bundle config set path vendor/bundle`) are used from the
331
+ copy the suite runs in.
332
+
333
+ A Rails system test runs its app in Capybara's Puma, in the test process: each
334
+ request is credited to the test that was running when it came, and said to
335
+ be.
336
+
337
+ ```sh
338
+ npx supercov -- npx playwright test
339
+ npx supercov -- bin/rails test:system
340
+ ```
341
+
283
342
  ## Crediting a test with what it reached
284
343
 
285
344
  Most runners attribute exactly at no cost: Supercov runs each Rust test in its
@@ -362,6 +421,37 @@ npx supercov -- go test -run TestParser ./internal/...
362
421
  npx supercov -- go test ./core/... ./app/...
363
422
  ```
364
423
 
424
+ ### A Go server under browser tests
425
+
426
+ A Playwright suite that starts a Go server with `go run` measures it: the copy
427
+ the suite runs in has its Go rewritten with probes, as for `go test`, and the
428
+ server is built from that copy. Go has no per-request context Supercov can
429
+ carry a test's name in to every probe, so the server sweeps what ran every
430
+ 5 ms, and each sweep is credited to the browser test that was running then,
431
+ if only one was, and said to be. Work done while two tests ran at once, as
432
+ with parallel workers, is the run's and no test's; the run says how many tests
433
+ that left without their server work, and `--workers=1` gives each test its
434
+ own. The server's coverage is a
435
+ Go run of its own, published as the run or beside the browser run, as for a
436
+ [Python server](#a-python-server-under-browser-tests).
437
+
438
+ A server binary built before the run is not the one Supercov instruments:
439
+ start it with `go run`, or build it in the test command.
440
+
441
+ ### A Java or Kotlin server under browser tests
442
+
443
+ A Playwright suite whose server is built by its own command (`./gradlew
444
+ bootRun`, `mvn spring-boot:run`, or `javac` and `java`) measures it the way it
445
+ measures a [Go server](#a-go-server-under-browser-tests): the copy's Java and
446
+ Kotlin are rewritten with probes, the runtime goes in each module's main
447
+ source set, and what the server ran is swept every 5 ms and credited by time
448
+ to the browser test running then, if only one was. A jar built before the run
449
+ is not the one Supercov instruments.
450
+
451
+ ```sh
452
+ npx supercov -- npx playwright test
453
+ ```
454
+
365
455
  ## Java and Kotlin
366
456
 
367
457
  | Runner | Attribution | Current requirement |
@@ -42,6 +42,11 @@ npx supercov runs latest scope
42
42
  Do not broaden the roots to dependencies or generated output merely to remove a
43
43
  warning. The goal is an honest boundary around code the repository owns.
44
44
 
45
+ Browser tests of a Python or Ruby server need a Playwright configuration in
46
+ the project, and the server's source under the roots: Supercov then measures
47
+ the server the suite starts. See
48
+ [Supported suites](supported-suites.md#a-python-server-under-browser-tests).
49
+
45
50
  A file `scope` lists as `test or fixture source` or `conventional tool script`
46
51
  was recognised by its path. If it is your code, name its directory, or the file
47
52
  itself, as a root: a named root is source whatever it is called.
@@ -85,6 +85,16 @@ When a run fails in a project that has a `build` script the command does not
85
85
  reach, it says this. `SUPERCOV_KEEP_WORKSPACE=1` leaves the instrumented copy
86
86
  in `.supercov/workspaces/` after a run, to inspect.
87
87
 
88
+ A package inside a larger git repository (`web/` beside `cmd/`, or
89
+ `packages/app` in a monorepo) is copied alone, but the copy sits at the
90
+ package's path in the repository, with the repository's other files and
91
+ directories linked beside it from your checkout. A suite that reaches outside
92
+ the package, a `webServer` running `go run ../cmd/testserver` or a tsconfig
93
+ extending `../../tsconfig.base.json`, finds them where it finds them without
94
+ Supercov. They are your checkout's own files, not copies, and they are not
95
+ measured; the repository's `.git` and lockfiles are never linked. To measure code outside the
96
+ package too, run Supercov from the repository root.
97
+
88
98
  ## Files Supercov creates
89
99
 
90
100
  | Location | What it is for |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supercov",
3
- "version": "4.8.1",
3
+ "version": "4.10.1",
4
4
  "description": "Coverage, security and code quality for coding agents",
5
5
  "keywords": [
6
6
  "coverage",
@@ -83,7 +83,7 @@
83
83
  "eval:skill": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag behavior trigger --allow-tools Bash Edit Write --max-cost-usd 15",
84
84
  "eval:skill-triggers": "claude plugin eval plugins/supercov --scaffold --trust-plugin --no-publish --model claude-sonnet-5 --judge-model claude-haiku-4-5 --threshold 0.66 --tag trigger --ablation none --max-cost-usd 5",
85
85
  "test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && npm run test:runtime && npm run test:rust-assets && npm run test:engine-integrations",
86
- "test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/background-by-time.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
86
+ "test:engine-integrations": "cargo build -p supercov && node scripts/html-report-integration.mjs && node scripts/rust-process-supervision.mjs && node scripts/rust-direct-node-integration.mjs && node scripts/rust-public-run-integration.mjs && node scripts/rust-embedded-runtime-integration.mjs && node scripts/rust-direct-vitest-integration.mjs && node scripts/rust-direct-jest-integration.mjs && node scripts/mocha-failure-output.mjs && node scripts/failure-output.mjs node ava tap && node scripts/side-by-side.mjs node && node scripts/merge-across.mjs && node scripts/rust-direct-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-integration.mjs && node scripts/cross-origin-requests.mjs && node scripts/leaving-page.mjs && node scripts/background-by-time.mjs && node scripts/python-server-playwright.mjs && node scripts/ruby-server-playwright.mjs && node scripts/go-server-playwright.mjs && node scripts/jvm-server-playwright.mjs && node scripts/rust-classic-browser-integration.mjs && node scripts/rust-top-packages-integration.mjs && node scripts/rust-generic-esbuild-integration.mjs && node scripts/rust-generic-tsc-integration.mjs && node scripts/rust-generic-build-matrix.mjs && node scripts/rust-vite-playwright-integration.mjs",
87
87
  "test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
88
88
  "test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
89
89
  "test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
@@ -120,14 +120,14 @@
120
120
  "test:windows-tls": "node scripts/windows-tls-test.mjs"
121
121
  },
122
122
  "optionalDependencies": {
123
- "@supercov/cli-darwin-arm64": "4.8.1",
124
- "@supercov/cli-darwin-x64": "4.8.1",
125
- "@supercov/cli-linux-arm64-gnu": "4.8.1",
126
- "@supercov/cli-linux-arm64-musl": "4.8.1",
127
- "@supercov/cli-linux-x64-gnu": "4.8.1",
128
- "@supercov/cli-linux-x64-musl": "4.8.1",
129
- "@supercov/cli-win32-arm64": "4.8.1",
130
- "@supercov/cli-win32-x64": "4.8.1"
123
+ "@supercov/cli-darwin-arm64": "4.10.1",
124
+ "@supercov/cli-darwin-x64": "4.10.1",
125
+ "@supercov/cli-linux-arm64-gnu": "4.10.1",
126
+ "@supercov/cli-linux-arm64-musl": "4.10.1",
127
+ "@supercov/cli-linux-x64-gnu": "4.10.1",
128
+ "@supercov/cli-linux-x64-musl": "4.10.1",
129
+ "@supercov/cli-win32-arm64": "4.10.1",
130
+ "@supercov/cli-win32-x64": "4.10.1"
131
131
  },
132
132
  "peerDependencies": {
133
133
  "@playwright/test": ">=1.55.0",
@@ -216,6 +216,9 @@ class CoveragePhaseController {
216
216
  // fixture override tears down before the collector's own fixture, and a
217
217
  // closed page cannot be evaluated, so the evidence is taken on the way out.
218
218
  earlySnapshots = [];
219
+ // Evidence documents gave as they left (`__supercovLeaving`), merged with
220
+ // the rest when there is any.
221
+ leftSnapshots = [];
219
222
  snapshottedPages = new WeakSet();
220
223
  snapshottedWorkers = new WeakSet();
221
224
  // Workers that did not answer an evaluation in time. They are not waited
@@ -279,8 +282,10 @@ class CoveragePhaseController {
279
282
  if (snapshot)
280
283
  snapshots.push(snapshot);
281
284
  }
282
- this.runtimeSnapshots = snapshots;
283
- return snapshots;
285
+ this.runtimeSnapshots = this.leftSnapshots.length > 0
286
+ ? [mergeSnapshots([...this.leftSnapshots, ...snapshots])]
287
+ : snapshots;
288
+ return this.runtimeSnapshots;
284
289
  }
285
290
  /**
286
291
  * A worker's evidence, or nothing when it cannot be read. A worker that
@@ -291,20 +296,15 @@ class CoveragePhaseController {
291
296
  if (this.workersWithoutRuntime.has(worker)) {
292
297
  // Asked once more, without waiting: it may have loaded an
293
298
  // instrumented script since it was registered.
294
- const answer = await bounded(worker
295
- .evaluate(() => globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__?.())
296
- .catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
299
+ const answer = await bounded(readSnapshot(worker).catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
297
300
  if (answer && answer !== WORKER_TIMED_OUT) {
298
301
  this.snapshottedWorkers.add(worker);
299
302
  return answer;
300
303
  }
301
304
  return undefined;
302
305
  }
303
- const answer = await bounded(worker
304
- .evaluate(() => {
305
- const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
306
- return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
307
- })
306
+ const answer = await bounded(readSnapshot(worker)
307
+ .then((snapshot) => snapshot ?? { decisions: [], hits: [], events: [] })
308
308
  .catch(() => undefined), WORKER_SNAPSHOT_WAIT_MS);
309
309
  if (answer === WORKER_TIMED_OUT) {
310
310
  this.unresponsiveWorkers.add(worker);
@@ -335,11 +335,11 @@ class CoveragePhaseController {
335
335
  .evaluate(({ token, url }) => {
336
336
  const entries = globalThis.__SUPERCOV_WORKER_EVIDENCE__?.() ?? [];
337
337
  const byToken = token ? entries.find((entry) => entry.token === token) : undefined;
338
- if (byToken)
339
- return byToken;
340
338
  const byUrl = entries.filter((entry) => entry.url === url);
341
- return byUrl.length === 1 ? byUrl[0] : undefined;
339
+ const found = byToken ?? (byUrl.length === 1 ? byUrl[0] : undefined);
340
+ return found ? JSON.stringify(found) : undefined;
342
341
  }, { token, url })
342
+ .then((text) => (typeof text === "string" ? JSON.parse(text) : undefined))
343
343
  .catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
344
344
  if (pushed && pushed !== WORKER_TIMED_OUT)
345
345
  return pushed;
@@ -349,11 +349,8 @@ class CoveragePhaseController {
349
349
  async snapshotPage(page) {
350
350
  const snapshots = [];
351
351
  for (const frame of page.frames()) {
352
- snapshots.push(await frame
353
- .evaluate(() => {
354
- const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
355
- return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
356
- })
352
+ snapshots.push(await readSnapshot(frame)
353
+ .then((snapshot) => snapshot ?? { decisions: [], hits: [], events: [] })
357
354
  .catch(() => ({
358
355
  decisions: [],
359
356
  hits: [],
@@ -432,6 +429,20 @@ class CoveragePhaseController {
432
429
  // learned its phase only from the next action, after it had
433
430
  // loaded, and everything it ran while loading was placed by time.
434
431
  await cdp.send("Page.enable").catch(() => undefined);
432
+ // A document that leaves hands its evidence over here: the
433
+ // next one reads what it stored only on the same origin.
434
+ cdp.on("Runtime.bindingCalled", ({ name, payload }) => {
435
+ if (name !== LEAVING_BINDING || this.runtimeSnapshots)
436
+ return;
437
+ try {
438
+ this.leftSnapshots.push(JSON.parse(payload));
439
+ }
440
+ catch {
441
+ // Not evidence.
442
+ }
443
+ });
444
+ await cdp.send("Runtime.enable").catch(() => undefined);
445
+ await cdp.send("Runtime.addBinding", { name: LEAVING_BINDING }).catch(() => undefined);
435
446
  this.cdpSessions.set(page, cdp);
436
447
  }
437
448
  const phaseId = this.requestPhaseId();
@@ -883,6 +894,63 @@ class CoveragePhaseController {
883
894
  await this.scriptUpdate.catch(() => undefined);
884
895
  }
885
896
  }
897
+ /**
898
+ * A page's or worker's evidence. It holds an event for every hit of every
899
+ * phase, tens of thousands of objects on a large frontend, and Playwright
900
+ * returns an object value by value: reading it took half a second a page at
901
+ * the end of every test (20,000 events: 470 ms, against 34 ms as one JSON
902
+ * string), and doubled a 76-test suite's time.
903
+ */
904
+ const LEAVING_BINDING = "__supercovLeaving";
905
+ /**
906
+ * One snapshot holding everything in `snapshots`, each observation once: a
907
+ * document that left gave its evidence, and the next one on its origin
908
+ * restored the same evidence and added to it.
909
+ */
910
+ function mergeSnapshots(snapshots) {
911
+ const hits = new Set();
912
+ const events = new Map();
913
+ const decisions = new Map();
914
+ const logicals = new Map();
915
+ const vectorsOf = (map, key, initial) => {
916
+ let entry = map.get(key);
917
+ if (!entry) {
918
+ entry = { ...initial, seen: new Map() };
919
+ map.set(key, entry);
920
+ }
921
+ return entry;
922
+ };
923
+ for (const snapshot of snapshots) {
924
+ for (const id of snapshot?.hits ?? [])
925
+ hits.add(id);
926
+ for (const event of snapshot?.events ?? [])
927
+ events.set(JSON.stringify(event), event);
928
+ for (const decision of snapshot?.decisions ?? []) {
929
+ const entry = vectorsOf(decisions, decision.meta?.id, { meta: decision.meta });
930
+ for (const vector of decision.vectors ?? [])
931
+ entry.seen.set(JSON.stringify(vector), vector);
932
+ }
933
+ for (const logical of snapshot?.logicals ?? []) {
934
+ const entry = vectorsOf(logicals, logical.id, { id: logical.id });
935
+ for (const vector of logical.vectors ?? [])
936
+ entry.seen.set(JSON.stringify(vector), vector);
937
+ }
938
+ }
939
+ return {
940
+ decisions: [...decisions.values()].map(({ seen, meta }) => ({ meta, vectors: [...seen.values()] })),
941
+ hits: [...hits],
942
+ events: [...events.values()],
943
+ logicals: [...logicals.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
944
+ };
945
+ }
946
+ function readSnapshot(target) {
947
+ return target
948
+ .evaluate(() => {
949
+ const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
950
+ return getSnapshot ? JSON.stringify(getSnapshot()) : undefined;
951
+ })
952
+ .then((text) => (typeof text === "string" ? JSON.parse(text) : undefined));
953
+ }
886
954
  let activeController;
887
955
  let bridgedAssertionDepth = 0;
888
956
  const controllers = new Map();
@@ -25,9 +25,6 @@ var __spreadProps = (a, b) => __spreadValues(a, b);
25
25
  // `globalThis.Buffer = undefined` to check it copes -- and the runtime runs
26
26
  // inside those tests; it must not fail where the application does not.
27
27
  var capturedBuffer = typeof Buffer === "undefined" ? void 0 : Buffer;
28
- // Likewise the timer: a test that installs fake timers would otherwise hold
29
- // the browser's evidence back until it advanced its clock.
30
- var capturedSetTimeout = setTimeout;
31
28
 
32
29
  // dist/transport.js
33
30
  var COVERAGE_SCOPE_HEADER = "x-supercov-scope";
@@ -419,7 +416,6 @@ function pushWorkerEvidence() {
419
416
  }
420
417
  runtimeGlobal.__SUPERCOV_WORKER_EVIDENCE__ = () => Array.from(received, ([token, pushed]) => ({ token, url: pushed.url, current: pushed.current, snapshot: pushed.snapshot }));
421
418
  })();
422
- var persistBrowserScheduled = false;
423
419
  var persistBrowserListenersInstalled = false;
424
420
  function persistBrowserNow() {
425
421
  if (!isBrowser)
@@ -429,31 +425,44 @@ function persistBrowserNow() {
429
425
  } catch (e) {
430
426
  }
431
427
  }
428
+ // The Playwright collector's binding takes the evidence of a document that
429
+ // is about to leave. localStorage gives it to the next document only on the
430
+ // same origin: a click that sent the page to another one (an admin's
431
+ // masquerade link, a sign-in redirect) lost every hit it made. Called from
432
+ // `pagehide` the binding never reached the collector; from `beforeunload`
433
+ // it does.
434
+ function handOverBrowser() {
435
+ try {
436
+ if (typeof runtimeGlobal.__supercovLeaving === "function")
437
+ runtimeGlobal.__supercovLeaving(JSON.stringify(coverageSnapshot()));
438
+ } catch (e) {
439
+ }
440
+ }
432
441
  function persistBrowser() {
433
442
  if (!isBrowser)
434
443
  return;
435
- // Persistence exists so evidence survives navigation, not to mirror every
436
- // event: serializing the whole snapshot per event is quadratic in a
437
- // render burst. Coalesce to one write per macrotask and flush when the
438
- // page is actually leaving.
439
- if (!persistBrowserListenersInstalled) {
440
- persistBrowserListenersInstalled = true;
441
- try {
442
- addEventListener("pagehide", persistBrowserNow);
443
- addEventListener("visibilitychange", () => {
444
- if (typeof document !== "undefined" && document.visibilityState === "hidden")
445
- persistBrowserNow();
446
- });
447
- } catch (e) {
448
- }
449
- }
450
- if (persistBrowserScheduled)
444
+ // Persistence exists so evidence survives navigation, so it is written
445
+ // when the page leaves. Written after every macrotask that recorded
446
+ // anything, the whole snapshot was serialized again and again: a large
447
+ // instrumented frontend doubled a Playwright suite's time. A page the
448
+ // test closes is read by the collector before it closes.
449
+ if (persistBrowserListenersInstalled)
451
450
  return;
452
- persistBrowserScheduled = true;
453
- capturedSetTimeout(() => {
454
- persistBrowserScheduled = false;
455
- persistBrowserNow();
456
- }, 0);
451
+ persistBrowserListenersInstalled = true;
452
+ try {
453
+ // `beforeunload` fires inside the assignment that starts the navigation,
454
+ // and the rest of that handler runs after it, before the document goes.
455
+ addEventListener("beforeunload", () => {
456
+ handOverBrowser();
457
+ Promise.resolve().then(handOverBrowser);
458
+ });
459
+ addEventListener("pagehide", persistBrowserNow);
460
+ addEventListener("visibilitychange", () => {
461
+ if (typeof document !== "undefined" && document.visibilityState === "hidden")
462
+ persistBrowserNow();
463
+ });
464
+ } catch (e) {
465
+ }
457
466
  }
458
467
  function attemptKey(scope) {
459
468
  return `${scope.runId}\0${scope.workerId}\0${scope.attemptId}`;