supercov 4.5.0 → 4.7.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 +68 -13
- package/docs/performance.md +14 -0
- package/package.json +10 -10
- package/runtime/javascript/playwright.mjs +91 -19
- package/runtime/javascript/runtime.mjs +6 -2
- 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,8 +523,13 @@ 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
|
|
515
|
-
|
|
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,
|
|
530
|
+
lockfile, configuration or toolchain change affects every test and is reported
|
|
531
|
+
as such; a test runner's settings affect only the runs that used that runner.
|
|
532
|
+
A source file
|
|
516
533
|
added since the run is outside every test's record; the working-tree check
|
|
517
534
|
says so, and the suite should run in full.
|
|
518
535
|
|
|
@@ -595,24 +612,61 @@ A test that is in two of the runs is one test in the merge. If it failed in
|
|
|
595
612
|
either, it is failed, which is why a command run again replaces its earlier
|
|
596
613
|
run and is not merged with it.
|
|
597
614
|
|
|
598
|
-
The runs may differ in their tests
|
|
599
|
-
|
|
600
|
-
|
|
615
|
+
The runs may differ in their tests, the helpers beside them, and the test
|
|
616
|
+
runners' settings, as long as nothing that changed reaches what an earlier
|
|
617
|
+
run's tests did. Measuring a new test alone and adding it to the runs before it
|
|
618
|
+
is then the whole suite's figure, without the whole suite's time:
|
|
601
619
|
|
|
602
620
|
```sh supercov-example
|
|
603
|
-
npx supercov -- npm test
|
|
604
|
-
npx supercov --
|
|
621
|
+
npx supercov -- npm test # the suite, once
|
|
622
|
+
npx supercov -- <the new test alone> # a test added since
|
|
605
623
|
npx supercov merge
|
|
606
624
|
```
|
|
607
625
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
626
|
+
Each file that differs from what an earlier run was recorded with is judged by
|
|
627
|
+
what that run's tests did with it:
|
|
628
|
+
|
|
629
|
+
- Another run's test file: it was not in this run. A file of the run's own
|
|
630
|
+
tests: what it recorded is of other tests, and the run is left out.
|
|
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.
|
|
640
|
+
- A test runner's settings: `playwright.config.*`, `vitest.config.*`,
|
|
641
|
+
`jest.config.*` and `ava.config.*` belong to their runner, and a run that
|
|
642
|
+
did not use that runner keeps. Of Playwright's settings, only those that
|
|
643
|
+
decide what a test does count: `use`, `testDir`, `webServer`,
|
|
644
|
+
`globalSetup`, `globalTeardown`, and a project's `dependencies` and
|
|
645
|
+
`teardown`. Timeouts, retries, workers, reporters, which files and names to
|
|
646
|
+
run, and projects added since schedule tests and change nothing an earlier
|
|
647
|
+
test reached. This needs the newer run to have measured Playwright.
|
|
648
|
+
- Any other configuration, such as `tsconfig.json`, `vite.config.*` or a
|
|
649
|
+
Babel file, may set how any test runs, and leaves the run out.
|
|
650
|
+
|
|
651
|
+
A run left out is named with the files and the functions that changed. The
|
|
652
|
+
merged run lists what differed and why it does not reach the earlier runs:
|
|
653
|
+
`merged across:` in `runs <id>`, and `mergedAcross` in JSON.
|
|
654
|
+
|
|
655
|
+
When you know a change Supercov counts does not alter what the earlier tests
|
|
656
|
+
ran, say so:
|
|
657
|
+
|
|
658
|
+
```sh supercov-example
|
|
659
|
+
npx supercov merge --allow-changed
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The product code, the dependencies, and what is measured must still be the
|
|
663
|
+
same, and a run whose own test files changed is still left out. What was
|
|
664
|
+
accepted is listed in `merged across:` as accepted.
|
|
611
665
|
|
|
612
666
|
`assertions assess` assesses a merged run across every suite merged into it,
|
|
613
|
-
reusing the answers saved when its parts were assessed. `tests affected`
|
|
614
|
-
|
|
615
|
-
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.
|
|
616
670
|
|
|
617
671
|
## Clean local data
|
|
618
672
|
|
|
@@ -660,6 +714,7 @@ terminal or offline environment after the package has been downloaded.
|
|
|
660
714
|
| `SUPERCOV_SOURCE_ROOTS` | Comma-separated directories or files that hold your own code, in any language; everything else is left out |
|
|
661
715
|
| `SUPERCOV_TEST_KIND` | Label the wrapped command as a test level such as `unit` or `e2e` |
|
|
662
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` |
|
|
663
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 |
|
|
664
719
|
|
|
665
720
|
Examples:
|
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.7.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/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/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.7.0",
|
|
124
|
+
"@supercov/cli-darwin-x64": "4.7.0",
|
|
125
|
+
"@supercov/cli-linux-arm64-gnu": "4.7.0",
|
|
126
|
+
"@supercov/cli-linux-arm64-musl": "4.7.0",
|
|
127
|
+
"@supercov/cli-linux-x64-gnu": "4.7.0",
|
|
128
|
+
"@supercov/cli-linux-x64-musl": "4.7.0",
|
|
129
|
+
"@supercov/cli-win32-arm64": "4.7.0",
|
|
130
|
+
"@supercov/cli-win32-x64": "4.7.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() {
|
|
@@ -1345,8 +1347,10 @@ function requestCoverageContext(value) {
|
|
|
1345
1347
|
}
|
|
1346
1348
|
}
|
|
1347
1349
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
+
// A frame of the app on another of its hosts carries only the cookies
|
|
1351
|
+
// made to cross sites.
|
|
1352
|
+
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);
|
|
1353
|
+
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
1354
|
const scope = decodeCoverageScope(typeof encodedScope === "string" ? encodedScope : void 0);
|
|
1351
1355
|
// The phase cookie outlives a test in a shared browser context just like
|
|
1352
1356
|
// the stored phase does (see currentPhaseId); a request carrying a scope
|
|
@@ -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
|