supercov 4.6.0 → 4.8.0
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 +29 -12
- package/docs/coverage-model.md +10 -1
- package/docs/performance.md +14 -0
- package/package.json +10 -10
- package/runtime/javascript/playwright.mjs +91 -19
- package/runtime/javascript/runtime.mjs +38 -4
- package/runtime/javascript/transport.mjs +5 -0
package/docs/cli.md
CHANGED
|
@@ -121,6 +121,18 @@ npx supercov -- cargo nextest run
|
|
|
121
121
|
Use the complete command you rely on before merging or deploying. A coverage run
|
|
122
122
|
preserves the wrapped command's exit status, so it can remain a CI gate.
|
|
123
123
|
|
|
124
|
+
A script that queries the run it just recorded can have Supercov write the
|
|
125
|
+
run's id to a file. `latest` is the newest run, which is another one when two
|
|
126
|
+
commands record runs side by side:
|
|
127
|
+
|
|
128
|
+
```sh supercov-example
|
|
129
|
+
SUPERCOV_RUN_ID_FILE=unit.id npx supercov -- <test command>
|
|
130
|
+
npx supercov runs <run-id> check --min-lines 80 # the id in unit.id
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The file is removed when the run starts and written when it is recorded, so a
|
|
134
|
+
run that is not recorded leaves no id behind.
|
|
135
|
+
|
|
124
136
|
## List and select runs
|
|
125
137
|
|
|
126
138
|
```sh supercov
|
|
@@ -511,9 +523,10 @@ pass in the run is listed regardless.
|
|
|
511
523
|
per line, for a runner's filter. A Vitest, Playwright or node:test test inside
|
|
512
524
|
`describe` blocks is printed as the blocks and the test joined by spaces
|
|
513
525
|
(`small charges`), which is what `vitest -t`, `--grep` and
|
|
514
|
-
`--test-name-pattern` match. A helper beside the tests
|
|
515
|
-
the tests whose files import it (a
|
|
516
|
-
|
|
526
|
+
`--test-name-pattern` match. A helper beside the tests, or source outside what is
|
|
527
|
+
measured, that changed affects the tests whose files import it (a JavaScript or
|
|
528
|
+
TypeScript helper that only gained functions affects none), and every test
|
|
529
|
+
where Supercov cannot read the imports. A dependency,
|
|
517
530
|
lockfile, configuration or toolchain change affects every test and is reported
|
|
518
531
|
as such; a test runner's settings affect only the runs that used that runner.
|
|
519
532
|
A source file
|
|
@@ -615,12 +628,15 @@ what that run's tests did with it:
|
|
|
615
628
|
|
|
616
629
|
- Another run's test file: it was not in this run. A file of the run's own
|
|
617
630
|
tests: what it recorded is of other tests, and the run is left out.
|
|
618
|
-
- A
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
631
|
+
- A file the run measured: left out, always.
|
|
632
|
+
- A helper, fixture or support file, or source outside what is measured (a
|
|
633
|
+
simulator kept out of `SUPERCOV_SOURCE_ROOTS`): left out of a run only when
|
|
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.
|
|
624
640
|
- A test runner's settings: `playwright.config.*`, `vitest.config.*`,
|
|
625
641
|
`jest.config.*` and `ava.config.*` belong to their runner, and a run that
|
|
626
642
|
did not use that runner keeps. Of Playwright's settings, only those that
|
|
@@ -648,9 +664,9 @@ same, and a run whose own test files changed is still left out. What was
|
|
|
648
664
|
accepted is listed in `merged across:` as accepted.
|
|
649
665
|
|
|
650
666
|
`assertions assess` assesses a merged run across every suite merged into it,
|
|
651
|
-
reusing the answers saved when its parts were assessed. `tests affected`
|
|
652
|
-
|
|
653
|
-
ran.
|
|
667
|
+
reusing the answers saved when its parts were assessed. `tests affected` of a
|
|
668
|
+
merged run is answered from the runs it was merged from, which hold the record
|
|
669
|
+
of what each test ran; a test in several of them is affected where any says so.
|
|
654
670
|
|
|
655
671
|
## Clean local data
|
|
656
672
|
|
|
@@ -698,6 +714,7 @@ terminal or offline environment after the package has been downloaded.
|
|
|
698
714
|
| `SUPERCOV_SOURCE_ROOTS` | Comma-separated directories or files that hold your own code, in any language; everything else is left out |
|
|
699
715
|
| `SUPERCOV_TEST_KIND` | Label the wrapped command as a test level such as `unit` or `e2e` |
|
|
700
716
|
| `SUPERCOV_SOURCE_TOOLS` | Comma-separated names of tools the test command runs that read source as text, such as a linter or a spell checker, besides Biome, oxlint, dprint, cspell and knip. Each is run on your source as you wrote it instead of on the instrumented copy |
|
|
717
|
+
| `SUPERCOV_RUN_ID_FILE` | A file to write the recorded run's id to, for a script to query that run rather than `latest` |
|
|
701
718
|
| `SUPERCOV_KEEP_WORKSPACE` | Set to `1` to leave the instrumented copy of the project in `.supercov/workspaces/` after the run, to inspect what the command ran |
|
|
702
719
|
|
|
703
720
|
Examples:
|
package/docs/coverage-model.md
CHANGED
|
@@ -96,7 +96,16 @@ Both are useful:
|
|
|
96
96
|
|
|
97
97
|
Code that runs while no test is running counts in the run's total and in no
|
|
98
98
|
test's: a server starting before the first test, modules loading, work between
|
|
99
|
-
tests.
|
|
99
|
+
tests.
|
|
100
|
+
|
|
101
|
+
A request to your app server that names no test -- a webhook from a mock
|
|
102
|
+
Stripe server Supercov does not measure -- is credited to the test running when
|
|
103
|
+
the server handled it, if that test was the only one running then. A mock that
|
|
104
|
+
runs under Supercov passes the test's identity on in its own requests, and its
|
|
105
|
+
webhook is credited exactly. The run's
|
|
106
|
+
`Attribution` line counts those tests as `with server work credited by time`,
|
|
107
|
+
and each one's `runs <id> test` page says so. Work done while tests ran at
|
|
108
|
+
once, or while none ran, stays no test's. When a run has any, the summary's `By test kind` table ends with a
|
|
100
109
|
`no test` row for what only that covered, which is why a run with one kind of
|
|
101
110
|
test can show a total above that kind's own:
|
|
102
111
|
|
package/docs/performance.md
CHANGED
|
@@ -120,6 +120,20 @@ Each completed run stores compressed evidence and metadata under
|
|
|
120
120
|
`.supercov/runs/<run-id>/`. Query views are derived from that evidence rather
|
|
121
121
|
than stored as a full report for every filter.
|
|
122
122
|
|
|
123
|
+
Most of a large run is its query index, `query-index.v1.bin`: on a suite of
|
|
124
|
+
about 400 tests with per-test evidence, 160 MB of a 216 MB run, beside 58 MB of
|
|
125
|
+
evidence. The index is a cache. Supercov builds it again from the evidence the
|
|
126
|
+
first time the run is queried without it, so a CI job that keeps runs as an
|
|
127
|
+
artifact can leave it out:
|
|
128
|
+
|
|
129
|
+
```sh supercov-example
|
|
130
|
+
tar czf supercov-runs.tgz --exclude 'query-index.v1.bin' .supercov/runs
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A merged run holds a copy of each run's evidence and an index of its own, so
|
|
134
|
+
merging adds about as much again. To hand a run to a reviewer, the HTML report
|
|
135
|
+
(`npx supercov report`) is usually the smaller artifact.
|
|
136
|
+
|
|
123
137
|
The isolated workspace may be larger because it can contain an instrumented
|
|
124
138
|
build cache. Supercov does not delete history in the background.
|
|
125
139
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.0",
|
|
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/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/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",
|
|
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.
|
|
124
|
-
"@supercov/cli-darwin-x64": "4.
|
|
125
|
-
"@supercov/cli-linux-arm64-gnu": "4.
|
|
126
|
-
"@supercov/cli-linux-arm64-musl": "4.
|
|
127
|
-
"@supercov/cli-linux-x64-gnu": "4.
|
|
128
|
-
"@supercov/cli-linux-x64-musl": "4.
|
|
129
|
-
"@supercov/cli-win32-arm64": "4.
|
|
130
|
-
"@supercov/cli-win32-x64": "4.
|
|
123
|
+
"@supercov/cli-darwin-arm64": "4.8.0",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.8.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.8.0",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.8.0",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.8.0",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.8.0",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.8.0",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.8.0"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"@playwright/test": ">=1.55.0",
|
|
@@ -18,7 +18,7 @@ import * as standardPlaywright from "@playwright/test";
|
|
|
18
18
|
import * as coverageRuntime from "./runtime.mjs";
|
|
19
19
|
import { inferTestProvenance } from "./provenance.mjs";
|
|
20
20
|
import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.mjs";
|
|
21
|
-
import { COVERAGE_PHASE_HEADER, COVERAGE_PHASE_COOKIE, COVERAGE_SCOPE_COOKIE, COVERAGE_SCOPE_HEADER, COVERAGE_CARRIER_ENV, encodeCoverageCarrier, encodeCoverageScope, serverEvidenceDirectory, serverEvidencePath, } from "./transport.mjs";
|
|
21
|
+
import { COVERAGE_PHASE_HEADER, COVERAGE_PHASE_COOKIE, COVERAGE_PHASE_SITE_COOKIE, COVERAGE_SCOPE_COOKIE, COVERAGE_SCOPE_HEADER, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_CARRIER_ENV, encodeCoverageCarrier, encodeCoverageScope, serverEvidenceDirectory, serverEvidencePath, } from "./transport.mjs";
|
|
22
22
|
export * from "@playwright/test";
|
|
23
23
|
const generatedTargetModule = "__SUPERCOV_PLAYWRIGHT_MODULE__";
|
|
24
24
|
const targetModule = process.env["SUPERCOV_PLAYWRIGHT_MODULE"] ??
|
|
@@ -236,9 +236,21 @@ class CoveragePhaseController {
|
|
|
236
236
|
// Parameter properties are stateful despite the base ESLint rule treating
|
|
237
237
|
// this as an empty constructor.
|
|
238
238
|
// eslint-disable-next-line no-useless-constructor
|
|
239
|
-
constructor(scope, configuredHeaders = {}) {
|
|
239
|
+
constructor(scope, configuredHeaders = {}, baseURL) {
|
|
240
240
|
this.scope = scope;
|
|
241
241
|
this.configuredHeaders = configuredHeaders;
|
|
242
|
+
// Where the app under test is served: the project's base URL, and the
|
|
243
|
+
// local hosts a web server listens on. A cookie is kept per host, for
|
|
244
|
+
// every port of it.
|
|
245
|
+
const hosts = ["http://localhost/", "http://127.0.0.1/"];
|
|
246
|
+
try {
|
|
247
|
+
if (baseURL)
|
|
248
|
+
hosts.unshift(new URL("/", baseURL).href);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// A base URL Playwright itself would refuse.
|
|
252
|
+
}
|
|
253
|
+
this.appHosts = [...new Set(hosts)];
|
|
242
254
|
}
|
|
243
255
|
allPages() {
|
|
244
256
|
return [...this.pages];
|
|
@@ -446,17 +458,34 @@ class CoveragePhaseController {
|
|
|
446
458
|
// own attempt, so the newest wins; the fetch patch is installed once
|
|
447
459
|
// and reads the attempt at call time, so a stale copy can never pin an
|
|
448
460
|
// old scope onto a request the way nested wrappers would.
|
|
449
|
-
await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie }) => {
|
|
461
|
+
await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie, scopeSiteCookie }) => {
|
|
450
462
|
const coverageGlobal = globalThis;
|
|
451
463
|
coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
|
|
452
464
|
coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
|
|
453
465
|
try {
|
|
454
466
|
document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
|
|
467
|
+
// And for this document's frames on another of the app's
|
|
468
|
+
// hosts, where crossing sites is allowed.
|
|
469
|
+
if (globalThis.isSecureContext)
|
|
470
|
+
document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
|
|
455
471
|
if (!coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__) {
|
|
456
472
|
const originalFetch = globalThis.fetch?.bind(globalThis);
|
|
457
473
|
if (originalFetch) {
|
|
458
474
|
coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__ = true;
|
|
459
475
|
globalThis.fetch = ((input, init) => {
|
|
476
|
+
// Only the page's own origin hears the scope. A
|
|
477
|
+
// header on a request to another origin makes the
|
|
478
|
+
// browser ask that origin first, and one that
|
|
479
|
+
// allows only the headers it uses refuses: the
|
|
480
|
+
// page's fetch failed under measurement alone.
|
|
481
|
+
try {
|
|
482
|
+
const target = new URL(input instanceof Request ? input.url : String(input), location.href);
|
|
483
|
+
if (target.origin !== location.origin)
|
|
484
|
+
return originalFetch(input, init);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return originalFetch(input, init);
|
|
488
|
+
}
|
|
460
489
|
const headers = new Headers(init?.headers ??
|
|
461
490
|
(input instanceof Request ? input.headers : undefined));
|
|
462
491
|
headers.set(scopeHeader, coverageGlobal.__SUPERCOV_ATTEMPT__?.scopeValue ?? scopeValue);
|
|
@@ -476,6 +505,7 @@ class CoveragePhaseController {
|
|
|
476
505
|
scopeHeader: COVERAGE_SCOPE_HEADER,
|
|
477
506
|
scopeValue: encodeCoverageScope(this.scope),
|
|
478
507
|
scopeCookie: COVERAGE_SCOPE_COOKIE,
|
|
508
|
+
scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
|
|
479
509
|
}).catch(() => undefined);
|
|
480
510
|
const register = (page) => {
|
|
481
511
|
const pending = this.registerPage(page).finally(() => this.pendingRegistrations.delete(pending));
|
|
@@ -513,6 +543,16 @@ class CoveragePhaseController {
|
|
|
513
543
|
if (!originalFetch)
|
|
514
544
|
return answer;
|
|
515
545
|
globalThis.fetch = ((input, init) => {
|
|
546
|
+
// As in a page: another origin would be asked first, and may
|
|
547
|
+
// refuse a header it does not expect.
|
|
548
|
+
try {
|
|
549
|
+
const target = new URL(input instanceof Request ? input.url : String(input), globalThis.location.href);
|
|
550
|
+
if (target.origin !== globalThis.location.origin)
|
|
551
|
+
return originalFetch(input, init);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return originalFetch(input, init);
|
|
555
|
+
}
|
|
516
556
|
const headers = new Headers(init?.headers ??
|
|
517
557
|
(input instanceof Request ? input.headers : undefined));
|
|
518
558
|
headers.set(scopeHeader, scopeValue);
|
|
@@ -705,13 +745,10 @@ class CoveragePhaseController {
|
|
|
705
745
|
const options = scoped[0] && typeof scoped[0] === "object"
|
|
706
746
|
? scoped[0]
|
|
707
747
|
: {};
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
...(activeCoverageHeaders() ?? {}),
|
|
713
|
-
},
|
|
714
|
-
};
|
|
748
|
+
// The scope reaches the app in cookies once the context is
|
|
749
|
+
// registered (`updateContextHeaders`); headers on the context would
|
|
750
|
+
// go to every origin the page fetches from.
|
|
751
|
+
scoped[0] = { ...options };
|
|
715
752
|
return scoped;
|
|
716
753
|
}
|
|
717
754
|
/**
|
|
@@ -734,7 +771,7 @@ class CoveragePhaseController {
|
|
|
734
771
|
}
|
|
735
772
|
async activateFrames(phaseId) {
|
|
736
773
|
await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
|
|
737
|
-
.evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
|
|
774
|
+
.evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie, scopeSiteCookie, phaseSiteCookie }) => {
|
|
738
775
|
globalThis.__SUPERCOV_PHASE_ID__ = id;
|
|
739
776
|
const coverageGlobal = globalThis;
|
|
740
777
|
// A document that loaded under an earlier test's attempt (a page
|
|
@@ -746,6 +783,10 @@ class CoveragePhaseController {
|
|
|
746
783
|
localStorage.setItem(storageKey, id);
|
|
747
784
|
document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
|
|
748
785
|
document.cookie = `${phaseCookie}=${encodeURIComponent(id)}; Path=/; SameSite=Lax`;
|
|
786
|
+
if (globalThis.isSecureContext) {
|
|
787
|
+
document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
|
|
788
|
+
document.cookie = `${phaseSiteCookie}=${encodeURIComponent(id)}; Path=/; SameSite=None; Secure`;
|
|
789
|
+
}
|
|
749
790
|
}
|
|
750
791
|
catch {
|
|
751
792
|
// Sandboxed/cross-origin frames may not expose localStorage.
|
|
@@ -757,6 +798,8 @@ class CoveragePhaseController {
|
|
|
757
798
|
scopeCookie: COVERAGE_SCOPE_COOKIE,
|
|
758
799
|
scopeValue: encodeCoverageScope(this.scope),
|
|
759
800
|
phaseCookie: COVERAGE_PHASE_COOKIE,
|
|
801
|
+
scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
|
|
802
|
+
phaseSiteCookie: COVERAGE_PHASE_SITE_COOKIE,
|
|
760
803
|
})
|
|
761
804
|
.catch(() => undefined))));
|
|
762
805
|
}
|
|
@@ -779,14 +822,43 @@ class CoveragePhaseController {
|
|
|
779
822
|
this.unresponsiveWorkers.add(worker);
|
|
780
823
|
})));
|
|
781
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Tell the app which test and phase its requests belong to, in cookies on
|
|
827
|
+
* the app's hosts. Headers set on the context went with every request the
|
|
828
|
+
* browser made, to any origin, and a header on a request to another
|
|
829
|
+
* origin makes the browser ask that origin first: one that allows only the
|
|
830
|
+
* headers it uses refused, and the page's request failed under
|
|
831
|
+
* measurement alone. A cookie never does. The page sends it to its own
|
|
832
|
+
* site from the first navigation on, and, on a secure origin, to frames
|
|
833
|
+
* of the app's other hosts as well.
|
|
834
|
+
*/
|
|
782
835
|
async updateContextHeaders(context, phaseId) {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
[
|
|
787
|
-
...(
|
|
788
|
-
|
|
789
|
-
|
|
836
|
+
const scope = encodeURIComponent(encodeCoverageScope(this.scope));
|
|
837
|
+
const phase = phaseId ? encodeURIComponent(phaseId) : undefined;
|
|
838
|
+
const pairs = (scopeName, phaseName) => [
|
|
839
|
+
[scopeName, scope],
|
|
840
|
+
...(phase ? [[phaseName, phase]] : []),
|
|
841
|
+
];
|
|
842
|
+
await Promise.all([
|
|
843
|
+
// The context's own headers are the owner's, unchanged.
|
|
844
|
+
context
|
|
845
|
+
.setExtraHTTPHeaders(this.contextConfiguredHeaders.get(context) ?? this.configuredHeaders)
|
|
846
|
+
.catch(() => undefined),
|
|
847
|
+
...this.appHosts.map((url) => {
|
|
848
|
+
const host = new URL(url);
|
|
849
|
+
// For the app's own site: requests, navigations and sockets.
|
|
850
|
+
const cookies = pairs(COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE).map(([name, value]) => ({ name, value, url, sameSite: "Lax" }));
|
|
851
|
+
// For its frames on another of its hosts: a cookie that
|
|
852
|
+
// crosses sites must be Secure, which needs https or a local
|
|
853
|
+
// host. Chromium keeps such a cookie only when it is given by
|
|
854
|
+
// domain; given by an http URL it is dropped without a word.
|
|
855
|
+
if (host.protocol === "https:" || ["localhost", "127.0.0.1"].includes(host.hostname)) {
|
|
856
|
+
cookies.push(...pairs(COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE).map(([name, value]) => ({ name, value, domain: host.hostname, path: "/", sameSite: "None", secure: true })));
|
|
857
|
+
}
|
|
858
|
+
// One host refused leaves the others theirs.
|
|
859
|
+
return context.addCookies(cookies).catch(() => undefined);
|
|
860
|
+
}),
|
|
861
|
+
]);
|
|
790
862
|
}
|
|
791
863
|
async activatePage(page, phaseId) {
|
|
792
864
|
const cdp = this.cdpSessions.get(page);
|
|
@@ -1352,7 +1424,7 @@ const instrumentedFixtures = {
|
|
|
1352
1424
|
async ({}, use, testInfo) => {
|
|
1353
1425
|
const scope = executionScope(testInfo);
|
|
1354
1426
|
const configuredHeaders = Object.fromEntries(Object.entries(testInfo.project.use.extraHTTPHeaders ?? {}).map(([name, value]) => [name, String(value)]));
|
|
1355
|
-
const controller = new CoveragePhaseController(scope, configuredHeaders);
|
|
1427
|
+
const controller = new CoveragePhaseController(scope, configuredHeaders, testInfo.project.use.baseURL);
|
|
1356
1428
|
controllers.set(scope.attemptId, controller);
|
|
1357
1429
|
activeController = controller;
|
|
1358
1430
|
directRuntime()?.activateCoverageScope(scope);
|
|
@@ -34,6 +34,8 @@ var COVERAGE_SCOPE_HEADER = "x-supercov-scope";
|
|
|
34
34
|
var COVERAGE_PHASE_HEADER = "x-supercov-phase";
|
|
35
35
|
var COVERAGE_SCOPE_COOKIE = "__supercov_scope";
|
|
36
36
|
var COVERAGE_PHASE_COOKIE = "__supercov_phase";
|
|
37
|
+
var COVERAGE_SCOPE_SITE_COOKIE = "__supercov_scope_site";
|
|
38
|
+
var COVERAGE_PHASE_SITE_COOKIE = "__supercov_phase_site";
|
|
37
39
|
var COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
|
|
38
40
|
var DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
|
|
39
41
|
function configuredServerEvidenceRoot() {
|
|
@@ -459,7 +461,16 @@ function attemptKey(scope) {
|
|
|
459
461
|
function serverRecordKey(record) {
|
|
460
462
|
var _a8;
|
|
461
463
|
const suffix = record.type === "decision" ? `${record.meta.id}:${vectorKey(record.vector)}` : record.id;
|
|
462
|
-
|
|
464
|
+
// Each request that names no test keeps its own first hits: the report
|
|
465
|
+
// credits them to the test running when it came, and a later request's
|
|
466
|
+
// work belongs to whichever test was running then.
|
|
467
|
+
const request = record.request ? `request-${record.request}:` : "";
|
|
468
|
+
return `${request}${(_a8 = record.phaseId) != null ? _a8 : "unscoped"}:${record.type}:${suffix}`;
|
|
469
|
+
}
|
|
470
|
+
// Numbers the requests a server handles that name no test.
|
|
471
|
+
function nextUnscopedRequest() {
|
|
472
|
+
state.unscopedRequests = (state.unscopedRequests || 0) + 1;
|
|
473
|
+
return state.unscopedRequests;
|
|
463
474
|
}
|
|
464
475
|
function beginBufferedServerEvidence(scope) {
|
|
465
476
|
if (isBrowser)
|
|
@@ -792,7 +803,11 @@ function appendServer(record) {
|
|
|
792
803
|
try {
|
|
793
804
|
const directory = scope ? serverEvidenceDirectory(scope) : void 0;
|
|
794
805
|
const path = scope ? serverEvidencePath(scope) : void 0;
|
|
795
|
-
|
|
806
|
+
// Work done while handling a request that named no test -- a webhook a
|
|
807
|
+
// mock server sends -- says so, with the request's number.
|
|
808
|
+
const stored = serverPhaseStorage == null ? void 0 : serverPhaseStorage.getStore();
|
|
809
|
+
const unscopedRequest = !scope && stored && stored.request ? { request: stored.request } : {};
|
|
810
|
+
const serialized = __spreadValues(__spreadValues(__spreadValues({}, record), scope ? { scope } : {}), unscopedRequest);
|
|
796
811
|
if (scope && state.bufferedAttempts.has(attemptKey(scope))) {
|
|
797
812
|
const key = attemptKey(scope);
|
|
798
813
|
const buffered = (_c = state.serverBuffers.get(key)) != null ? _c : {
|
|
@@ -1345,8 +1360,10 @@ function requestCoverageContext(value) {
|
|
|
1345
1360
|
}
|
|
1346
1361
|
}
|
|
1347
1362
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1363
|
+
// A frame of the app on another of its hosts carries only the cookies
|
|
1364
|
+
// made to cross sites.
|
|
1365
|
+
const encodedScope = (_a8 = headers.get(COVERAGE_SCOPE_HEADER)) != null ? _a8 : cookies.has(COVERAGE_SCOPE_COOKIE) ? cookies.get(COVERAGE_SCOPE_COOKIE) : cookies.get(COVERAGE_SCOPE_SITE_COOKIE);
|
|
1366
|
+
const rawPhaseId = (_b = headers.get(COVERAGE_PHASE_HEADER)) != null ? _b : cookies.has(COVERAGE_PHASE_COOKIE) ? cookies.get(COVERAGE_PHASE_COOKIE) : cookies.get(COVERAGE_PHASE_SITE_COOKIE);
|
|
1350
1367
|
const scope = decodeCoverageScope(typeof encodedScope === "string" ? encodedScope : void 0);
|
|
1351
1368
|
// The phase cookie outlives a test in a shared browser context just like
|
|
1352
1369
|
// the stored phase does (see currentPhaseId); a request carrying a scope
|
|
@@ -1383,9 +1400,24 @@ function installNodeRequestPropagation() {
|
|
|
1383
1400
|
// the server process were created by a shared before hook.
|
|
1384
1401
|
const incoming = requestCoverageContext(args[0]);
|
|
1385
1402
|
if (incoming) context = incoming.scope ? incoming : context != null ? context : currentRequestContext();
|
|
1403
|
+
// A request that names no test, to a server no test owns, is numbered:
|
|
1404
|
+
// the report credits its work to the test running when it came.
|
|
1405
|
+
if (incoming && serverPhaseStorage && !(context && (context.scope || context.phaseId)))
|
|
1406
|
+
context = { request: nextUnscopedRequest() };
|
|
1407
|
+
// Node emits a request's body and its response's events outside the
|
|
1408
|
+
// handler's context, so a handler that reads the body -- `req.on('end')`
|
|
1409
|
+
// -- ran as no test's. The request and response carry it with them.
|
|
1410
|
+
if (incoming && context) {
|
|
1411
|
+
for (const emitter of args.slice(0, 2)) {
|
|
1412
|
+
if (emitter && typeof emitter === "object" && typeof emitter.emit === "function")
|
|
1413
|
+
emitterCoverageContexts.set(emitter, context);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1386
1416
|
}
|
|
1387
1417
|
const invoke = () => Reflect.apply(original, this, [event, ...args]);
|
|
1388
1418
|
try {
|
|
1419
|
+
if (context && context.request && !context.scope)
|
|
1420
|
+
return serverPhaseStorage.run(context, () => withProbeV2Context(context, invoke));
|
|
1389
1421
|
return context ? withCoverageCarrier({ version: 1, ...context }, invoke) : invoke();
|
|
1390
1422
|
} finally {
|
|
1391
1423
|
if (event === "close") emitterCoverageContexts.delete(this);
|
|
@@ -1406,6 +1438,8 @@ function withRequestPhase(handler, event) {
|
|
|
1406
1438
|
// owner. Shared servers have no captured test scope and stay unattributed.
|
|
1407
1439
|
const inheritedContext = requestContext && requestContext.scope ? {} : registeredContext.scope ? registeredContext : requestContext === void 0 ? currentRequestContext() : {};
|
|
1408
1440
|
const context = __spreadValues(__spreadValues({}, ((_a8 = requestContext == null ? void 0 : requestContext.scope) != null ? _a8 : inheritedContext.scope) ? { scope: (_b = requestContext == null ? void 0 : requestContext.scope) != null ? _b : inheritedContext.scope } : {}), ((_c = requestContext == null ? void 0 : requestContext.phaseId) != null ? _c : inheritedContext.phaseId) ? { phaseId: (_d = requestContext == null ? void 0 : requestContext.phaseId) != null ? _d : inheritedContext.phaseId } : {});
|
|
1441
|
+
if (requestContext !== void 0 && !context.scope)
|
|
1442
|
+
context.request = nextUnscopedRequest();
|
|
1409
1443
|
const invoke = () => {
|
|
1410
1444
|
if (event === "connection" && context.scope && args[0] &&
|
|
1411
1445
|
typeof args[0] === "object" && typeof args[0].emit === "function")
|
|
@@ -2,6 +2,11 @@ export const COVERAGE_SCOPE_HEADER = "x-supercov-scope";
|
|
|
2
2
|
export const COVERAGE_PHASE_HEADER = "x-supercov-phase";
|
|
3
3
|
export const COVERAGE_SCOPE_COOKIE = "__supercov_scope";
|
|
4
4
|
export const COVERAGE_PHASE_COOKIE = "__supercov_phase";
|
|
5
|
+
// The same, for frames of the app on another of its hosts: a cookie that
|
|
6
|
+
// crosses sites must be Secure, and a Secure cookie is not sent over ws://,
|
|
7
|
+
// so the two cannot be one.
|
|
8
|
+
export const COVERAGE_SCOPE_SITE_COOKIE = "__supercov_scope_site";
|
|
9
|
+
export const COVERAGE_PHASE_SITE_COOKIE = "__supercov_phase_site";
|
|
5
10
|
export const COVERAGE_CARRIER_ENV = "SUPERCOV_CONTEXT";
|
|
6
11
|
export const DEFAULT_SERVER_EVIDENCE_ROOT = "/tmp/supercov-server-evidence";
|
|
7
12
|
// Buffer as it was when Supercov loaded. Tests replace globals -- axios's set
|