supercov 4.24.7 → 4.25.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.
@@ -18,7 +18,8 @@ import * as standardPlaywright from "@playwright/test";
18
18
  import * as coverageRuntime from "./runtime.mjs";
19
19
  import { inferTestProvenance, testTags } from "./provenance.mjs";
20
20
  import { appendJsonLineDurableSync, appendJsonLineSync } from "./atomic.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";
21
+ import { expandSnapshot, mergeSnapshots } from "./snapshots.mjs";
22
+ import { COVERAGE_DOCUMENT_PHASE_COOKIE, COVERAGE_DOCUMENT_PHASE_SITE_COOKIE, 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, startDocumentInPhase, } from "./transport.mjs";
22
23
  export * from "@playwright/test";
23
24
  const generatedTargetModule = "__SUPERCOV_PLAYWRIGHT_MODULE__";
24
25
  const targetModule = process.env["SUPERCOV_PLAYWRIGHT_MODULE"] ??
@@ -51,6 +52,33 @@ const browserRuntimePrelude = `(() => { if (globalThis.__SUPERCOV_DIRECT_RUNTIME
51
52
  const WORKER_ACTIVATION_WAIT_MS = 250;
52
53
  const WORKER_SNAPSHOT_WAIT_MS = 5000;
53
54
  const WORKER_TIMED_OUT = Symbol("worker did not answer");
55
+ /**
56
+ * Supercov's own Playwright calls, made as one internal API call. Each call a
57
+ * suite makes captures its stack, maps every frame through the runner's
58
+ * source maps, and reports a step and a trace event: the collector's calls at
59
+ * every action did all of that, and on a real suite they were most of what an
60
+ * action waited for, and showed up among the test's own steps. Inside one
61
+ * internal call the rest find its zone: they capture and report nothing. The
62
+ * one stack it captures names nothing anyone reads, so it is not formatted.
63
+ */
64
+ const unformattedStack = () => "";
65
+ function internally(owner, work) {
66
+ const wrapApiCall = owner?._wrapApiCall;
67
+ if (typeof wrapApiCall !== "function")
68
+ return work();
69
+ const prepareStackTrace = Error.prepareStackTrace;
70
+ Error.prepareStackTrace = unformattedStack;
71
+ try {
72
+ // `{ internal: true }`, or `true` where older releases take a flag.
73
+ return wrapApiCall.call(owner, () => {
74
+ Error.prepareStackTrace = prepareStackTrace;
75
+ return work();
76
+ }, { internal: true });
77
+ }
78
+ finally {
79
+ Error.prepareStackTrace = prepareStackTrace;
80
+ }
81
+ }
54
82
  function bounded(promise, milliseconds) {
55
83
  let timer;
56
84
  return Promise.race([
@@ -197,6 +225,7 @@ class CoveragePhaseController {
197
225
  cdpSessions = new Map();
198
226
  newDocumentScriptIds = new Map();
199
227
  pendingRegistrations = new Set();
228
+ pendingAssertion;
200
229
  pageRegistrations = new WeakMap();
201
230
  scriptUpdate = Promise.resolve();
202
231
  proxyCache = new WeakMap();
@@ -257,12 +286,22 @@ class CoveragePhaseController {
257
286
  allPages() {
258
287
  return [...this.pages];
259
288
  }
289
+ /** Any Playwright object of this test's, to make internal calls through. */
290
+ anyOwner() {
291
+ for (const owners of [this.contexts, this.pages, this.workers])
292
+ for (const owner of owners)
293
+ return owner;
294
+ return undefined;
295
+ }
260
296
  allWorkers() {
261
297
  return [...this.workers];
262
298
  }
263
299
  async collectRuntimeSnapshots() {
264
300
  if (this.runtimeSnapshots)
265
301
  return this.runtimeSnapshots;
302
+ return internally(this.anyOwner() ?? [...liveTrackedContexts()][0], () => this.collectRuntimeSnapshotsNow());
303
+ }
304
+ async collectRuntimeSnapshotsNow() {
266
305
  timingCount("collectRuntimeSnapshots.cold");
267
306
  // Contexts the suite created without going through any fixture the
268
307
  // collector wraps (a raw `browser.newContext()` mid-test) are found
@@ -369,6 +408,9 @@ class CoveragePhaseController {
369
408
  async snapshotBeforeClose(page) {
370
409
  if (!this.pages.has(page) || this.snapshottedPages.has(page) || this.runtimeSnapshots)
371
410
  return;
411
+ return internally(page, () => this.snapshotBeforeCloseNow(page));
412
+ }
413
+ async snapshotBeforeCloseNow(page) {
372
414
  this.snapshottedPages.add(page);
373
415
  this.earlySnapshots.push(...(await this.snapshotPage(page)));
374
416
  // A dedicated worker ends with the page that owns it, so it is read
@@ -393,7 +435,7 @@ class CoveragePhaseController {
393
435
  for (const context of liveTrackedContexts()) {
394
436
  if (!this.contexts.has(context)) {
395
437
  const headersKnown = trackedContextConfiguredHeaders.has(context);
396
- await this.registerContext(context, { adopted: !headersKnown }).catch(() => undefined);
438
+ await internally(context, () => this.registerContext(context, { adopted: !headersKnown })).catch(() => undefined);
397
439
  }
398
440
  }
399
441
  }
@@ -407,8 +449,19 @@ class CoveragePhaseController {
407
449
  * already has one of this test's phases is left alone: a reset that
408
450
  * arrives late must not drop what this test ran.
409
451
  */
410
- async takeCarriedEvidence() {
411
- await Promise.all([...this.pendingRegistrations]);
452
+ /**
453
+ * `pages`, when given, are the pages the test starts with, read from the
454
+ * live contexts while those are being registered: what they carry is in
455
+ * their documents, which registering a context does not touch, so the
456
+ * test's setup need not wait for one before the other.
457
+ */
458
+ async takeCarriedEvidence(pages) {
459
+ if (pages === undefined)
460
+ await Promise.all([...this.pendingRegistrations]);
461
+ const targets = pages ?? this.allPages();
462
+ return internally(targets[0] ?? this.anyOwner(), () => this.takeCarriedEvidenceNow(targets));
463
+ }
464
+ async takeCarriedEvidenceNow(pages) {
412
465
  const prefix = `${this.scope.attemptId}:`;
413
466
  const take = (target) => bounded(target
414
467
  .evaluate((prefix) => {
@@ -416,16 +469,17 @@ class CoveragePhaseController {
416
469
  if (String(coverageGlobal.__SUPERCOV_PHASE_ID__ ?? "").startsWith(prefix))
417
470
  return undefined;
418
471
  const getSnapshot = coverageGlobal.__SUPERCOV_COVERAGE_SNAPSHOT__;
472
+ const compact = coverageGlobal.__SUPERCOV_COMPACT_SNAPSHOT__;
419
473
  const reset = coverageGlobal.__SUPERCOV_RESET__;
420
474
  if (!getSnapshot || !reset)
421
475
  return undefined;
422
- const snapshot = JSON.stringify(getSnapshot());
476
+ const snapshot = typeof compact === "function" ? compact(getSnapshot()) : JSON.stringify(getSnapshot());
423
477
  reset();
424
478
  return snapshot;
425
479
  }, prefix)
426
- .then((text) => (typeof text === "string" ? JSON.parse(text) : undefined))
480
+ .then((text) => (typeof text === "string" ? expandSnapshot(JSON.parse(text)) : undefined))
427
481
  .catch(() => undefined), WORKER_ACTIVATION_WAIT_MS);
428
- const taken = await Promise.all(this.allPages().flatMap((page) => [
482
+ const taken = await Promise.all(pages.flatMap((page) => [
429
483
  ...page.frames().map(take),
430
484
  ...page.workers().map(take),
431
485
  ]));
@@ -447,7 +501,7 @@ class CoveragePhaseController {
447
501
  return Promise.resolve();
448
502
  let registration = this.pageRegistrations.get(page);
449
503
  if (!registration) {
450
- registration = this.registerNewPage(page);
504
+ registration = internally(page, () => this.registerNewPage(page));
451
505
  this.pageRegistrations.set(page, registration);
452
506
  }
453
507
  return registration;
@@ -463,7 +517,15 @@ class CoveragePhaseController {
463
517
  await this.registerContext(context, {
464
518
  adopted: !this.contexts.has(context) && tracked && !headersKnown,
465
519
  });
466
- const cdp = await page.context().newCDPSession(page).catch(() => undefined);
520
+ // A page of a context whose headers are not known learns each phase
521
+ // from a new-document script replaced through a session of its own
522
+ // (activatePage). Every other page needs none: a session with the
523
+ // Page domain on has the browser send it every event of the page a
524
+ // second time, and the browser's own process spent more on that than
525
+ // the renderers spent running the instrumented app.
526
+ const cdp = this.adoptedContexts.has(context)
527
+ ? await page.context().newCDPSession(page).catch(() => undefined)
528
+ : undefined;
467
529
  if (cdp) {
468
530
  // Chromium runs a session's new-document scripts only once the
469
531
  // session has enabled the Page domain. Without it the script that
@@ -471,25 +533,13 @@ class CoveragePhaseController {
471
533
  // learned its phase only from the next action, after it had
472
534
  // loaded, and everything it ran while loading was placed by time.
473
535
  await cdp.send("Page.enable").catch(() => undefined);
474
- // A document that leaves hands its evidence over here: the
475
- // next one reads what it stored only on the same origin.
476
- cdp.on("Runtime.bindingCalled", ({ name, payload }) => {
477
- if (name !== LEAVING_BINDING || this.runtimeSnapshots)
478
- return;
479
- try {
480
- this.leftSnapshots.push(JSON.parse(payload));
481
- }
482
- catch {
483
- // Not evidence.
484
- }
485
- });
486
- await cdp.send("Runtime.enable").catch(() => undefined);
487
- await cdp.send("Runtime.addBinding", { name: LEAVING_BINDING }).catch(() => undefined);
488
536
  this.cdpSessions.set(page, cdp);
489
537
  }
490
538
  const phaseId = this.requestPhaseId();
491
- if (phaseId)
539
+ if (phaseId && this.adoptedContexts.has(context))
492
540
  await this.activatePage(page, phaseId);
541
+ else if (phaseId)
542
+ await this.activateFrames(phaseId, [page]);
493
543
  page.on("worker", (worker) => {
494
544
  void this.registerWorker(worker);
495
545
  });
@@ -503,11 +553,18 @@ class CoveragePhaseController {
503
553
  patchContextPrototype(context);
504
554
  if (adopted)
505
555
  this.adoptedContexts.add(context);
506
- // A Rust crate compiled to wasm the run measures: pages remember
507
- // their wasm instances, so each test's work can be taken from them.
508
- if (process.env.SUPERCOV_WASM_REGIONS === "1")
509
- await context.addInitScript({ content: "globalThis.__SUPERCOV_WASM_REGIONS__ = true;" }).catch(() => undefined);
510
- await context.addInitScript({ content: browserRuntimePrelude }).catch(() => undefined);
556
+ return internally(context, () => this.registerContextNow(context, adopted));
557
+ }
558
+ async registerContextNow(context, adopted) {
559
+ // The context's own scripts first: sent when it was made, so they
560
+ // are in place, or sent now for a context made some other way.
561
+ await prepareContext(context);
562
+ // Then what every new document of the context runs for this test, in
563
+ // this order, as one script: one call where several, each awaited,
564
+ // were most of what a test waited for before its page opened. Each
565
+ // part runs on its own, as separate scripts did, so one that throws
566
+ // leaves the others.
567
+ const parts = [];
511
568
  // A context that outlives its test (a worker-scoped browser fixture)
512
569
  // is registered again by every later test, so this script accumulates
513
570
  // on it and each new document runs every copy in registration order.
@@ -515,55 +572,25 @@ class CoveragePhaseController {
515
572
  // own attempt, so the newest wins; the fetch patch is installed once
516
573
  // and reads the attempt at call time, so a stale copy can never pin an
517
574
  // old scope onto a request the way nested wrappers would.
518
- await context.addInitScript(({ attemptId, scopeHeader, scopeValue, scopeCookie, scopeSiteCookie }) => {
519
- const coverageGlobal = globalThis;
520
- coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
521
- coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
522
- try {
523
- document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
524
- // And for this document's frames on another of the app's
525
- // hosts, where crossing sites is allowed.
526
- if (globalThis.isSecureContext)
527
- document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
528
- if (!coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__) {
529
- const originalFetch = globalThis.fetch?.bind(globalThis);
530
- if (originalFetch) {
531
- coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__ = true;
532
- globalThis.fetch = ((input, init) => {
533
- // Only the page's own origin hears the scope. A
534
- // header on a request to another origin makes the
535
- // browser ask that origin first, and one that
536
- // allows only the headers it uses refuses: the
537
- // page's fetch failed under measurement alone.
538
- try {
539
- const target = new URL(input instanceof Request ? input.url : String(input), location.href);
540
- if (target.origin !== location.origin)
541
- return originalFetch(input, init);
542
- }
543
- catch {
544
- return originalFetch(input, init);
545
- }
546
- const headers = new Headers(init?.headers ??
547
- (input instanceof Request ? input.headers : undefined));
548
- headers.set(scopeHeader, coverageGlobal.__SUPERCOV_ATTEMPT__?.scopeValue ?? scopeValue);
549
- const phase = coverageGlobal.__SUPERCOV_PHASE_ID__;
550
- if (phase)
551
- headers.set("x-supercov-phase", phase);
552
- return originalFetch(input, { ...init, headers });
553
- });
554
- }
555
- }
556
- }
557
- catch {
558
- // Browser instrumentation must not change application behavior.
559
- }
560
- }, {
575
+ parts.push(`(${announceAttempt})(${JSON.stringify({
561
576
  attemptId: this.scope.attemptId,
562
577
  scopeHeader: COVERAGE_SCOPE_HEADER,
563
578
  scopeValue: encodeCoverageScope(this.scope),
564
579
  scopeCookie: COVERAGE_SCOPE_COOKIE,
565
580
  scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
566
- }).catch(() => undefined);
581
+ })});`);
582
+ // A new document starts in the last action's phase, from the cookie
583
+ // each action sets: one script per context, where a script replaced on
584
+ // every page at every phase took two Playwright calls each time.
585
+ parts.push(`(${startDocumentInPhase})(${JSON.stringify({
586
+ attemptId: this.scope.attemptId,
587
+ phaseCookie: COVERAGE_DOCUMENT_PHASE_COOKIE,
588
+ phaseSiteCookie: COVERAGE_DOCUMENT_PHASE_SITE_COOKIE,
589
+ })});`);
590
+ await Promise.all([
591
+ context.addInitScript({ content: separately(parts) }).catch(() => undefined),
592
+ adopted ? undefined : this.updateContextHeaders(context, this.requestPhaseId(), this.lastActionId),
593
+ ]);
567
594
  const register = (page) => {
568
595
  const pending = this.registerPage(page).finally(() => this.pendingRegistrations.delete(pending));
569
596
  this.pendingRegistrations.add(pending);
@@ -576,8 +603,6 @@ class CoveragePhaseController {
576
603
  this.contextListeners.set(context, [["page", register], ["serviceworker", registerServiceWorker]]);
577
604
  for (const worker of context.serviceWorkers())
578
605
  void this.registerWorker(worker);
579
- if (!adopted)
580
- await this.updateContextHeaders(context, this.requestPhaseId());
581
606
  for (const page of context.pages())
582
607
  register(page);
583
608
  }
@@ -585,6 +610,9 @@ class CoveragePhaseController {
585
610
  if (this.workers.has(worker))
586
611
  return;
587
612
  this.workers.add(worker);
613
+ return internally(worker, () => this.registerWorkerNow(worker));
614
+ }
615
+ async registerWorkerNow(worker) {
588
616
  const phaseId = this.requestPhaseId();
589
617
  const registration = await worker
590
618
  .evaluate(({ attemptId, scopeHeader, scopeValue, phaseHeader, phase }) => {
@@ -636,12 +664,21 @@ class CoveragePhaseController {
636
664
  const phase = this.createPhase("action", operation);
637
665
  this.lastActionId = phase.id;
638
666
  this.activePhaseId = phase.id;
639
- await timed("action.activateInBrowser", () => this.activateInBrowser(phase.id));
667
+ await timed("action.activateInBrowser", () => this.activateInBrowser(phase.id, phase.id));
640
668
  return phase;
641
669
  }
642
- beginAssertion(operation, source = callerSource()) {
670
+ beginAssertion(operation, source = callerSource(), reachesBrowser = true) {
643
671
  timingCount("beginAssertion");
644
672
  const phase = this.createPhase("assertion", operation, this.lastActionId, source);
673
+ // An assertion that is over before any message could reach the
674
+ // browser (`expect(value).toBe(1)`) gets no browser phase: telling the
675
+ // page and the app's cookies afterwards labelled what the page did
676
+ // next, the last action's work, as this assertion's, and queued two
677
+ // calls ahead of the test's next action.
678
+ if (!reachesBrowser) {
679
+ timingCount("assertion.offBrowser");
680
+ return phase;
681
+ }
645
682
  this.activePhaseId = phase.id;
646
683
  // Playwright queues browser protocol commands in order. Starting this
647
684
  // evaluation before an async locator assertion is sufficient to tag its
@@ -649,12 +686,37 @@ class CoveragePhaseController {
649
686
  void timed("assertion.activateInBrowser", () => this.activateInBrowser(phase.id));
650
687
  return phase;
651
688
  }
689
+ /**
690
+ * An instrumented assertion's phase, opened around its arguments as well
691
+ * as its matcher, which reaches the browser just before the first thing
692
+ * in it that can: a call on one of the test's Playwright objects, or a
693
+ * matcher that waits (promotePendingAssertion). Playwright sends calls in
694
+ * order, so the page has the phase before anything the assertion asks of
695
+ * it. One that asks nothing of the page sends nothing.
696
+ */
697
+ beginBridgedAssertion(operation, source) {
698
+ const phase = this.beginAssertion(operation, source, false);
699
+ this.pendingAssertion = phase;
700
+ return phase;
701
+ }
702
+ promotePendingAssertion() {
703
+ const phase = this.pendingAssertion;
704
+ if (!phase)
705
+ return;
706
+ this.pendingAssertion = undefined;
707
+ timingCount("assertion.promoted");
708
+ this.activePhaseId = phase.id;
709
+ void timed("assertion.activateInBrowser", () => this.activateInBrowser(phase.id));
710
+ }
652
711
  requestPhaseId() {
653
712
  return this.activePhaseId;
654
713
  }
655
714
  async dispose() {
656
715
  this.disposed = true;
657
716
  await Promise.all([...this.pendingRegistrations]);
717
+ return internally(this.anyOwner(), () => this.disposeNow());
718
+ }
719
+ async disposeNow() {
658
720
  await this.scriptUpdate;
659
721
  for (const [context, listeners] of this.contextListeners) {
660
722
  for (const [event, listener] of listeners)
@@ -662,6 +724,11 @@ class CoveragePhaseController {
662
724
  }
663
725
  this.contextListeners.clear();
664
726
  for (const [page, cdp] of this.cdpSessions) {
727
+ // A closed page took its session with it: detaching it failed for
728
+ // every page a test closed, each failure a Playwright error with a
729
+ // stack, and was the only collector call on a slice that failed.
730
+ if (page.isClosed?.())
731
+ continue;
665
732
  const identifier = this.newDocumentScriptIds.get(page);
666
733
  if (identifier)
667
734
  await cdp
@@ -673,6 +740,8 @@ class CoveragePhaseController {
673
740
  }
674
741
  }
675
742
  finish(phase, error) {
743
+ if (this.pendingAssertion === phase)
744
+ this.pendingAssertion = undefined;
676
745
  phase.endedAtMs = Date.now();
677
746
  phase.status = error === undefined ? "passed" : "failed";
678
747
  if (error !== undefined)
@@ -695,6 +764,7 @@ class CoveragePhaseController {
695
764
  return value;
696
765
  const method = String(property);
697
766
  return (...args) => {
767
+ this.promotePendingAssertion();
698
768
  const isRequest = REQUEST_METHODS.has(method) &&
699
769
  isApiRequestContext(object);
700
770
  if (ACTION_METHODS.has(method) || isRequest) {
@@ -818,19 +888,33 @@ class CoveragePhaseController {
818
888
  * the 43 extra seconds, with the database worker's bounded wait the
819
889
  * largest part).
820
890
  */
821
- async activateInBrowser(phaseId) {
891
+ activateInBrowser(phaseId, documentPhaseId) {
892
+ return internally(this.anyOwner(), () => this.activateInBrowserNow(phaseId, documentPhaseId));
893
+ }
894
+ async activateInBrowserNow(phaseId, documentPhaseId) {
822
895
  await Promise.all([
823
896
  timed("activate.contextHeaders", () => Promise.all([...this.contexts]
824
897
  .filter((context) => !this.adoptedContexts.has(context))
825
- .map((context) => this.updateContextHeaders(context, phaseId)))),
898
+ .map((context) => this.updateContextHeaders(context, phaseId, documentPhaseId)))),
826
899
  this.activateFrames(phaseId),
827
900
  this.activateWorkers(phaseId),
828
- timed("activate.pageScript", () => Promise.all([...this.pages].map((page) => this.activatePage(page, phaseId)))),
901
+ // A context whose headers are not known gets no cookies, so its
902
+ // pages' new documents learn the phase from a script replaced here.
903
+ timed("activate.pageScript", () => Promise.all([...this.pages]
904
+ .filter((page) => this.adoptedContexts.has(page.context()))
905
+ .map((page) => this.activatePage(page, phaseId)))),
829
906
  ]);
830
907
  }
831
- async activateFrames(phaseId) {
832
- await timed("activate.frameEvaluate", () => Promise.all([...this.pages].flatMap((page) => page.frames()).map((frame) => frame
833
- .evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie, scopeSiteCookie, phaseSiteCookie }) => {
908
+ async activateFrames(phaseId, pages = this.pages) {
909
+ // The app's own hosts have the phase from the context's cookies, set
910
+ // beside this; a document there writing them again was a round trip
911
+ // to the cookie store per cookie, on every action. A context whose
912
+ // headers are not known gets none, so its documents still write them.
913
+ const covered = (page) => this.adoptedContexts.has(page.context())
914
+ ? []
915
+ : this.appHosts.map((url) => new URL(url).hostname);
916
+ await timed("activate.frameEvaluate", () => Promise.all([...pages].flatMap((page) => page.frames().map((frame) => [frame, covered(page)])).map(([frame, coveredHosts]) => frame
917
+ .evaluate(({ id, attemptId, storageKey, scopeCookie, scopeValue, phaseCookie, scopeSiteCookie, phaseSiteCookie, coveredHosts }) => {
834
918
  globalThis.__SUPERCOV_PHASE_ID__ = id;
835
919
  const coverageGlobal = globalThis;
836
920
  // A document that loaded under an earlier test's attempt (a page
@@ -840,6 +924,8 @@ class CoveragePhaseController {
840
924
  coverageGlobal.__SUPERCOV_ACTIVATE_PROBE_CONTEXT__?.(attemptId, id);
841
925
  try {
842
926
  localStorage.setItem(storageKey, id);
927
+ if (coveredHosts.includes(location.hostname))
928
+ return;
843
929
  document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
844
930
  document.cookie = `${phaseCookie}=${encodeURIComponent(id)}; Path=/; SameSite=Lax`;
845
931
  if (globalThis.isSecureContext) {
@@ -859,8 +945,9 @@ class CoveragePhaseController {
859
945
  phaseCookie: COVERAGE_PHASE_COOKIE,
860
946
  scopeSiteCookie: COVERAGE_SCOPE_SITE_COOKIE,
861
947
  phaseSiteCookie: COVERAGE_PHASE_SITE_COOKIE,
948
+ coveredHosts,
862
949
  })
863
- .catch(() => undefined))));
950
+ .catch(() => timingCount("activate.frameRefused")))));
864
951
  }
865
952
  async activateWorkers(phaseId) {
866
953
  await timed("activate.workerEvaluate", () => Promise.all([...this.workers].map(async (worker) => {
@@ -891,32 +978,49 @@ class CoveragePhaseController {
891
978
  * site from the first navigation on, and, on a secure origin, to frames
892
979
  * of the app's other hosts as well.
893
980
  */
894
- async updateContextHeaders(context, phaseId) {
981
+ async updateContextHeaders(context, phaseId, documentPhaseId) {
895
982
  const scope = encodeURIComponent(encodeCoverageScope(this.scope));
896
983
  const phase = phaseId ? encodeURIComponent(phaseId) : undefined;
897
- const pairs = (scopeName, phaseName) => [
984
+ const documentPhase = documentPhaseId ? encodeURIComponent(documentPhaseId) : undefined;
985
+ const pairs = (scopeName, phaseName, documentName) => [
898
986
  [scopeName, scope],
899
987
  ...(phase ? [[phaseName, phase]] : []),
988
+ ...(documentPhase ? [[documentName, documentPhase]] : []),
900
989
  ];
901
990
  // The context's own headers are left alone: they are the suite's, and
902
991
  // a test that sets more with `setExtraHTTPHeaders` keeps them. Setting
903
992
  // them back at every phase dropped a test's `X-Forwarded-For`.
904
- await Promise.all([
905
- ...this.appHosts.map((url) => {
906
- const host = new URL(url);
907
- // For the app's own site: requests, navigations and sockets.
908
- const cookies = pairs(COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE).map(([name, value]) => ({ name, value, url, sameSite: "Lax" }));
909
- // For its frames on another of its hosts: a cookie that
910
- // crosses sites must be Secure, which needs https or a local
911
- // host. Chromium keeps such a cookie only when it is given by
912
- // domain; given by an http URL it is dropped without a word.
913
- if (host.protocol === "https:" || ["localhost", "127.0.0.1"].includes(host.hostname)) {
914
- cookies.push(...pairs(COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE).map(([name, value]) => ({ name, value, domain: host.hostname, path: "/", sameSite: "None", secure: true })));
915
- }
916
- // One host refused leaves the others theirs.
917
- return context.addCookies(cookies).catch(() => undefined);
918
- }),
919
- ]);
993
+ const byHost = this.appHosts.map((url) => {
994
+ const host = new URL(url);
995
+ // For the app's own site: requests, navigations and sockets.
996
+ const cookies = pairs(COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE, COVERAGE_DOCUMENT_PHASE_COOKIE).map(([name, value]) => ({ name, value, url, sameSite: "Lax" }));
997
+ // For its frames on another of its hosts: a cookie that
998
+ // crosses sites must be Secure, which needs https or a local
999
+ // host. Chromium keeps such a cookie only when it is given by
1000
+ // domain; given by an http URL it is dropped without a word.
1001
+ if (host.protocol === "https:" || ["localhost", "127.0.0.1"].includes(host.hostname)) {
1002
+ cookies.push(...pairs(COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE, COVERAGE_DOCUMENT_PHASE_SITE_COOKIE).map(([name, value]) => ({ name, value, domain: host.hostname, path: "/", sameSite: "None", secure: true })));
1003
+ }
1004
+ return cookies;
1005
+ });
1006
+ // Every host's cookies in one call: a call per host, each its own
1007
+ // round trip through Playwright, was most of what an action waited
1008
+ // for. A cookie is kept per host, for every port of it: hosts that
1009
+ // differ only by port gave the same cookies twice, and each goes once.
1010
+ // A cookie the browser refuses fails the whole call, so then each host
1011
+ // is given its own and one refused leaves the others theirs.
1012
+ const distinct = new Map();
1013
+ for (const cookie of byHost.flat()) {
1014
+ const target = cookie.url ? new URL(cookie.url) : undefined;
1015
+ const where = target ? `${target.protocol}//${target.hostname}${target.pathname}` : `${cookie.domain}${cookie.path}`;
1016
+ const key = JSON.stringify([cookie.name, cookie.value, where, cookie.sameSite, cookie.secure ?? false]);
1017
+ if (!distinct.has(key))
1018
+ distinct.set(key, cookie);
1019
+ }
1020
+ await context.addCookies([...distinct.values()]).catch(() => {
1021
+ timingCount("activate.cookiesRefused");
1022
+ return Promise.all(byHost.map((cookies) => context.addCookies(cookies).catch(() => timingCount("activate.hostCookiesRefused"))));
1023
+ });
920
1024
  }
