supercov 0.0.31 → 0.0.32
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/supported-suites.md
CHANGED
|
@@ -67,6 +67,14 @@ Playwright support includes Chromium, Firefox, and WebKit, along with pages,
|
|
|
67
67
|
frames, popups, workers, request contexts, WebSockets, and test-launched child
|
|
68
68
|
processes where the runner exposes their identity.
|
|
69
69
|
|
|
70
|
+
Browsers a suite launches itself are covered too. A fixture that calls
|
|
71
|
+
`chromium.launchPersistentContext`, or `launch`/`connect` and hands out its own
|
|
72
|
+
contexts and pages in place of Playwright's `page` fixture, is adopted by each
|
|
73
|
+
test's collector: its pages are read before the fixture closes them, and a
|
|
74
|
+
context kept for the whole worker follows the current test's identity. Actions
|
|
75
|
+
on such pages are not recorded as separate phases, so their evidence is
|
|
76
|
+
attributed to the test and its assertions rather than to individual clicks.
|
|
77
|
+
|
|
70
78
|
Node child processes inherit coverage automatically. Long-running servers get
|
|
71
79
|
a short drain window after the test command finishes so buffered evidence can
|
|
72
80
|
arrive. Work without a reliable test identity is kept as background coverage
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supercov",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
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",
|
|
@@ -70,12 +70,12 @@
|
|
|
70
70
|
"test:python-monitoring": "cargo build -p supercov && node scripts/python-monitoring-integration.mjs"
|
|
71
71
|
},
|
|
72
72
|
"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.
|
|
73
|
+
"@supercov/cli-darwin-arm64": "0.0.32",
|
|
74
|
+
"@supercov/cli-darwin-x64": "0.0.32",
|
|
75
|
+
"@supercov/cli-linux-arm64-gnu": "0.0.32",
|
|
76
|
+
"@supercov/cli-linux-arm64-musl": "0.0.32",
|
|
77
|
+
"@supercov/cli-linux-x64-gnu": "0.0.32",
|
|
78
|
+
"@supercov/cli-linux-x64-musl": "0.0.32"
|
|
79
79
|
},
|
|
80
80
|
"peerDependencies": {
|
|
81
81
|
"@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,
|