supercov 0.0.31 → 0.0.33
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/README.md +2 -1
- package/docs/getting-started.md +6 -2
- package/docs/supported-suites.md +92 -1
- package/docs/troubleshooting.md +15 -0
- package/package.json +11 -10
- package/runtime/javascript/playwright.js +271 -41
- package/runtime/javascript/runtime.js +21 -2
package/README.md
CHANGED
|
@@ -109,11 +109,12 @@ The denominator comes from source structure before the run, so adding or removin
|
|
|
109
109
|
| TypeScript | Available | `npx supercov -- npm test` |
|
|
110
110
|
| Rust | Available | `npx supercov -- cargo test` |
|
|
111
111
|
| Python | Available | `npx supercov -- pytest` |
|
|
112
|
+
| Ruby | Available | `npx supercov -- rspec` |
|
|
112
113
|
| Zig | Coming soon | — |
|
|
113
114
|
| PHP | Coming soon | — |
|
|
114
115
|
| C | Coming soon | — |
|
|
115
116
|
|
|
116
|
-
Supercov requires Node.js 22 or newer. Rust support currently uses Rust 1.95; cargo-nextest 0.9.138 and 0.9.140 are supported. Python support requires CPython 3.12 or newer and measures pytest and unittest runs.
|
|
117
|
+
Supercov requires Node.js 22 or newer. Rust support currently uses Rust 1.95; cargo-nextest 0.9.138 and 0.9.140 are supported. Python support requires CPython 3.12 or newer and measures pytest and unittest runs. Ruby support requires Ruby 3.3 or newer (3.4 or newer for full measurement) and measures RSpec, Minitest, test-unit and Cucumber runs.
|
|
117
118
|
|
|
118
119
|
## Supported test suites
|
|
119
120
|
|
package/docs/getting-started.md
CHANGED
|
@@ -9,7 +9,7 @@ npx supercov -- npm test
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
No account, config file, import, custom reporter, or hosted service is required.
|
|
12
|
-
Supercov supports JavaScript, TypeScript, Rust, and
|
|
12
|
+
Supercov supports JavaScript, TypeScript, Rust, Python, and Ruby today.
|
|
13
13
|
|
|
14
14
|
## Before you start
|
|
15
15
|
|
|
@@ -18,7 +18,8 @@ You need:
|
|
|
18
18
|
- Node.js 22 or newer;
|
|
19
19
|
- a test command that already works in the repository; and
|
|
20
20
|
- for Rust, the Rust 1.95 toolchain;
|
|
21
|
-
- for Python, CPython 3.12 or newer with pytest or unittest
|
|
21
|
+
- for Python, CPython 3.12 or newer with pytest or unittest;
|
|
22
|
+
- for Ruby, Ruby 3.4 or newer with RSpec, Minitest, test-unit or Cucumber (3.3 measures lines, methods and simple branches only).
|
|
22
23
|
|
|
23
24
|
The CLI is distributed through npm, even for Rust projects. The first `npx`
|
|
24
25
|
invocation may download Supercov from the npm registry. Supercov itself does not
|
|
@@ -41,6 +42,9 @@ npx supercov -- cargo nextest run
|
|
|
41
42
|
|
|
42
43
|
# Python
|
|
43
44
|
npx supercov -- pytest
|
|
45
|
+
|
|
46
|
+
# Ruby
|
|
47
|
+
npx supercov -- rspec
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
Supercov runs that command in an isolated, instrumented copy of the project.
|
package/docs/supported-suites.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Supported languages and test suites
|
|
2
2
|
|
|
3
|
-
Supercov supports JavaScript, TypeScript, Rust, and
|
|
3
|
+
Supercov supports JavaScript, TypeScript, Rust, Python, and Ruby today. Start with
|
|
4
4
|
the same test command the repository already uses; Supercov detects supported
|
|
5
5
|
runners inside that command.
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ npx supercov -- npm test
|
|
|
9
9
|
npx supercov -- npx playwright test
|
|
10
10
|
npx supercov -- cargo test
|
|
11
11
|
npx supercov -- pytest
|
|
12
|
+
npx supercov -- rspec
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
## Language support
|
|
@@ -19,6 +20,7 @@ npx supercov -- pytest
|
|
|
19
20
|
| TypeScript | Available | `npx supercov -- npm test` |
|
|
20
21
|
| Rust | Available | `npx supercov -- cargo test` |
|
|
21
22
|
| Python | Available | `npx supercov -- pytest` |
|
|
23
|
+
| Ruby | Available | `npx supercov -- rspec` |
|
|
22
24
|
| Zig | Coming soon | — |
|
|
23
25
|
| PHP | Coming soon | — |
|
|
24
26
|
| C | Coming soon | — |
|
|
@@ -67,6 +69,14 @@ Playwright support includes Chromium, Firefox, and WebKit, along with pages,
|
|
|
67
69
|
frames, popups, workers, request contexts, WebSockets, and test-launched child
|
|
68
70
|
processes where the runner exposes their identity.
|
|
69
71
|
|
|
72
|
+
Browsers a suite launches itself are covered too. A fixture that calls
|
|
73
|
+
`chromium.launchPersistentContext`, or `launch`/`connect` and hands out its own
|
|
74
|
+
contexts and pages in place of Playwright's `page` fixture, is adopted by each
|
|
75
|
+
test's collector: its pages are read before the fixture closes them, and a
|
|
76
|
+
context kept for the whole worker follows the current test's identity. Actions
|
|
77
|
+
on such pages are not recorded as separate phases, so their evidence is
|
|
78
|
+
attributed to the test and its assertions rather than to individual clicks.
|
|
79
|
+
|
|
70
80
|
Node child processes inherit coverage automatically. Long-running servers get
|
|
71
81
|
a short drain window after the test command finishes so buffered evidence can
|
|
72
82
|
arrive. Work without a reliable test identity is kept as background coverage
|
|
@@ -129,6 +139,87 @@ npx supercov -- uv run pytest
|
|
|
129
139
|
npx supercov -- python -m unittest
|
|
130
140
|
```
|
|
131
141
|
|
|
142
|
+
## Ruby
|
|
143
|
+
|
|
144
|
+
| Runner | Attribution | Current requirement |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| RSpec | Exact example and before/example/after phase identity | Ruby 3.4 or newer for full measurement; run with `npx supercov -- rspec` or `bundle exec rspec` |
|
|
147
|
+
| Minitest (including Minitest::Spec and ActiveSupport::TestCase) | Exact test and setup/test/teardown identity; skips recorded | `ruby -Itest ...`, `rake test`, `rails test` |
|
|
148
|
+
| test-unit | Exact test and setup/test/teardown identity; omissions and pendings recorded | `ruby -Itest ...`, `rake test` |
|
|
149
|
+
| parallel_tests, Rails process workers | Exact per worker process | Workers inherit the run through `RUBYOPT`; verified on a Rails app with bootsnap, Zeitwerk and two forked workers |
|
|
150
|
+
| Thread-parallel Minitest (`parallelize_me!`, `parallelize(with: :threads)`) | Probe observations exact per test; line, method and simple-branch observations made while phases overlapped go to the run, declared | |
|
|
151
|
+
| Cucumber | Exact scenario identity (`features/x.feature:LINE`), hook steps as setup/teardown | `cucumber`, `bundle exec cucumber` |
|
|
152
|
+
|
|
153
|
+
Supercov measures Ruby with Ruby's own `Coverage` module plus probe calls it
|
|
154
|
+
splices into application files in memory as they load. Nothing on disk is
|
|
155
|
+
rewritten or copied; the project runs with its own interpreter and bundle, and
|
|
156
|
+
Supercov only adds a `-r` entry to `RUBYOPT` and a few `SUPERCOV_*` variables.
|
|
157
|
+
No insertion adds a line, so backtraces keep their line numbers.
|
|
158
|
+
|
|
159
|
+
Measured obligations are statements, method definitions, `if`/`unless`/
|
|
160
|
+
ternary/`while`/`until` decisions with MC/DC vectors over `&&`/`||` operands,
|
|
161
|
+
`while`/`until`/`for` iteration and the idiomatic iterator blocks (`each`,
|
|
162
|
+
`map`, `times`, `select`, ...), `&&`/`||`/`||=`/`&&=` short-circuiting,
|
|
163
|
+
`case`/`when`, `case`/`in` and `&.` selection, and `begin`/`rescue` completion,
|
|
164
|
+
handler selection and propagation. `||=` and `&&=` on method-call, index and
|
|
165
|
+
constant targets are exact too: an arrival probe and a right-side probe count
|
|
166
|
+
the skipped side without re-reading the target. Blocks and lambdas are
|
|
167
|
+
statements inside their methods, not function entry points, and that includes
|
|
168
|
+
a `define_method` block: Ruby's own method coverage reports one entry per
|
|
169
|
+
method it defines, but the block's body is measured statement by statement
|
|
170
|
+
instead of as a definition.
|
|
171
|
+
|
|
172
|
+
A statement on a line Ruby's own line table never counts (`x = case`, a
|
|
173
|
+
multi-line literal assignment, a bare `begin`, `if false`) gets a probe at load
|
|
174
|
+
time instead.
|
|
175
|
+
`if true`/`if false`/`if nil` and other literal predicates are folded the way
|
|
176
|
+
Ruby folds them: no branch, and the dead arm is not an obligation. A Spring
|
|
177
|
+
preloader started before the run has no hook and fails closed; JRuby and
|
|
178
|
+
TruffleRuby are not supported.
|
|
179
|
+
|
|
180
|
+
The runtime loads through `RUBYOPT` before Bundler and requires only
|
|
181
|
+
`coverage`, so it never activates a gem an application's Gemfile pins
|
|
182
|
+
differently. Insertions are checked against Ruby itself by a sweep
|
|
183
|
+
(`scripts/ruby-position-sweep.rb`) over Ruby's whole standard library and the
|
|
184
|
+
Rails, Rack, RSpec, Minitest and Cucumber gems, about 3,000 files: every file
|
|
185
|
+
is transformed and compiled, and every position Supercov expects is compared
|
|
186
|
+
with what Ruby reports. With `--load` each file also runs twice, untouched and
|
|
187
|
+
transformed, so the probes are proven to preserve behaviour and every method
|
|
188
|
+
position is checked.
|
|
189
|
+
|
|
190
|
+
A `begin` whose body ends in an expression that can `return` from inside
|
|
191
|
+
itself has its handlers and propagation measured as usual, but its normal
|
|
192
|
+
completion is declared instead of probed unless every branch of that
|
|
193
|
+
expression can carry the probe: Ruby cannot pass such an expression as an
|
|
194
|
+
argument, which is what a probe wrapper does.
|
|
195
|
+
|
|
196
|
+
Measuring never breaks the program being measured. If a file cannot be
|
|
197
|
+
compiled with its probes, it loads unmodified: Ruby's `Coverage` still
|
|
198
|
+
measures its lines, methods and own branches, and only the obligations a
|
|
199
|
+
probe would have proven are declared for that file. Setting
|
|
200
|
+
`SUPERCOV_RUBY_SKIP_PROBES` to a comma-separated list of path fragments puts
|
|
201
|
+
chosen files on that same path deliberately, which is the escape hatch if
|
|
202
|
+
instrumentation ever disagrees with one of yours.
|
|
203
|
+
|
|
204
|
+
An obligation Supercov declares unmeasured leaves the obligation totals and is
|
|
205
|
+
reported as a measurement limit rather than a gap. Its line still counts in
|
|
206
|
+
the line total, as it does for every other language, so a file with declared
|
|
207
|
+
obligations reads slightly pessimistically on lines.
|
|
208
|
+
|
|
209
|
+
Ruby 3.3 does not apply its `Coverage` module to code compiled by a load hook,
|
|
210
|
+
so on 3.3 Supercov measures through `Coverage` alone: lines, methods and the
|
|
211
|
+
branches Ruby reports itself. Everything that needs a probe (multi-condition
|
|
212
|
+
decisions, `||=`, loops, `rescue` flow, a second statement on a line) is
|
|
213
|
+
declared unmeasured on that interpreter rather than shown as a gap. Ruby 3.4
|
|
214
|
+
and newer measure everything.
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
npx supercov -- rspec
|
|
218
|
+
npx supercov -- bundle exec rspec
|
|
219
|
+
npx supercov -- ruby -Itest test/shapes_test.rb
|
|
220
|
+
npx supercov -- bin/rails test
|
|
221
|
+
```
|
|
222
|
+
|
|
132
223
|
## Containers, VMs, and remote execution
|
|
133
224
|
|
|
134
225
|
Supercov can collect from supported processes launched through a container, VM,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -60,6 +60,21 @@ Compare the command with [Supported suites](supported-suites.md). If the runner
|
|
|
60
60
|
should be supported, preserve the summary and exact command when reporting the
|
|
61
61
|
problem.
|
|
62
62
|
|
|
63
|
+
## A Ruby file behaves differently while Supercov measures it
|
|
64
|
+
|
|
65
|
+
Supercov splices probe calls into Ruby sources in memory as they load, and a
|
|
66
|
+
file that cannot be compiled with them is measured through Ruby's `Coverage`
|
|
67
|
+
module alone rather than failing the run. To put a file on that path
|
|
68
|
+
deliberately, name a fragment of its path:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
SUPERCOV_RUBY_SKIP_PROBES=app/models/order.rb npx supercov -- bundle exec rspec
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Its lines, methods and simple branches stay measured; everything that needs a
|
|
75
|
+
probe is declared as a measurement limit for that file. Please report the file,
|
|
76
|
+
since Supercov aims to instrument every Ruby source correctly.
|
|
77
|
+
|
|
63
78
|
## A run is marked stale
|
|
64
79
|
|
|
65
80
|
A stored run remains valid history, but it stops describing the current
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "Zero-edit, runner-aware coverage completeness for JavaScript, TypeScript, Rust, and Python test suites",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"test:watchdog": "cargo build -p supercov && node scripts/watchdog-integration.mjs",
|
|
51
51
|
"test:engine-contract": "cargo build -p supercov && node scripts/engine-contract.mjs",
|
|
52
52
|
"test:agent": "cargo build -p supercov && node scripts/agent-query-eval.mjs",
|
|
53
|
-
"test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && 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-playwright-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",
|
|
53
|
+
"test:engine": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && cargo test --workspace && cargo build -p supercov && npm run test:runtime && npm run test:rust-assets && 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-playwright-integration.mjs && node scripts/rust-custom-browser-playwright-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",
|
|
54
54
|
"test:platform": "cargo test --workspace && cargo build -p supercov && node scripts/rust-process-supervision.mjs && node scripts/workspace-crash-integration.mjs",
|
|
55
55
|
"test:native-package": "cargo build --release -p supercov && node scripts/native-package-integration.mjs && node scripts/native-release-set-integration.mjs",
|
|
56
56
|
"test:pypi-wheel": "node scripts/pypi-wheel-integration.mjs",
|
|
@@ -64,18 +64,19 @@
|
|
|
64
64
|
"benchmark:check": "cargo build --release -p supercov && node scripts/rust-transform-benchmark.mjs",
|
|
65
65
|
"benchmark:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-benchmark.mjs",
|
|
66
66
|
"check": "cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings && npm run test && npm run test:runtime && npm run test:rust-assets && node scripts/package-preflight.mjs",
|
|
67
|
-
"release:check": "npm run check && npm run test:engine && npm run test:fixture && npm run test:watchdog && npm run test:engine-contract && npm run test:agent && npm run test:python-monitoring && npm run test:packed-npx && npm run test:clang-mcdc && npm run benchmark:check",
|
|
67
|
+
"release:check": "npm run check && npm run test:engine && npm run test:fixture && npm run test:watchdog && npm run test:engine-contract && npm run test:agent && npm run test:python-monitoring && npm run test:ruby-coverage && npm run test:packed-npx && npm run test:clang-mcdc && npm run benchmark:check",
|
|
68
68
|
"prepack": "node scripts/package-preflight.mjs",
|
|
69
69
|
"prepublishOnly": "npm run release:check",
|
|
70
|
-
"test:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-integration.mjs"
|
|
70
|
+
"test:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-integration.mjs",
|
|
71
|
+
"test:ruby-coverage": "cargo build -p supercov && node scripts/ruby-coverage-integration.mjs"
|
|
71
72
|
},
|
|
72
73
|
"optionalDependencies": {
|
|
73
|
-
"@supercov/cli-darwin-arm64": "0.0.
|
|
74
|
-
"@supercov/cli-darwin-x64": "0.0.
|
|
75
|
-
"@supercov/cli-linux-arm64-gnu": "0.0.
|
|
76
|
-
"@supercov/cli-linux-arm64-musl": "0.0.
|
|
77
|
-
"@supercov/cli-linux-x64-gnu": "0.0.
|
|
78
|
-
"@supercov/cli-linux-x64-musl": "0.0.
|
|
74
|
+
"@supercov/cli-darwin-arm64": "0.0.33",
|
|
75
|
+
"@supercov/cli-darwin-x64": "0.0.33",
|
|
76
|
+
"@supercov/cli-linux-arm64-gnu": "0.0.33",
|
|
77
|
+
"@supercov/cli-linux-arm64-musl": "0.0.33",
|
|
78
|
+
"@supercov/cli-linux-x64-gnu": "0.0.33",
|
|
79
|
+
"@supercov/cli-linux-x64-musl": "0.0.33"
|
|
79
80
|
},
|
|
80
81
|
"peerDependencies": {
|
|
81
82
|
"@playwright/test": ">=1.55.0",
|
|
@@ -168,6 +168,20 @@ class CoveragePhaseController {
|
|
|
168
168
|
scriptUpdate = Promise.resolve();
|
|
169
169
|
proxyCache = new WeakMap();
|
|
170
170
|
runtimeSnapshots;
|
|
171
|
+
// Contexts this controller found rather than created: it does not know
|
|
172
|
+
// what `extraHTTPHeaders` their owner configured, so it must not rewrite
|
|
173
|
+
// them (the scope cookie and the fetch patch still carry attribution).
|
|
174
|
+
adoptedContexts = new Set();
|
|
175
|
+
// Listeners installed on contexts that outlive this test, removed on
|
|
176
|
+
// dispose so a worker-scoped context does not keep registering pages with
|
|
177
|
+
// controllers of tests that already finished.
|
|
178
|
+
contextListeners = new Map();
|
|
179
|
+
// Snapshots read from pages the suite closed before teardown. A `page`
|
|
180
|
+
// fixture override tears down before the collector's own fixture, and a
|
|
181
|
+
// closed page cannot be evaluated, so the evidence is taken on the way out.
|
|
182
|
+
earlySnapshots = [];
|
|
183
|
+
snapshottedPages = new WeakSet();
|
|
184
|
+
disposed = false;
|
|
171
185
|
// Parameter properties are stateful despite the base ESLint rule treating
|
|
172
186
|
// this as an empty constructor.
|
|
173
187
|
// eslint-disable-next-line no-useless-constructor
|
|
@@ -185,21 +199,15 @@ class CoveragePhaseController {
|
|
|
185
199
|
if (this.runtimeSnapshots)
|
|
186
200
|
return this.runtimeSnapshots;
|
|
187
201
|
timingCount("collectRuntimeSnapshots.cold");
|
|
188
|
-
|
|
202
|
+
// Contexts the suite created without going through any fixture the
|
|
203
|
+
// collector wraps (a raw `browser.newContext()` mid-test) are found
|
|
204
|
+
// through their browser now, so their still-open pages are read too.
|
|
205
|
+
await this.adoptTrackedContexts().catch(() => undefined);
|
|
206
|
+
const snapshots = [...this.earlySnapshots];
|
|
189
207
|
for (const page of this.allPages()) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
194
|
-
return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
|
|
195
|
-
})
|
|
196
|
-
.catch(() => ({
|
|
197
|
-
decisions: [],
|
|
198
|
-
hits: [],
|
|
199
|
-
events: [],
|
|
200
|
-
}));
|
|
201
|
-
snapshots.push(snapshot);
|
|
202
|
-
}
|
|
208
|
+
if (this.snapshottedPages.has(page))
|
|
209
|
+
continue;
|
|
210
|
+
snapshots.push(...(await this.snapshotPage(page)));
|
|
203
211
|
}
|
|
204
212
|
for (const worker of this.allWorkers()) {
|
|
205
213
|
const snapshot = await worker
|
|
@@ -217,12 +225,50 @@ class CoveragePhaseController {
|
|
|
217
225
|
this.runtimeSnapshots = snapshots;
|
|
218
226
|
return snapshots;
|
|
219
227
|
}
|
|
228
|
+
async snapshotPage(page) {
|
|
229
|
+
const snapshots = [];
|
|
230
|
+
for (const frame of page.frames()) {
|
|
231
|
+
snapshots.push(await frame
|
|
232
|
+
.evaluate(() => {
|
|
233
|
+
const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
|
|
234
|
+
return getSnapshot?.() ?? { decisions: [], hits: [], events: [] };
|
|
235
|
+
})
|
|
236
|
+
.catch(() => ({
|
|
237
|
+
decisions: [],
|
|
238
|
+
hits: [],
|
|
239
|
+
events: [],
|
|
240
|
+
})));
|
|
241
|
+
}
|
|
242
|
+
return snapshots;
|
|
243
|
+
}
|
|
244
|
+
/** Read a registered page's evidence while it can still be evaluated. */
|
|
245
|
+
async snapshotBeforeClose(page) {
|
|
246
|
+
if (!this.pages.has(page) || this.snapshottedPages.has(page) || this.runtimeSnapshots)
|
|
247
|
+
return;
|
|
248
|
+
this.snapshottedPages.add(page);
|
|
249
|
+
this.earlySnapshots.push(...(await this.snapshotPage(page)));
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Register every live context the process created outside the wrapped
|
|
253
|
+
* fixtures: contexts launched directly (`launchPersistentContext`) and
|
|
254
|
+
* every context of every browser launched or connected directly.
|
|
255
|
+
*/
|
|
256
|
+
async adoptTrackedContexts() {
|
|
257
|
+
if (this.disposed)
|
|
258
|
+
return;
|
|
259
|
+
for (const context of liveTrackedContexts()) {
|
|
260
|
+
if (!this.contexts.has(context))
|
|
261
|
+
await this.registerContext(context, this.configuredHeaders, { adopted: true }).catch(() => undefined);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
220
264
|
async registerPage(page) {
|
|
221
|
-
if (this.pages.has(page))
|
|
265
|
+
if (this.pages.has(page) || this.disposed)
|
|
222
266
|
return;
|
|
223
267
|
timingCount("registerPage");
|
|
224
268
|
this.pages.add(page);
|
|
225
|
-
await this.registerContext(page.context()
|
|
269
|
+
await this.registerContext(page.context(), this.configuredHeaders, {
|
|
270
|
+
adopted: !this.contexts.has(page.context()) && liveTrackedContexts().has(page.context()),
|
|
271
|
+
});
|
|
226
272
|
const cdp = await page.context().newCDPSession(page).catch(() => undefined);
|
|
227
273
|
if (cdp)
|
|
228
274
|
this.cdpSessions.set(page, cdp);
|
|
@@ -235,26 +281,40 @@ class CoveragePhaseController {
|
|
|
235
281
|
for (const worker of page.workers())
|
|
236
282
|
void this.registerWorker(worker);
|
|
237
283
|
}
|
|
238
|
-
async registerContext(context, configuredHeaders = this.configuredHeaders) {
|
|
239
|
-
if (this.contexts.has(context))
|
|
284
|
+
async registerContext(context, configuredHeaders = this.configuredHeaders, { adopted = false } = {}) {
|
|
285
|
+
if (this.contexts.has(context) || this.disposed)
|
|
240
286
|
return;
|
|
241
287
|
this.contexts.add(context);
|
|
242
288
|
this.contextConfiguredHeaders.set(context, configuredHeaders);
|
|
289
|
+
if (adopted)
|
|
290
|
+
this.adoptedContexts.add(context);
|
|
291
|
+
// A context that outlives its test (a worker-scoped browser fixture)
|
|
292
|
+
// is registered again by every later test, so this script accumulates
|
|
293
|
+
// on it and each new document runs every copy in registration order.
|
|
294
|
+
// That is made harmless rather than avoided: every copy publishes its
|
|
295
|
+
// own attempt, so the newest wins; the fetch patch is installed once
|
|
296
|
+
// and reads the attempt at call time, so a stale copy can never pin an
|
|
297
|
+
// old scope onto a request the way nested wrappers would.
|
|
243
298
|
await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie }) => {
|
|
244
|
-
|
|
299
|
+
const coverageGlobal = globalThis;
|
|
300
|
+
coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
|
|
301
|
+
coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
|
|
245
302
|
try {
|
|
246
303
|
document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
if (!coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__) {
|
|
305
|
+
const originalFetch = globalThis.fetch?.bind(globalThis);
|
|
306
|
+
if (originalFetch) {
|
|
307
|
+
coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__ = true;
|
|
308
|
+
globalThis.fetch = ((input, init) => {
|
|
309
|
+
const headers = new Headers(init?.headers ??
|
|
310
|
+
(input instanceof Request ? input.headers : undefined));
|
|
311
|
+
headers.set(scopeHeader, coverageGlobal.__SUPERCOV_ATTEMPT__?.scopeValue ?? scopeValue);
|
|
312
|
+
const phase = coverageGlobal.__SUPERCOV_PHASE_ID__;
|
|
313
|
+
if (phase)
|
|
314
|
+
headers.set("x-supercov-phase", phase);
|
|
315
|
+
return originalFetch(input, { ...init, headers });
|
|
316
|
+
});
|
|
317
|
+
}
|
|
258
318
|
}
|
|
259
319
|
}
|
|
260
320
|
catch {
|
|
@@ -265,18 +325,21 @@ class CoveragePhaseController {
|
|
|
265
325
|
scopeHeader: COVERAGE_SCOPE_HEADER,
|
|
266
326
|
scopeValue: encodeCoverageScope(this.scope),
|
|
267
327
|
scopeCookie: COVERAGE_SCOPE_COOKIE,
|
|
268
|
-
});
|
|
328
|
+
}).catch(() => undefined);
|
|
269
329
|
const register = (page) => {
|
|
270
330
|
const pending = this.registerPage(page).finally(() => this.pendingRegistrations.delete(pending));
|
|
271
331
|
this.pendingRegistrations.add(pending);
|
|
272
332
|
};
|
|
273
|
-
|
|
274
|
-
context.on("serviceworker", (worker) => {
|
|
333
|
+
const registerServiceWorker = (worker) => {
|
|
275
334
|
void this.registerWorker(worker);
|
|
276
|
-
}
|
|
335
|
+
};
|
|
336
|
+
context.on("page", register);
|
|
337
|
+
context.on("serviceworker", registerServiceWorker);
|
|
338
|
+
this.contextListeners.set(context, [["page", register], ["serviceworker", registerServiceWorker]]);
|
|
277
339
|
for (const worker of context.serviceWorkers())
|
|
278
340
|
void this.registerWorker(worker);
|
|
279
|
-
|
|
341
|
+
if (!adopted)
|
|
342
|
+
await this.updateContextHeaders(context, this.requestPhaseId());
|
|
280
343
|
for (const page of context.pages())
|
|
281
344
|
register(page);
|
|
282
345
|
}
|
|
@@ -333,8 +396,14 @@ class CoveragePhaseController {
|
|
|
333
396
|
return this.activePhaseId;
|
|
334
397
|
}
|
|
335
398
|
async dispose() {
|
|
399
|
+
this.disposed = true;
|
|
336
400
|
await Promise.all([...this.pendingRegistrations]);
|
|
337
401
|
await this.scriptUpdate;
|
|
402
|
+
for (const [context, listeners] of this.contextListeners) {
|
|
403
|
+
for (const [event, listener] of listeners)
|
|
404
|
+
context.off?.(event, listener);
|
|
405
|
+
}
|
|
406
|
+
this.contextListeners.clear();
|
|
338
407
|
for (const [page, cdp] of this.cdpSessions) {
|
|
339
408
|
const identifier = this.newDocumentScriptIds.get(page);
|
|
340
409
|
if (identifier)
|
|
@@ -431,10 +500,15 @@ class CoveragePhaseController {
|
|
|
431
500
|
ArrayBuffer.isView(result))
|
|
432
501
|
return result;
|
|
433
502
|
const candidate = result;
|
|
503
|
+
if (typeof candidate["contexts"] === "function" &&
|
|
504
|
+
typeof candidate["newContext"] === "function")
|
|
505
|
+
trackBrowser(result);
|
|
434
506
|
if (typeof candidate["pages"] === "function" &&
|
|
435
|
-
typeof candidate["route"] === "function")
|
|
507
|
+
typeof candidate["route"] === "function") {
|
|
508
|
+
trackContext(result);
|
|
436
509
|
await this.registerContext(result, (sourceArgs?.[0]
|
|
437
510
|
?.extraHTTPHeaders ?? this.configuredHeaders));
|
|
511
|
+
}
|
|
438
512
|
if (typeof candidate["frames"] === "function" &&
|
|
439
513
|
typeof candidate["context"] === "function")
|
|
440
514
|
await this.registerPage(result);
|
|
@@ -472,12 +546,18 @@ class CoveragePhaseController {
|
|
|
472
546
|
return scoped;
|
|
473
547
|
}
|
|
474
548
|
async activateInBrowser(phaseId) {
|
|
475
|
-
await timed("activate.contextHeaders", () => Promise.all([...this.contexts]
|
|
549
|
+
await timed("activate.contextHeaders", () => Promise.all([...this.contexts]
|
|
550
|
+
.filter((context) => !this.adoptedContexts.has(context))
|
|
551
|
+
.map((context) => this.updateContextHeaders(context, phaseId))));
|
|
476
552
|
await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
|
|
477
|
-
.evaluate(({ id, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
|
|
553
|
+
.evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie }) => {
|
|
478
554
|
globalThis.__SUPERCOV_PHASE_ID__ = id;
|
|
479
555
|
const coverageGlobal = globalThis;
|
|
480
|
-
|
|
556
|
+
// A document that loaded under an earlier test's attempt (a page
|
|
557
|
+
// adopted mid-life from a shared context) follows the current one.
|
|
558
|
+
coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
|
|
559
|
+
coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
|
|
560
|
+
coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(attemptId, id);
|
|
481
561
|
try {
|
|
482
562
|
localStorage.setItem(storageKey, id);
|
|
483
563
|
document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
|
|
@@ -488,6 +568,7 @@ class CoveragePhaseController {
|
|
|
488
568
|
}
|
|
489
569
|
}, {
|
|
490
570
|
id: phaseId,
|
|
571
|
+
attemptId: this.scope.attemptId,
|
|
491
572
|
storageKey: PHASE_STORAGE_KEY,
|
|
492
573
|
scopeCookie: COVERAGE_SCOPE_COOKIE,
|
|
493
574
|
scopeValue: encodeCoverageScope(this.scope),
|
|
@@ -538,6 +619,129 @@ class CoveragePhaseController {
|
|
|
538
619
|
let activeController;
|
|
539
620
|
let bridgedAssertionDepth = 0;
|
|
540
621
|
const controllers = new Map();
|
|
622
|
+
// Browsers and contexts the process created without any fixture the collector
|
|
623
|
+
// wraps. Test harnesses routinely launch their own browser in a worker-scoped
|
|
624
|
+
// fixture (`chromium.launchPersistentContext` for a shared profile,
|
|
625
|
+
// `chromium.launch`/`connect` plus `browser.newContext()` per test) and
|
|
626
|
+
// override `page` on top; those objects never pass through the `browser`/`page`
|
|
627
|
+
// fixtures, and imports made from inside node_modules are never redirected to
|
|
628
|
+
// this shim, so wrapping exports would not see them either. Every page they
|
|
629
|
+
// opened ran unmeasured. The Playwright classes are patched below so such
|
|
630
|
+
// launches are recorded here, and each test's controller adopts what is live.
|
|
631
|
+
const trackedBrowsers = new Set();
|
|
632
|
+
const trackedContexts = new Set();
|
|
633
|
+
const patchedPrototypes = new WeakSet();
|
|
634
|
+
function trackBrowser(browser) {
|
|
635
|
+
if (!browser || typeof browser !== "object" || trackedBrowsers.has(browser))
|
|
636
|
+
return;
|
|
637
|
+
trackedBrowsers.add(browser);
|
|
638
|
+
browser.once?.("disconnected", () => trackedBrowsers.delete(browser));
|
|
639
|
+
patchBrowserPrototype(browser);
|
|
640
|
+
}
|
|
641
|
+
function trackContext(context) {
|
|
642
|
+
if (!context || typeof context !== "object" || trackedContexts.has(context))
|
|
643
|
+
return;
|
|
644
|
+
trackedContexts.add(context);
|
|
645
|
+
context.once?.("close", () => trackedContexts.delete(context));
|
|
646
|
+
patchContextPrototype(context);
|
|
647
|
+
}
|
|
648
|
+
function liveTrackedContexts() {
|
|
649
|
+
const contexts = new Set(trackedContexts);
|
|
650
|
+
for (const browser of trackedBrowsers) {
|
|
651
|
+
try {
|
|
652
|
+
for (const context of browser.contexts())
|
|
653
|
+
contexts.add(context);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// A browser that disconnected between the event and this sweep.
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return contexts;
|
|
660
|
+
}
|
|
661
|
+
/** The controller that registered `page`, whichever test it belongs to. */
|
|
662
|
+
function controllerOwning(page) {
|
|
663
|
+
if (activeController?.pages.has(page))
|
|
664
|
+
return activeController;
|
|
665
|
+
for (const controller of controllers.values())
|
|
666
|
+
if (controller.pages.has(page))
|
|
667
|
+
return controller;
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
function patchOnce(target, method, replace) {
|
|
671
|
+
const prototype = target && Object.getPrototypeOf(target);
|
|
672
|
+
if (!prototype || typeof prototype[method] !== "function")
|
|
673
|
+
return;
|
|
674
|
+
const marker = `__SUPERCOV_PATCHED_${method}__`;
|
|
675
|
+
if (prototype[marker])
|
|
676
|
+
return;
|
|
677
|
+
Object.defineProperty(prototype, marker, { value: true });
|
|
678
|
+
prototype[method] = replace(prototype[method]);
|
|
679
|
+
}
|
|
680
|
+
/** Record every context a directly launched or connected browser creates. */
|
|
681
|
+
function patchBrowserPrototype(browser) {
|
|
682
|
+
const prototype = Object.getPrototypeOf(browser);
|
|
683
|
+
if (!prototype || patchedPrototypes.has(prototype))
|
|
684
|
+
return;
|
|
685
|
+
patchedPrototypes.add(prototype);
|
|
686
|
+
patchOnce(browser, "newContext", (original) => async function (...args) {
|
|
687
|
+
const context = await original.apply(this, args);
|
|
688
|
+
trackContext(context);
|
|
689
|
+
const controller = activeController;
|
|
690
|
+
if (controller && !controller.contexts.has(context))
|
|
691
|
+
await controller.registerContext(context, args[0]?.extraHTTPHeaders ?? controller.configuredHeaders, { adopted: true }).catch(() => undefined);
|
|
692
|
+
return context;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Read a page's evidence before the suite closes it. A `page` fixture defined
|
|
697
|
+
* downstream of this shim tears down before the collector's own fixture, and
|
|
698
|
+
* a customer context opened mid-test is usually closed in a `finally`; either
|
|
699
|
+
* way the page is gone by the time the test's snapshots are collected.
|
|
700
|
+
*/
|
|
701
|
+
function patchContextPrototype(context) {
|
|
702
|
+
patchOnce(context, "close", (original) => async function (...args) {
|
|
703
|
+
for (const page of this.pages()) {
|
|
704
|
+
await controllerOwning(page)?.snapshotBeforeClose(page).catch(() => undefined);
|
|
705
|
+
}
|
|
706
|
+
return original.apply(this, args);
|
|
707
|
+
});
|
|
708
|
+
for (const page of context.pages())
|
|
709
|
+
patchPagePrototype(page);
|
|
710
|
+
context.on?.("page", patchPagePrototype);
|
|
711
|
+
}
|
|
712
|
+
function patchPagePrototype(page) {
|
|
713
|
+
patchOnce(page, "close", (original) => async function (...args) {
|
|
714
|
+
await controllerOwning(this)?.snapshotBeforeClose(this).catch(() => undefined);
|
|
715
|
+
return original.apply(this, args);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Patch the browser types' launch and connect paths on their shared prototype
|
|
720
|
+
* so every browser or context the process creates is tracked, whichever module
|
|
721
|
+
* path imported them. When a test is running its controller registers the new
|
|
722
|
+
* object at once; otherwise the next controller adopts it.
|
|
723
|
+
*/
|
|
724
|
+
function installBrowserLaunchTracking() {
|
|
725
|
+
const browserType = standardPlaywright.chromium ?? standardPlaywright.firefox ?? standardPlaywright.webkit;
|
|
726
|
+
if (!browserType)
|
|
727
|
+
return;
|
|
728
|
+
for (const method of ["launch", "connect", "connectOverCDP"]) {
|
|
729
|
+
patchOnce(browserType, method, (original) => async function (...args) {
|
|
730
|
+
const browser = await original.apply(this, args);
|
|
731
|
+
trackBrowser(browser);
|
|
732
|
+
return browser;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
patchOnce(browserType, "launchPersistentContext", (original) => async function (...args) {
|
|
736
|
+
const context = await original.apply(this, args);
|
|
737
|
+
trackContext(context);
|
|
738
|
+
const controller = activeController;
|
|
739
|
+
if (controller)
|
|
740
|
+
await controller.registerContext(context, args[1]?.extraHTTPHeaders ?? controller.configuredHeaders, { adopted: true }).catch(() => undefined);
|
|
741
|
+
return context;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
installBrowserLaunchTracking();
|
|
541
745
|
const directRuntime = () => globalThis.__SUPERCOV_DIRECT_RUNTIME__ ?? coverageRuntime;
|
|
542
746
|
globalThis.__SUPERCOV_ASSERTION_PHASE_BRIDGE__ = (operation, source, callback) => {
|
|
543
747
|
const controller = activeController;
|
|
@@ -839,7 +1043,7 @@ function mergeCollectedPhases(controllerPhases, fallbackPhases) {
|
|
|
839
1043
|
});
|
|
840
1044
|
return [...controllerPhases, ...remaining];
|
|
841
1045
|
}
|
|
842
|
-
const
|
|
1046
|
+
const instrumentedFixtures = {
|
|
843
1047
|
page: async ({ page }, use, testInfo) => {
|
|
844
1048
|
const scope = executionScope(testInfo);
|
|
845
1049
|
const controller = controllers.get(scope.attemptId);
|
|
@@ -887,6 +1091,7 @@ const instrumentedTest = base.extend({
|
|
|
887
1091
|
controllers.set(scope.attemptId, controller);
|
|
888
1092
|
activeController = controller;
|
|
889
1093
|
directRuntime()?.activateCoverageScope(scope);
|
|
1094
|
+
await controller.adoptTrackedContexts().catch(() => undefined);
|
|
890
1095
|
const serverOutput = serverEvidencePath(scope);
|
|
891
1096
|
mkdirSync(serverEvidenceDirectory(scope), { recursive: true });
|
|
892
1097
|
rmSync(serverOutput, { force: true });
|
|
@@ -951,6 +1156,31 @@ const instrumentedTest = base.extend({
|
|
|
951
1156
|
},
|
|
952
1157
|
{ auto: true },
|
|
953
1158
|
],
|
|
954
|
-
}
|
|
1159
|
+
};
|
|
1160
|
+
const instrumentedTest = base.extend(instrumentedFixtures);
|
|
955
1161
|
export const test = instrumentedTest;
|
|
1162
|
+
/**
|
|
1163
|
+
* A Playwright `test` object: callable, and carrying the API a spec drives.
|
|
1164
|
+
* Facades export several -- one per fixture set -- and every one of them must
|
|
1165
|
+
* collect. Instrumenting only the discovered export left a real suite's
|
|
1166
|
+
* storefront fixture entirely unmeasured: its 20 tests ran with no controller,
|
|
1167
|
+
* so nothing they executed was ever read back.
|
|
1168
|
+
*/
|
|
1169
|
+
function isPlaywrightTest(value) {
|
|
1170
|
+
return typeof value === "function" &&
|
|
1171
|
+
typeof value.extend === "function" &&
|
|
1172
|
+
typeof value.describe === "function" &&
|
|
1173
|
+
typeof value.use === "function";
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Re-export one of the facade's values. A test object is extended with the
|
|
1177
|
+
* collector's fixtures; the overrides compose with the facade's own (`page`
|
|
1178
|
+
* receives the facade's page and wraps it), so a custom browser fixture keeps
|
|
1179
|
+
* working and is measured. Anything else passes through untouched.
|
|
1180
|
+
*/
|
|
1181
|
+
export function __supercovAdapterExport(value) {
|
|
1182
|
+
if (!isPlaywrightTest(value))
|
|
1183
|
+
return value;
|
|
1184
|
+
return value === base ? instrumentedTest : value.extend(instrumentedFixtures);
|
|
1185
|
+
}
|
|
956
1186
|
/*__SUPERCOV_ADAPTER_EXPORTS__*/
|
|
@@ -808,14 +808,28 @@ function installServerChildPropagation() {
|
|
|
808
808
|
runtimeGlobal.__SUPERCOV_CHILD_PATCHED__ = true;
|
|
809
809
|
}
|
|
810
810
|
installServerChildPropagation();
|
|
811
|
+
/**
|
|
812
|
+
* Phase ids are minted as `<attemptId>:phase:<n>`, so a phase can be checked
|
|
813
|
+
* against the attempt it claims to belong to without a lookup.
|
|
814
|
+
*/
|
|
815
|
+
function phaseBelongsToAttempt(phaseId, attemptId) {
|
|
816
|
+
return typeof phaseId === "string" && typeof attemptId === "string" && attemptId.length > 0 && phaseId.startsWith(`${attemptId}:phase:`);
|
|
817
|
+
}
|
|
811
818
|
function currentPhaseId() {
|
|
812
819
|
if (runtimeGlobal.__SUPERCOV_PHASE_ID__)
|
|
813
820
|
return runtimeGlobal.__SUPERCOV_PHASE_ID__;
|
|
814
821
|
if (!isBrowser)
|
|
815
822
|
return currentRequestContext().phaseId;
|
|
816
823
|
try {
|
|
824
|
+
// The stored phase is per origin, so in a browser context that outlives
|
|
825
|
+
// its test (a persistent profile shared by a whole worker) it still holds
|
|
826
|
+
// the previous test's last phase when the next test's document loads. A
|
|
827
|
+
// phase from another attempt would tag this test's evidence with a phase
|
|
828
|
+
// it never reported, which the archive rightly rejects; only a phase of
|
|
829
|
+
// the current attempt is honoured.
|
|
817
830
|
const local = localStorage.getItem(phaseStorageKey);
|
|
818
|
-
|
|
831
|
+
const attemptId = runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ != null ? runtimeGlobal.__SUPERCOV_MCDC_TEST_ID__ : testId;
|
|
832
|
+
if (local && phaseBelongsToAttempt(local, attemptId))
|
|
819
833
|
return local;
|
|
820
834
|
return void 0;
|
|
821
835
|
} catch (e) {
|
|
@@ -870,7 +884,10 @@ function requestCoverageContext(value) {
|
|
|
870
884
|
const encodedScope = (_a8 = headers.get(COVERAGE_SCOPE_HEADER)) != null ? _a8 : cookies.get(COVERAGE_SCOPE_COOKIE);
|
|
871
885
|
const rawPhaseId = (_b = headers.get(COVERAGE_PHASE_HEADER)) != null ? _b : cookies.get(COVERAGE_PHASE_COOKIE);
|
|
872
886
|
const scope = decodeCoverageScope(typeof encodedScope === "string" ? encodedScope : void 0);
|
|
873
|
-
|
|
887
|
+
// The phase cookie outlives a test in a shared browser context just like
|
|
888
|
+
// the stored phase does (see currentPhaseId); a request carrying a scope
|
|
889
|
+
// and a phase from different attempts keeps the scope and drops the phase.
|
|
890
|
+
const phaseId = typeof rawPhaseId === "string" && rawPhaseId.length > 0 && (!scope || phaseBelongsToAttempt(rawPhaseId, scope.attemptId)) ? rawPhaseId : void 0;
|
|
874
891
|
return __spreadValues(__spreadValues({}, scope ? { scope } : {}), phaseId ? { phaseId } : {});
|
|
875
892
|
}
|
|
876
893
|
function withRequestPhase(handler) {
|
|
@@ -1186,6 +1203,7 @@ const directRuntimeApi = {
|
|
|
1186
1203
|
optionalCallReached,
|
|
1187
1204
|
optionalSelect,
|
|
1188
1205
|
parenthesizedAssignmentValue,
|
|
1206
|
+
phaseBelongsToAttempt,
|
|
1189
1207
|
registerProbeV2,
|
|
1190
1208
|
resetCoverage,
|
|
1191
1209
|
selectionBegin,
|
|
@@ -1233,6 +1251,7 @@ export {
|
|
|
1233
1251
|
optionalCallReached,
|
|
1234
1252
|
optionalSelect,
|
|
1235
1253
|
parenthesizedAssignmentValue,
|
|
1254
|
+
phaseBelongsToAttempt,
|
|
1236
1255
|
registerProbeV2,
|
|
1237
1256
|
resetCoverage,
|
|
1238
1257
|
selectionBegin,
|