921
1025
  async activatePage(page, phaseId) {
922
1026
  const cdp = this.cdpSessions.get(page);
@@ -948,55 +1052,114 @@ class CoveragePhaseController {
948
1052
  * the end of every test (20,000 events: 470 ms, against 34 ms as one JSON
949
1053
  * string), and doubled a 76-test suite's time.
950
1054
  */
951
- const LEAVING_BINDING = "__supercovLeaving";
952
1055
  /**
953
- * One snapshot holding everything in `snapshots`, each observation once: a
954
- * document that left gave its evidence, and the next one on its origin
955
- * restored the same evidence and added to it.
1056
+ * The attempt a new document belongs to, for the app's server: its cookies,
1057
+ * and the header its fetch sends to its own origin.
956
1058
  */
957
- function mergeSnapshots(snapshots) {
958
- const hits = new Set();
959
- const events = new Map();
960
- const decisions = new Map();
961
- const logicals = new Map();
962
- const vectorsOf = (map, key, initial) => {
963
- let entry = map.get(key);
964
- if (!entry) {
965
- entry = { ...initial, seen: new Map() };
966
- map.set(key, entry);
967
- }
968
- return entry;
969
- };
970
- for (const snapshot of snapshots) {
971
- for (const id of snapshot?.hits ?? [])
972
- hits.add(id);
973
- for (const event of snapshot?.events ?? [])
974
- events.set(JSON.stringify(event), event);
975
- for (const decision of snapshot?.decisions ?? []) {
976
- const entry = vectorsOf(decisions, decision.id, { id: decision.id });
977
- for (const vector of decision.vectors ?? [])
978
- entry.seen.set(JSON.stringify(vector), vector);
979
- }
980
- for (const logical of snapshot?.logicals ?? []) {
981
- const entry = vectorsOf(logicals, logical.id, { id: logical.id });
982
- for (const vector of logical.vectors ?? [])
983
- entry.seen.set(JSON.stringify(vector), vector);
1059
+ function announceAttempt({ attemptId, scopeHeader, scopeValue, scopeCookie, scopeSiteCookie }) {
1060
+ const coverageGlobal = globalThis;
1061
+ coverageGlobal.__SUPERCOV_ATTEMPT__ = { attemptId, scopeValue };
1062
+ coverageGlobal.__SUPERCOV_MCDC_TEST_ID__ = attemptId;
1063
+ try {
1064
+ document.cookie = `${scopeCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=Lax`;
1065
+ // And for this document's frames on another of the app's
1066
+ // hosts, where crossing sites is allowed.
1067
+ if (globalThis.isSecureContext)
1068
+ document.cookie = `${scopeSiteCookie}=${encodeURIComponent(scopeValue)}; Path=/; SameSite=None; Secure`;
1069
+ if (!coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__) {
1070
+ const originalFetch = globalThis.fetch?.bind(globalThis);
1071
+ if (originalFetch) {
1072
+ coverageGlobal.__SUPERCOV_BROWSER_FETCH_PATCHED__ = true;
1073
+ globalThis.fetch = ((input, init) => {
1074
+ // Only the page's own origin hears the scope. A
1075
+ // header on a request to another origin makes the
1076
+ // browser ask that origin first, and one that
1077
+ // allows only the headers it uses refuses: the
1078
+ // page's fetch failed under measurement alone.
1079
+ try {
1080
+ const target = new URL(input instanceof Request ? input.url : String(input), location.href);
1081
+ if (target.origin !== location.origin)
1082
+ return originalFetch(input, init);
1083
+ }
1084
+ catch {
1085
+ return originalFetch(input, init);
1086
+ }
1087
+ const headers = new Headers(init?.headers ??
1088
+ (input instanceof Request ? input.headers : undefined));
1089
+ headers.set(scopeHeader, coverageGlobal.__SUPERCOV_ATTEMPT__?.scopeValue ?? scopeValue);
1090
+ const phase = coverageGlobal.__SUPERCOV_PHASE_ID__;
1091
+ if (phase)
1092
+ headers.set("x-supercov-phase", phase);
1093
+ return originalFetch(input, { ...init, headers });
1094
+ });
1095
+ }
984
1096
  }
985
1097
  }
986
- return {
987
- decisions: [...decisions.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
988
- hits: [...hits],
989
- events: [...events.values()],
990
- logicals: [...logicals.values()].map(({ seen, id }) => ({ id, vectors: [...seen.values()] })),
991
- };
1098
+ catch {
1099
+ // Browser instrumentation must not change application behavior.
1100
+ }
1101
+ }
1102
+ const LEAVING_BINDING = "__supercovLeaving";
1103
+ /** Scripts as one, each part run on its own as separate scripts were. */
1104
+ function separately(parts) {
1105
+ return parts.map((part) => `try {\n${part}\n} catch (error) {}`).join("\n");
1106
+ }
1107
+ /** A document that leaves hands its evidence to the test that has its page. */
1108
+ function takeLeavingEvidence(source, payload) {
1109
+ const controller = source?.page ? controllerOwning(source.page) : undefined;
1110
+ if (!controller || controller.runtimeSnapshots)
1111
+ return;
1112
+ try {
1113
+ timingCount("transfer.leavingBytes", typeof payload === "string" ? payload.length : 0);
1114
+ controller.leftSnapshots.push(expandSnapshot(JSON.parse(payload)));
1115
+ }
1116
+ catch {
1117
+ // Not evidence.
1118
+ }
1119
+ }
1120
+ /**
1121
+ * What a context's documents run whichever test has them -- the runtime, and
1122
+ * the binding through which a leaving document hands its evidence over (the
1123
+ * next one reads what it stored only on the same origin) -- sent once, when
1124
+ * the context is made: Playwright opens the test's page meanwhile, and the
1125
+ * test's setup waits only for its own script and cookies. A context that
1126
+ * outlives its test keeps the one binding: the test that has the page now
1127
+ * takes what it hands over.
1128
+ */
1129
+ const preparedContexts = new WeakMap();
1130
+ function prepareContext(context) {
1131
+ let prepared = preparedContexts.get(context);
1132
+ if (!prepared) {
1133
+ const parts = [];
1134
+ // A Rust crate compiled to wasm the run measures: pages remember
1135
+ // their wasm instances, so each test's work can be taken from them.
1136
+ if (process.env.SUPERCOV_WASM_REGIONS === "1")
1137
+ parts.push("globalThis.__SUPERCOV_WASM_REGIONS__ = true;");
1138
+ parts.push(browserRuntimePrelude);
1139
+ prepared = Promise.resolve(internally(context, () => Promise.all([
1140
+ context.addInitScript({ content: separately(parts) }).catch(() => undefined),
1141
+ context.exposeBinding(LEAVING_BINDING, takeLeavingEvidence).catch(() => undefined),
1142
+ ]))).catch(() => undefined);
1143
+ preparedContexts.set(context, prepared);
1144
+ }
1145
+ return prepared;
992
1146
  }
993
1147
  function readSnapshot(target) {
994
1148
  return target
995
1149
  .evaluate(() => {
996
1150
  const getSnapshot = globalThis.__SUPERCOV_COVERAGE_SNAPSHOT__;
997
- return getSnapshot ? JSON.stringify(getSnapshot()) : undefined;
1151
+ const compact = globalThis.__SUPERCOV_COMPACT_SNAPSHOT__;
1152
+ if (!getSnapshot)
1153
+ return undefined;
1154
+ return typeof compact === "function" ? compact(getSnapshot()) : JSON.stringify(getSnapshot());
998
1155
  })
999
- .then((text) => (typeof text === "string" ? JSON.parse(text) : undefined));
1156
+ .then((text) => {
1157
+ if (typeof text !== "string")
1158
+ return undefined;
1159
+ // Phase timing counts the bytes each read carried, as its "ms".
1160
+ timingCount("transfer.snapshotBytes", text.length);
1161
+ return expandSnapshot(JSON.parse(text));
1162
+ });
1000
1163
  }
1001
1164
  let activeController;
1002
1165
  let bridgedAssertionDepth = 0;
@@ -1034,6 +1197,7 @@ function trackContext(context, configuredHeaders) {
1034
1197
  trackedContexts.add(context);
1035
1198
  context.once?.("close", () => trackedContexts.delete(context));
1036
1199
  patchContextPrototype(context);
1200
+ void prepareContext(context);
1037
1201
  }
1038
1202
  function liveTrackedContexts() {
1039
1203
  const contexts = new Set(trackedContexts);
@@ -1090,7 +1254,7 @@ function patchBrowserPrototype(browser) {
1090
1254
  * way the page is gone by the time the test's snapshots are collected.
1091
1255
  */
1092
1256
  /** The cookies Supercov gives a context to carry the test's scope. */
1093
- const OWN_COOKIES = new Set([COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE]);
1257
+ const OWN_COOKIES = new Set([COVERAGE_SCOPE_COOKIE, COVERAGE_PHASE_COOKIE, COVERAGE_DOCUMENT_PHASE_COOKIE, COVERAGE_SCOPE_SITE_COOKIE, COVERAGE_PHASE_SITE_COOKIE, COVERAGE_DOCUMENT_PHASE_SITE_COOKIE]);
1094
1258
  const suiteCookies = (cookies) => (Array.isArray(cookies) ? cookies.filter((cookie) => !OWN_COOKIES.has(cookie?.name)) : cookies);
1095
1259
  function patchContextPrototype(context) {
1096
1260
  // A test reads and saves the cookies it and its app set, never
@@ -1162,7 +1326,7 @@ globalThis.__SUPERCOV_ASSERTION_PHASE_BRIDGE__ = (operation, source, callback) =
1162
1326
  const runtime = directRuntime();
1163
1327
  if (!controller || !runtime)
1164
1328
  return { handled: false };
1165
- const phase = controller.beginAssertion(operation, source);
1329
+ const phase = controller.beginBridgedAssertion(operation, source);
1166
1330
  bridgedAssertionDepth += 1;
1167
1331
  try {
1168
1332
  const value = runtime.withCoverageCarrier({ version: 1, scope: controller.scope, phaseId: phase.id }, callback);
@@ -1194,7 +1358,7 @@ globalThis.__SUPERCOV_ASSERTION_PHASE_OPENER__ = (operation, source) => {
1194
1358
  const controller = activeController;
1195
1359
  if (!controller || !directRuntime())
1196
1360
  return undefined;
1197
- const phase = controller.beginAssertion(operation, source);
1361
+ const phase = controller.beginBridgedAssertion(operation, source);
1198
1362
  bridgedAssertionDepth += 1;
1199
1363
  let held = true;
1200
1364
  const release = () => {
@@ -1378,7 +1542,45 @@ function installChildProcessScopePropagation() {
1378
1542
  syncBuiltinESMExports();
1379
1543
  }
1380
1544
  installChildProcessScopePropagation();
1381
- function wrapMatchers(matchers, path = "expect") {
1545
+ // expect's own matchers that settle in the call, whatever they are given, on
1546
+ // a value that is no Playwright object: none of them can wait for the page.
1547
+ // Under `expect.poll`, `.resolves` or `.rejects` the same names wait.
1548
+ const SETTLED_MATCHERS = new Set([
1549
+ "toBe",
1550
+ "toBeCloseTo",
1551
+ "toBeDefined",
1552
+ "toBeFalsy",
1553
+ "toBeGreaterThan",
1554
+ "toBeGreaterThanOrEqual",
1555
+ "toBeInstanceOf",
1556
+ "toBeLessThan",
1557
+ "toBeLessThanOrEqual",
1558
+ "toBeNaN",
1559
+ "toBeNull",
1560
+ "toBeTruthy",
1561
+ "toBeUndefined",
1562
+ "toContain",
1563
+ "toContainEqual",
1564
+ "toEqual",
1565
+ "toHaveLength",
1566
+ "toHaveProperty",
1567
+ "toMatch",
1568
+ "toMatchObject",
1569
+ "toStrictEqual",
1570
+ ]);
1571
+ const PLAYWRIGHT_VALUE_TYPES = new Set(["APIResponse", "ElementHandle", "Frame", "FrameLocator", "JSHandle", "Locator", "Page"]);
1572
+ function isPlaywrightValue(value) {
1573
+ if (!value || (typeof value !== "object" && typeof value !== "function"))
1574
+ return false;
1575
+ try {
1576
+ return "_guid" in value || "_frame" in value || "_selector" in value ||
1577
+ PLAYWRIGHT_VALUE_TYPES.has(value.constructor?.name);
1578
+ }
1579
+ catch {
1580
+ return true;
1581
+ }
1582
+ }
1583
+ function wrapMatchers(matchers, path = "expect", settles = false) {
1382
1584
  return new Proxy(matchers, {
1383
1585
  get(target, property) {
1384
1586
  // Matcher objects and built-ins can expose getters whose internal slots
@@ -1389,14 +1591,19 @@ function wrapMatchers(matchers, path = "expect") {
1389
1591
  if (value &&
1390
1592
  typeof value === "object" &&
1391
1593
  (name === "not" || name === "resolves" || name === "rejects"))
1392
- return wrapMatchers(value, `${path}.${name}`);
1594
+ return wrapMatchers(value, `${path}.${name}`, settles && name === "not");
1393
1595
  if (typeof value !== "function")
1394
1596
  return value;
1395
1597
  return (...args) => {
1396
1598
  const controller = activeController;
1599
+ const settled = settles && SETTLED_MATCHERS.has(name);
1397
1600
  const phase = bridgedAssertionDepth === 0
1398
- ? controller?.beginAssertion(`${path}.${name}`)
1601
+ ? controller?.beginAssertion(`${path}.${name}`, undefined, !settled)
1399
1602
  : undefined;
1603
+ // Inside an instrumented assertion, a matcher that can wait
1604
+ // takes the assertion's phase to the browser before it starts.
1605
+ if (bridgedAssertionDepth > 0 && !settled)
1606
+ controller?.promotePendingAssertion();
1400
1607
  try {
1401
1608
  const result = Reflect.apply(value, target, args);
1402
1609
  if (result instanceof Promise) {
@@ -1423,21 +1630,21 @@ function wrapMatchers(matchers, path = "expect") {
1423
1630
  },
1424
1631
  });
1425
1632
  }
1426
- function wrapExpectCallable(callable) {
1633
+ function wrapExpectCallable(callable, polls = false) {
1427
1634
  return new Proxy(callable, {
1428
1635
  apply(target, thisArg, argumentsList) {
1429
1636
  const result = Reflect.apply(target, thisArg, argumentsList);
1430
1637
  if (typeof result === "function")
1431
- return wrapExpectCallable(result);
1638
+ return wrapExpectCallable(result, polls);
1432
1639
  if (result && typeof result === "object")
1433
- return wrapMatchers(result);
1640
+ return wrapMatchers(result, "expect", !polls && !isPlaywrightValue(argumentsList[0]));
1434
1641
  return result;
1435
1642
  },
1436
1643
  get(target, property, receiver) {
1437
1644
  const value = Reflect.get(target, property, receiver);
1438
1645
  if (typeof value !== "function")
1439
1646
  return value;
1440
- return wrapExpectCallable(value.bind(target));
1647
+ return wrapExpectCallable(value.bind(target), polls || property === "poll");
1441
1648
  },
1442
1649
  });
1443
1650
  }
@@ -1467,26 +1674,6 @@ function executionScope(testInfo) {
1467
1674
  attemptId,
1468
1675
  };
1469
1676
  }
1470
- function readServerRecords(scope) {
1471
- try {
1472
- return readFileSync(serverEvidencePath(scope), "utf8")
1473
- .split("\n")
1474
- .filter(Boolean)
1475
- .flatMap((line) => {
1476
- try {
1477
- const record = JSON.parse(line);
1478
- return record.scope?.attemptId === scope.attemptId ? [record] : [];
1479
- }
1480
- catch {
1481
- // Ignore a final partial line if the server was writing during collection.
1482
- return [];
1483
- }
1484
- });
1485
- }
1486
- catch {
1487
- return [];
1488
- }
1489
- }
1490
1677
  function phaseSourceLine(source) {
1491
1678
  return source?.replace(/:\d+$/, "");
1492
1679
  }
@@ -1523,6 +1710,48 @@ function mergeCollectedPhases(controllerPhases, fallbackPhases) {
1523
1710
  * written to one uniquely named, one-shot evidence file in the runner's shared
1524
1711
  * directory as well. No test streams into this path.
1525
1712
  */
1713
+ /**
1714
+ * A snapshot's events as columns.
1715
+ * A page records each hit once per test as an event of five fields, and its
1716
+ * thousands of them were most of a test's record. The engine decodes the
1717
+ * columns into exactly these events, in this order.
1718
+ */
1719
+ function columnarEvents(snapshot) {
1720
+ const events = snapshot?.events;
1721
+ if (!Array.isArray(events) || events.length === 0)
1722
+ return snapshot;
1723
+ const tables = { types: new Map(), phases: new Map(), environments: new Map() };
1724
+ const index = (table, value) => {
1725
+ let found = table.get(value);
1726
+ if (found === undefined) {
1727
+ found = table.size;
1728
+ table.set(value, found);
1729
+ }
1730
+ return found;
1731
+ };
1732
+ const columns = { id: [], type: [], phase: [], environment: [], t: [], vector: {} };
1733
+ let previous = 0;
1734
+ events.forEach((event, position) => {
1735
+ columns.id.push(event.id);
1736
+ columns.type.push(index(tables.types, event.type));
1737
+ columns.phase.push(event.phaseId == null ? -1 : index(tables.phases, event.phaseId));
1738
+ columns.environment.push(index(tables.environments, event.environment));
1739
+ columns.t.push(event.timestampMs - previous);
1740
+ previous = event.timestampMs;
1741
+ if (event.vector !== undefined)
1742
+ columns.vector[position] = event.vector;
1743
+ });
1744
+ const { events: _events, ...rest } = snapshot;
1745
+ return {
1746
+ ...rest,
1747
+ eventColumns: {
1748
+ types: [...tables.types.keys()],
1749
+ phases: [...tables.phases.keys()],
1750
+ environments: [...tables.environments.keys()],
1751
+ ...columns,
1752
+ },
1753
+ };
1754
+ }
1526
1755
  function appendEvidenceLine(serialized) {
1527
1756
  const evidenceDirectory = process.env["SUPERCOV_EVIDENCE_DIR"] ??
1528
1757
  (GENERATED_EVIDENCE_DIRECTORY.startsWith("__")
@@ -1542,7 +1771,7 @@ const instrumentedFixtures = {
1542
1771
  const controller = controllers.get(scope.attemptId);
1543
1772
  if (!controller)
1544
1773
  throw new Error(`Supercov's automatic Playwright collector was not initialized for ${scope.attemptId}`);
1545
- await controller.registerPage(page);
1774
+ await timed("fixture.pageSetup", () => controller.registerPage(page));
1546
1775
  try {
1547
1776
  await use(controller.wrap(page));
1548
1777
  }
@@ -1550,7 +1779,7 @@ const instrumentedFixtures = {
1550
1779
  // This fixture still owns a live page. The automatic test fixture tears
1551
1780
  // down after page dependencies and therefore cannot reliably evaluate
1552
1781
  // frames on every Playwright version.
1553
- await controller.collectRuntimeSnapshots();
1782
+ await timed("fixture.pageTeardown", () => controller.collectRuntimeSnapshots());
1554
1783
  }
1555
1784
  },
1556
1785
  browser: [
@@ -1580,12 +1809,25 @@ const instrumentedFixtures = {
1580
1809
  async ({}, use, testInfo) => {
1581
1810
  const scope = executionScope(testInfo);
1582
1811
  const configuredHeaders = Object.fromEntries(Object.entries(testInfo.project.use.extraHTTPHeaders ?? {}).map(([name, value]) => [name, String(value)]));
1812
+ const setupStarted = performance.now();
1583
1813
  const controller = new CoveragePhaseController(scope, configuredHeaders, testInfo.project.use.baseURL);
1584
1814
  controllers.set(scope.attemptId, controller);
1585
1815
  activeController = controller;
1586
1816
  directRuntime()?.activateCoverageScope(scope);
1587
- await controller.adoptTrackedContexts().catch(() => undefined);
1588
- const carried = await controller.takeCarriedEvidence().catch(() => undefined);
1817
+ // The pages the test starts with, whose evidence is taken while
1818
+ // their contexts are registered.
1819
+ const startingPages = [...liveTrackedContexts()].flatMap((context) => {
1820
+ try {
1821
+ return context.pages();
1822
+ }
1823
+ catch {
1824
+ return [];
1825
+ }
1826
+ });
1827
+ const [, carried] = await Promise.all([
1828
+ timed("fixture.adopt", () => controller.adoptTrackedContexts().catch(() => undefined)),
1829
+ timed("fixture.carried", () => controller.takeCarriedEvidence(startingPages).catch(() => undefined)),
1830
+ ]);
1589
1831
  if (carried) {
1590
1832
  appendEvidenceLine(`${JSON.stringify({
1591
1833
  testId: `__supercov_background__:playwright:${scope.attemptId}`,
@@ -1593,7 +1835,7 @@ const instrumentedFixtures = {
1593
1835
  status: "unknown",
1594
1836
  provenance: { runner: "background", kind: "background", source: "explicit" },
1595
1837
  role: "background",
1596
- browser: [carried],
1838
+ browser: [columnarEvents(carried)],
1597
1839
  })}\n`);
1598
1840
  }
1599
1841
  const serverOutput = serverEvidencePath(scope);
@@ -1603,14 +1845,15 @@ const instrumentedFixtures = {
1603
1845
  // no test is credited by time to the one test running then, and a
1604
1846
  // test is running for all of this, not only during its actions.
1605
1847
  const startedAtMs = Date.now();
1848
+ timingCount("fixture.testSetup", performance.now() - setupStarted);
1606
1849
  try {
1607
1850
  await use();
1608
1851
  }
1609
1852
  finally {
1853
+ const teardownStarted = performance.now();
1610
1854
  const endedAtMs = Date.now();
1611
1855
  try {
1612
1856
  const browser = await controller.collectRuntimeSnapshots();
1613
- const server = readServerRecords(scope);
1614
1857
  // Emit an artifact even when this test touched no application source.
1615
1858
  // A complete test-to-coverage matrix must also identify tests that are
1616
1859
  // removable without changing coverage.
@@ -1635,14 +1878,14 @@ const instrumentedFixtures = {
1635
1878
  }),
1636
1879
  phases: mergeCollectedPhases(controller.phases, directRuntime()?.takeNodeAssertionPhases(scope) ?? []),
1637
1880
  ...(controller.workerEvidenceMissing ? { attribution: "partial" } : {}),
1638
- browser,
1639
- server,
1881
+ browser: browser.map(columnarEvents),
1640
1882
  span: [startedAtMs, endedAtMs],
1641
1883
  };
1642
1884
  appendEvidenceLine(`${JSON.stringify(payload)}\n`);
1643
1885
  }
1644
1886
  finally {
1645
1887
  await timed("controller.dispose", () => controller.dispose());
1888
+ timingCount("fixture.testTeardown", performance.now() - teardownStarted);
1646
1889
  timingReport(testInfo.title);
1647
1890
  directRuntime()?.activateCoverageScope();
1648
1891
  if (activeController === controller)
@@ -1656,6 +1899,10 @@ const instrumentedFixtures = {
1656
1899
  };
1657
1900
  const instrumentedTest = base.extend(instrumentedFixtures);
1658
1901
  export const test = instrumentedTest;
1902
+ // `export *` passes no default on, and `import test from "@playwright/test"`
1903
+ // is Playwright's own default export: such a spec found none and failed with
1904
+ // `(0 , _test.default) is not a function`.
1905
+ export default instrumentedTest;
1659
1906
  /**
1660
1907
  * A Playwright `test` object: callable, and carrying the API a spec drives.
1661
1908
  * Facades export several -- one per fixture set -- and every one of them must