@piwitests/reporter 0.52.0 → 0.54.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.
@@ -49,6 +49,7 @@ __export(capture_fixtures_exports, {
49
49
  pageMapFetcher: () => pageMapFetcher,
50
50
  piwiFixtures: () => piwiFixtures,
51
51
  probeElementAttrs: () => probeElementAttrs,
52
+ readPageInteraction: () => readPageInteraction,
52
53
  recordResourceCensus: () => recordResourceCensus,
53
54
  resolveCodeReach: () => resolveCodeReach,
54
55
  startCodeReach: () => startCodeReach,
@@ -1634,6 +1635,57 @@ function mergeCandidates(base, extra) {
1634
1635
  return merged.sort((a, b) => b.score - a.score);
1635
1636
  }
1636
1637
 
1638
+ // ../core/src/page-interaction.ts
1639
+ var ELEMENT_PATH_MAX_CHARS = 2e3;
1640
+ var ELEMENT_LABEL_MAX_CHARS = 120;
1641
+ var MAX_INDETERMINATE = 20;
1642
+ var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
1643
+ var LEVEL = `${STEP}(?:/${STEP})*`;
1644
+ var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
1645
+ function isElementPath(path14) {
1646
+ return typeof path14 === "string" && path14.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path14);
1647
+ }
1648
+ var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
1649
+ function elementRef(v) {
1650
+ if (!v || typeof v !== "object") return null;
1651
+ const { path: path14, label } = v;
1652
+ if (!isElementPath(path14)) return null;
1653
+ return { path: path14, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
1654
+ }
1655
+ function textPoint(v) {
1656
+ if (!v || typeof v !== "object") return null;
1657
+ const { path: path14, offset } = v;
1658
+ const at = offsetOf(offset);
1659
+ return isElementPath(path14) && at !== null ? { path: path14, offset: at } : null;
1660
+ }
1661
+ function sanitizePageInteraction(raw) {
1662
+ if (!raw || typeof raw !== "object") return {};
1663
+ const r = raw;
1664
+ const out = {};
1665
+ const focus = r.focus;
1666
+ const focused = focus && typeof focus === "object" ? elementRef(focus.element) : null;
1667
+ if (focus && focused) {
1668
+ out.focus = { element: focused, visible: focus.visible === true };
1669
+ const range = Array.isArray(focus.selection) && focus.selection.length === 2 ? focus.selection : [];
1670
+ const [start, end] = range.map(offsetOf);
1671
+ if (start != null && end != null && start <= end) out.focus.selection = [start, end];
1672
+ }
1673
+ const hover = elementRef(r.hover);
1674
+ if (hover) out.hover = hover;
1675
+ const selection = r.selection;
1676
+ if (selection && typeof selection === "object") {
1677
+ const start = textPoint(selection.start);
1678
+ const end = textPoint(selection.end);
1679
+ const length = offsetOf(selection.length);
1680
+ if (start && end && length !== null && length > 0) out.selection = { start, end, length };
1681
+ }
1682
+ if (Array.isArray(r.indeterminate)) {
1683
+ const boxes = r.indeterminate.map(elementRef).filter((ref) => ref !== null).slice(0, MAX_INDETERMINATE);
1684
+ if (boxes.length) out.indeterminate = boxes;
1685
+ }
1686
+ return out;
1687
+ }
1688
+
1637
1689
  // src/internal/capture/locator-healing.ts
1638
1690
  var path = __toESM(require("path"));
1639
1691
 
@@ -1971,6 +2023,9 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
1971
2023
  return null;
1972
2024
  }
1973
2025
 
2026
+ // ../core/src/wire.ts
2027
+ var FAILURE_SCREENSHOT_ATTACHMENT = "piwi-failure-screenshot";
2028
+
1974
2029
  // src/internal/capture/attachments.ts
1975
2030
  var ATTACHMENT_NAMES = {
1976
2031
  locators: "piwi-locators",
@@ -1982,6 +2037,7 @@ var ATTACHMENT_NAMES = {
1982
2037
  pageInventory: "piwi-page-inventory",
1983
2038
  locatorPages: "piwi-locator-pages",
1984
2039
  codeReach: "piwi-code-reach",
2040
+ serverCoverageRequests: "piwi-server-coverage-requests",
1985
2041
  webVitals: "piwi-web-vitals",
1986
2042
  locatorSuggestion: "piwi-locator-suggestion",
1987
2043
  pageState: "piwi-page-state",
@@ -1994,6 +2050,25 @@ var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1994
2050
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
1995
2051
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
1996
2052
 
2053
+ // src/internal/capture/failure-screenshot.ts
2054
+ function keepsFailureScreenshots(option, retry) {
2055
+ const mode = typeof option === "string" ? option : option?.mode;
2056
+ return mode === "on" || mode === "only-on-failure" || mode === "on-first-failure" && retry === 0;
2057
+ }
2058
+ async function readFailureScreenshot(page, option) {
2059
+ const options = typeof option === "object" && option !== null ? option : null;
2060
+ try {
2061
+ return await page.screenshot({
2062
+ fullPage: options?.fullPage,
2063
+ omitBackground: options?.omitBackground,
2064
+ timeout: 5e3,
2065
+ caret: "initial"
2066
+ });
2067
+ } catch {
2068
+ return null;
2069
+ }
2070
+ }
2071
+
1997
2072
  // src/internal/capture/page-inventory.ts
1998
2073
  var PAGE_INVENTORY_MAX_ENTRIES = 500;
1999
2074
  function collectPageInventoryInPage(maxEntries) {
@@ -2225,7 +2300,7 @@ var DEFAULTS = {
2225
2300
  // Off by default: the page inventory reads control and link names from every
2226
2301
  // visited page, so it stays opt-in until a project turns it on.
2227
2302
  capturePageInventory: false,
2228
- // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2303
+ // Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
2229
2304
  captureCodeReach: false,
2230
2305
  captureServerTraces: true,
2231
2306
  captureResources: true,
@@ -2270,6 +2345,8 @@ var PIWI_ENV_KEYS = {
2270
2345
  capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
2271
2346
  captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2272
2347
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2348
+ coverageBrowser: "PIWI_COVERAGE_BROWSER",
2349
+ coverageServer: "PIWI_COVERAGE_SERVER",
2273
2350
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2274
2351
  captureResources: "PIWI_CAPTURE_RESOURCES",
2275
2352
  leakCheck: "PIWI_LEAK_CHECK",
@@ -3826,6 +3903,18 @@ async function pauseHere(page, testInfo, point, probe, onPick) {
3826
3903
  }
3827
3904
  }
3828
3905
 
3906
+ // src/internal/collect/coverage-settings.ts
3907
+ var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
3908
+ function isServerGranularity(value) {
3909
+ return typeof value === "string" && SERVER_VALUES.has(value);
3910
+ }
3911
+ function browserCoverageInWorker() {
3912
+ return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || process.env[PIWI_ENV_KEYS.coverageBrowser] === "files";
3913
+ }
3914
+ function serverCoverageInWorker() {
3915
+ return isServerGranularity(process.env[PIWI_ENV_KEYS.coverageServer]);
3916
+ }
3917
+
3829
3918
  // src/internal/support/aria-sampling.ts
3830
3919
  var path7 = __toESM(require("path"));
3831
3920
  var os3 = __toESM(require("os"));
@@ -3950,6 +4039,9 @@ function boxCaptureFrames(extraPrefixes = []) {
3950
4039
  } catch {
3951
4040
  }
3952
4041
  }
4042
+ function callMethod(target, prop, args) {
4043
+ return target[prop](...args);
4044
+ }
3953
4045
  function internalCall(owner, run) {
3954
4046
  const wrap = owner?._wrapApiCall;
3955
4047
  if (typeof wrap !== "function") return run();
@@ -3962,6 +4054,16 @@ var fs6 = __toESM(require("fs"));
3962
4054
  var path10 = __toESM(require("path"));
3963
4055
  var import_node_child_process = require("child_process");
3964
4056
 
4057
+ // ../core/src/coverage.ts
4058
+ var MAX_PATH_LENGTH = 500;
4059
+ function normalizeCoveragePath(raw) {
4060
+ if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4061
+ const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4062
+ if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
4063
+ if (/(^|\/)node_modules\//.test(path14)) return null;
4064
+ return path14;
4065
+ }
4066
+
3965
4067
  // ../core/src/source-map.ts
3966
4068
  var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3967
4069
  var BASE64_VALUE = new Int8Array(128).fill(-1);
@@ -4173,10 +4275,8 @@ function reachedSources(entry, map) {
4173
4275
  function finalizeCodeReach(files) {
4174
4276
  const out = /* @__PURE__ */ new Set();
4175
4277
  for (const raw of files) {
4176
- const file = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4177
- if (!file || file.startsWith("../") || file.startsWith("/") || /^[A-Za-z]:\//.test(file)) continue;
4178
- if (/(^|\/)node_modules\//.test(file)) continue;
4179
- out.add(file);
4278
+ const file = normalizeCoveragePath(raw);
4279
+ if (file !== null) out.add(file);
4180
4280
  }
4181
4281
  return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
4182
4282
  }
@@ -4277,8 +4377,8 @@ function fileForUrl(url, roots, bases) {
4277
4377
  function relativeToRepo(file, repoRoot) {
4278
4378
  return path10.relative(repoRoot, file).split(path10.sep).join("/");
4279
4379
  }
4280
- function scriptHash(entry) {
4281
- return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
4380
+ function scriptVersion(entry) {
4381
+ return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
4282
4382
  }
4283
4383
  async function loadMap(entry, fetchMap) {
4284
4384
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -4293,7 +4393,7 @@ async function loadMap(entry, fetchMap) {
4293
4393
  } catch {
4294
4394
  return null;
4295
4395
  }
4296
- const version = scriptHash(entry);
4396
+ const version = scriptVersion(entry);
4297
4397
  let pending = mapCache.get(absolute, version);
4298
4398
  if (!pending) {
4299
4399
  pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
@@ -4309,29 +4409,34 @@ function parseMap(text) {
4309
4409
  }
4310
4410
  }
4311
4411
  async function resolveScript(entry, roots, bases, fetchMap) {
4312
- const version = scriptHash(entry);
4412
+ const version = scriptVersion(entry);
4313
4413
  const cached = scriptCache.get(entry.url, version);
4314
4414
  if (cached) return cached;
4315
- let resolution;
4316
4415
  const file = fileForUrl(entry.url, roots.roots, bases);
4317
- if (file) resolution = { kind: "file", file, lookup: await loadMap(entry, fetchMap) };
4318
- else {
4319
- const lookup = await loadMap(entry, fetchMap);
4320
- resolution = lookup ? { kind: "map", lookup, files: [] } : { kind: "none" };
4321
- }
4322
- scriptCache.set(entry.url, version, resolution);
4416
+ const lookup = await loadMap(entry, fetchMap);
4417
+ let resolution;
4418
+ if (file) resolution = { kind: "file", file, lookup, source: entry.source };
4419
+ else resolution = lookup ? { kind: "map", lookup, files: [], source: entry.source } : { kind: "none" };
4420
+ if (entry.source !== void 0) scriptCache.set(entry.url, version, resolution);
4323
4421
  return resolution;
4324
4422
  }
4423
+ function withSource(entry, resolution) {
4424
+ if (entry.source !== void 0 || resolution.source === void 0) return entry;
4425
+ return { ...entry, source: resolution.source };
4426
+ }
4325
4427
  async function resolveCodeReach(entries, roots, fetchMap) {
4326
4428
  const files = /* @__PURE__ */ new Set();
4327
4429
  const bases = viteBases(entries);
4328
- for (const entry of entries) {
4329
- if (!entry.url || !entry.functions?.length || REPLAYED_SCRIPT.test(entry.url)) continue;
4330
- const resolution = await resolveScript(entry, roots, bases, fetchMap);
4430
+ const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
4431
+ const known = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
4432
+ for (const [i, covered] of scripts.entries()) {
4433
+ const resolution = known[i] ?? await resolveScript(covered, roots, bases, fetchMap);
4331
4434
  if (resolution.kind === "file") {
4435
+ const entry = withSource(covered, resolution);
4332
4436
  const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
4333
4437
  if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
4334
4438
  } else if (resolution.kind === "map") {
4439
+ const entry = withSource(covered, resolution);
4335
4440
  for (const index of reachedSources(entry, resolution.lookup)) {
4336
4441
  if (resolution.files[index] === void 0) {
4337
4442
  const source = resolution.lookup.sources[index] ?? "";
@@ -4373,23 +4478,85 @@ function isChromiumPage(page) {
4373
4478
  return false;
4374
4479
  }
4375
4480
  }
4481
+ var captures = /* @__PURE__ */ new WeakMap();
4482
+ function versionKey(url, hash) {
4483
+ return `${hash} ${url}`;
4484
+ }
4485
+ function onScriptParsed(page, capture, event) {
4486
+ if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
4487
+ capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash });
4488
+ const key = versionKey(event.url, event.hash);
4489
+ if (capture.sources.has(key) || scriptCache.get(event.url, event.hash)) return;
4490
+ const source = internalCall(
4491
+ page,
4492
+ () => capture.cdp.send("Debugger.getScriptSource", { scriptId: event.scriptId })
4493
+ ).then(
4494
+ (response) => response.scriptSource,
4495
+ () => {
4496
+ if (capture.sources.get(key) === source) capture.sources.delete(key);
4497
+ return null;
4498
+ }
4499
+ );
4500
+ capture.sources.set(key, source);
4501
+ }
4502
+ async function detach(page, cdp) {
4503
+ try {
4504
+ await internalCall(page, () => cdp.detach());
4505
+ } catch {
4506
+ }
4507
+ }
4376
4508
  async function startCodeReach(page) {
4377
- if (!isChromiumPage(page) || !page.coverage) return false;
4509
+ if (!isChromiumPage(page) || captures.has(page)) return false;
4510
+ let cdp = null;
4378
4511
  try {
4512
+ cdp = await internalCall(page, () => page.context().newCDPSession(page));
4513
+ const capture = { cdp, scripts: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
4514
+ cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
4515
+ cdp.on("Debugger.paused", () => {
4516
+ internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
4517
+ });
4518
+ });
4379
4519
  await internalCall(
4380
4520
  page,
4381
- () => page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: false })
4521
+ () => Promise.all([
4522
+ capture.cdp.send("Debugger.enable"),
4523
+ capture.cdp.send("Debugger.setSkipAllPauses", { skip: true }),
4524
+ capture.cdp.send("Profiler.enable"),
4525
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
4526
+ ])
4382
4527
  );
4528
+ captures.set(page, capture);
4383
4529
  return true;
4384
4530
  } catch {
4531
+ if (cdp) await detach(page, cdp);
4385
4532
  return false;
4386
4533
  }
4387
4534
  }
4388
4535
  async function stopCodeReach(page) {
4536
+ const capture = captures.get(page);
4537
+ if (!capture) return null;
4538
+ captures.delete(page);
4539
+ const { cdp } = capture;
4389
4540
  try {
4390
- return await internalCall(page, () => page.coverage.stopJSCoverage());
4541
+ const coverage = await internalCall(page, async () => {
4542
+ const taken = await cdp.send("Profiler.takePreciseCoverage");
4543
+ await cdp.send("Profiler.stopPreciseCoverage");
4544
+ return taken;
4545
+ });
4546
+ const entries = [];
4547
+ for (const script of coverage.result) {
4548
+ const parsed2 = capture.scripts.get(script.scriptId);
4549
+ if (!parsed2) continue;
4550
+ const entry = { url: parsed2.url, hash: parsed2.hash, functions: script.functions };
4551
+ const source = await capture.sources.get(versionKey(parsed2.url, parsed2.hash));
4552
+ if (source != null) entry.source = source;
4553
+ entries.push(entry);
4554
+ }
4555
+ return entries;
4391
4556
  } catch {
4392
4557
  return null;
4558
+ } finally {
4559
+ await detach(page, cdp);
4393
4560
  }
4394
4561
  }
4395
4562
 
@@ -5258,6 +5425,243 @@ async function recordResourceCensus(testInfo) {
5258
5425
  if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
5259
5426
  }
5260
5427
 
5428
+ // src/internal/capture/page-interaction.ts
5429
+ function readPageInteraction() {
5430
+ const doc = globalThis.document;
5431
+ const SKIPPED_TEXT = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"];
5432
+ const pathOf = (el) => {
5433
+ const levels = [];
5434
+ for (let node = el; node; ) {
5435
+ const steps = [];
5436
+ let root = null;
5437
+ for (let cur = node; cur; cur = cur.parentElement) {
5438
+ let position = 1;
5439
+ for (let s = cur.previousElementSibling; s; s = s.previousElementSibling) {
5440
+ if (s.localName === cur.localName) position++;
5441
+ }
5442
+ steps.unshift(`${cur.localName}:${position}`);
5443
+ if (!cur.parentElement) root = cur.parentNode;
5444
+ }
5445
+ levels.unshift(steps.join("/"));
5446
+ node = root && root.host ? root.host : null;
5447
+ }
5448
+ return levels.join("|");
5449
+ };
5450
+ const isField = (el) => /^(input|textarea|select)$/.test(el.localName) || !!el.isContentEditable;
5451
+ const label = (el) => {
5452
+ const tag = el.localName;
5453
+ if (el.id) return `${tag}#${el.id}`;
5454
+ for (const attr2 of ["name", "aria-label"]) {
5455
+ const value = el.getAttribute(attr2);
5456
+ if (value) return `${tag}[${attr2}="${value}"]`;
5457
+ }
5458
+ const text = isField(el) ? "" : String(el.textContent || "").replace(/\s+/g, " ").trim();
5459
+ return text ? `${tag} "${text.length > 40 ? `${text.slice(0, 40)}\u2026` : text}"` : tag;
5460
+ };
5461
+ const ref = (el) => ({ path: pathOf(el), label: label(el) });
5462
+ const textOffset = (el, stop, extra) => {
5463
+ const walker = doc.createTreeWalker(el, 4);
5464
+ let offset = 0;
5465
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
5466
+ if (stop && (text === stop || stop.contains(text) || stop.compareDocumentPosition(text) & 4)) break;
5467
+ if (!SKIPPED_TEXT.includes(text.parentElement?.tagName)) offset += text.data.length;
5468
+ }
5469
+ return offset + extra;
5470
+ };
5471
+ const textPoint2 = (node, offset) => {
5472
+ if (node.nodeType === 3)
5473
+ return { path: pathOf(node.parentElement), offset: textOffset(node.parentElement, node, offset) };
5474
+ return { path: pathOf(node), offset: textOffset(node, node.childNodes[offset] ?? null, 0) };
5475
+ };
5476
+ const out = {};
5477
+ let active = doc.activeElement;
5478
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
5479
+ if (active && active !== doc.body && active !== doc.documentElement) {
5480
+ let visible = false;
5481
+ try {
5482
+ visible = active.matches(":focus-visible");
5483
+ } catch {
5484
+ }
5485
+ out.focus = { element: ref(active), visible };
5486
+ try {
5487
+ if (typeof active.selectionStart === "number" && typeof active.selectionEnd === "number") {
5488
+ out.focus.selection = [active.selectionStart, active.selectionEnd];
5489
+ }
5490
+ } catch {
5491
+ }
5492
+ }
5493
+ let scope = doc;
5494
+ for (; ; ) {
5495
+ const chain = scope.querySelectorAll(":hover");
5496
+ const innermost = chain[chain.length - 1];
5497
+ if (!innermost) break;
5498
+ out.hover = ref(innermost);
5499
+ if (!innermost.shadowRoot) break;
5500
+ scope = innermost.shadowRoot;
5501
+ }
5502
+ const selection = doc.getSelection?.();
5503
+ if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active && isField(active))) {
5504
+ const range = selection.getRangeAt(0);
5505
+ const start = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer;
5506
+ const end = range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer;
5507
+ if (start?.nodeType === 1 && end?.nodeType === 1) {
5508
+ out.selection = {
5509
+ start: textPoint2(range.startContainer, range.startOffset),
5510
+ end: textPoint2(range.endContainer, range.endOffset),
5511
+ length: String(selection).length
5512
+ };
5513
+ }
5514
+ }
5515
+ const boxes = [];
5516
+ const roots = [doc];
5517
+ for (let i = 0; i < roots.length && boxes.length < 20; i++) {
5518
+ for (const el of roots[i].querySelectorAll("*")) {
5519
+ if (el.shadowRoot) roots.push(el.shadowRoot);
5520
+ if (el.localName === "input" && el.type === "checkbox" && el.indeterminate && boxes.length < 20)
5521
+ boxes.push(ref(el));
5522
+ }
5523
+ }
5524
+ if (boxes.length) out.indeterminate = boxes;
5525
+ return out;
5526
+ }
5527
+
5528
+ // src/internal/capture/api-requests.ts
5529
+ var SENDING_METHODS = /* @__PURE__ */ new Set(["fetch", "get", "post", "put", "patch", "delete", "head"]);
5530
+ var PROXIES = /* @__PURE__ */ new WeakMap();
5531
+ var OBSERVED = /* @__PURE__ */ new WeakSet();
5532
+ var PATCHED_REQUESTS = /* @__PURE__ */ new WeakSet();
5533
+ function observeApiRequests(playwright, recorder) {
5534
+ const apiRequest = playwright?.request;
5535
+ if (!apiRequest || typeof apiRequest !== "object" || typeof apiRequest.newContext !== "function") return;
5536
+ if (PATCHED_REQUESTS.has(apiRequest)) return;
5537
+ PATCHED_REQUESTS.add(apiRequest);
5538
+ const create = apiRequest.newContext.bind(apiRequest);
5539
+ apiRequest.newContext = async function newContext(...args) {
5540
+ const context = await create(...args);
5541
+ return observeApiRequestContext(context, recorder, baseUrlOption(args[0]));
5542
+ };
5543
+ }
5544
+ function observeApiRequestContext(context, recorder, baseURL = null) {
5545
+ if (!context || typeof context !== "object" || OBSERVED.has(context)) return context;
5546
+ const known = PROXIES.get(context);
5547
+ if (known) return known;
5548
+ const forwarders = /* @__PURE__ */ new Map();
5549
+ const proxy = new Proxy(context, {
5550
+ get(target, prop) {
5551
+ const value = Reflect.get(target, prop);
5552
+ if (typeof value !== "function" || prop === "constructor") return value;
5553
+ const cached = forwarders.get(prop);
5554
+ if (cached?.method === value) return cached.forward;
5555
+ const forward = typeof prop === "string" && SENDING_METHODS.has(prop) ? sendingMethod(target, prop, recorder, baseURL) : value.bind(target);
5556
+ forwarders.set(prop, { method: value, forward });
5557
+ return forward;
5558
+ }
5559
+ });
5560
+ PROXIES.set(context, proxy);
5561
+ OBSERVED.add(proxy);
5562
+ return proxy;
5563
+ }
5564
+ function sendingMethod(target, prop, recorder, baseURL) {
5565
+ return function send(...args) {
5566
+ const record = recorder();
5567
+ if (!record) return callMethod(target, prop, args);
5568
+ const startedAt = Date.now();
5569
+ const result = callMethod(target, prop, args);
5570
+ if (typeof result?.then !== "function") return result;
5571
+ const report = (response, error) => {
5572
+ try {
5573
+ record({
5574
+ method: methodOf(prop, args),
5575
+ url: urlOf(args[0]),
5576
+ baseURL,
5577
+ startedAt,
5578
+ endedAt: Date.now(),
5579
+ response,
5580
+ error
5581
+ });
5582
+ } catch {
5583
+ }
5584
+ };
5585
+ return result.then(
5586
+ (response) => {
5587
+ report(response);
5588
+ return response;
5589
+ },
5590
+ (error) => {
5591
+ report(null, error);
5592
+ throw error;
5593
+ }
5594
+ );
5595
+ };
5596
+ }
5597
+ function methodOf(prop, args) {
5598
+ if (prop !== "fetch") return prop.toUpperCase();
5599
+ const option = args[1]?.method;
5600
+ if (typeof option === "string" && option) return option.toUpperCase();
5601
+ const request = args[0];
5602
+ const method = typeof request?.method === "function" ? request.method() : null;
5603
+ return typeof method === "string" && method ? method.toUpperCase() : "GET";
5604
+ }
5605
+ function urlOf(target) {
5606
+ if (typeof target === "string") return target;
5607
+ const url = typeof target?.url === "function" ? target.url() : null;
5608
+ return typeof url === "string" ? url : null;
5609
+ }
5610
+ function baseUrlOption(options) {
5611
+ const baseURL = options?.baseURL;
5612
+ return typeof baseURL === "string" && baseURL ? baseURL : null;
5613
+ }
5614
+ var MAX_FAILURE_LENGTH = 300;
5615
+ var API_ERROR_PREFIX = /^apiRequestContext\.\w+: /;
5616
+ var STATUS_FAILURE = /^(\d{3}) /;
5617
+ function apiCallEntry(call, baseURL) {
5618
+ const response = call.response;
5619
+ if (response) {
5620
+ const timing = responseTiming(response) ?? { startTime: call.startedAt, duration: call.endedAt - call.startedAt };
5621
+ return {
5622
+ method: call.method,
5623
+ url: response.url(),
5624
+ status: response.status(),
5625
+ duration: timing.duration,
5626
+ startTime: timing.startTime
5627
+ };
5628
+ }
5629
+ const url = resolveUrl(call.url, call.baseURL ?? baseURL);
5630
+ if (!url) return null;
5631
+ const failure = failureText(call.error);
5632
+ const status = STATUS_FAILURE.exec(failure);
5633
+ return {
5634
+ method: call.method,
5635
+ url,
5636
+ status: status ? Number(status[1]) : 0,
5637
+ duration: Math.max(0, call.endedAt - call.startedAt),
5638
+ startTime: call.startedAt,
5639
+ ...status ? {} : { failure }
5640
+ };
5641
+ }
5642
+ function responseTiming(response) {
5643
+ if (typeof response.timing !== "function") return null;
5644
+ const timing = response.timing();
5645
+ if (!(timing.startTime > 0) || !(timing.responseEnd >= 0)) return null;
5646
+ return {
5647
+ startTime: timing.startTime,
5648
+ duration: Math.max(0, Math.round(timing.responseEnd - Math.max(0, timing.requestStart)))
5649
+ };
5650
+ }
5651
+ function resolveUrl(url, baseURL) {
5652
+ if (!url) return null;
5653
+ try {
5654
+ return new URL(url, baseURL ?? void 0).toString();
5655
+ } catch {
5656
+ return url;
5657
+ }
5658
+ }
5659
+ function failureText(error) {
5660
+ const message = error instanceof Error ? error.message : String(error);
5661
+ const line = message.split("\n", 1)[0].replace(API_ERROR_PREFIX, "").trim();
5662
+ return (line || "failed").slice(0, MAX_FAILURE_LENGTH);
5663
+ }
5664
+
5261
5665
  // src/internal/capture/capture-fixtures.ts
5262
5666
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
5263
5667
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -5291,6 +5695,7 @@ function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
5291
5695
  function createSink() {
5292
5696
  return {
5293
5697
  networkRequests: [],
5698
+ openResponses: /* @__PURE__ */ new Map(),
5294
5699
  consoleEntries: [],
5295
5700
  dialogs: [],
5296
5701
  pendingHandlers: [],
@@ -5304,6 +5709,8 @@ function createSink() {
5304
5709
  stashedPageState: null,
5305
5710
  stashedAria: null,
5306
5711
  stashedAriaJson: null,
5712
+ failureScreenshot: null,
5713
+ stashedScreenshot: null,
5307
5714
  pageInventories: [],
5308
5715
  probeItem: null,
5309
5716
  probePageErrors: 0,
@@ -5314,12 +5721,14 @@ function createSink() {
5314
5721
  userPick: null,
5315
5722
  pause: null,
5316
5723
  locatorPages: new LocatorPageLog(),
5317
- codeReach: null
5724
+ codeReach: null,
5725
+ serverCoverage: /* @__PURE__ */ new Map(),
5726
+ baseURL: null
5318
5727
  };
5319
5728
  }
5320
5729
  function maybeStartCodeReach(page) {
5321
5730
  const sink = currentSink;
5322
- if (!sink || process.env.PIWI_CAPTURE_CODE_REACH !== "true") return Promise.resolve(false);
5731
+ if (!sink || !browserCoverageInWorker()) return Promise.resolve(false);
5323
5732
  if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
5324
5733
  const started = startCodeReach(page);
5325
5734
  sink.codeReach = { page, started, stopped: false, files: null };
@@ -5460,7 +5869,7 @@ var PAGE_STATE_MAX_STORAGE_KEYS = 50;
5460
5869
  var PAGE_STATE_MAX_COOKIES = 30;
5461
5870
  var PAGE_STATE_HISTORY_CAP = 2048;
5462
5871
  var TOKEN_MASK_RES = [/\beyJ[\w-]{10,}\.[\w-]{5,}\.[\w-]{5,}\b/g, /\b[0-9a-f]{32,}\b/gi];
5463
- function buildPageState(raw, cookies) {
5872
+ function buildPageState(raw, cookies, interaction) {
5464
5873
  let historyState = raw.historyState;
5465
5874
  if (historyState) {
5466
5875
  for (const re of TOKEN_MASK_RES) historyState = historyState.replace(re, "[masked]");
@@ -5486,7 +5895,8 @@ function buildPageState(raw, cookies) {
5486
5895
  secure: Boolean(c.secure),
5487
5896
  ...c.sameSite !== void 0 ? { sameSite: String(c.sameSite) } : {},
5488
5897
  ...typeof c.expires === "number" ? { expires: c.expires } : {}
5489
- }))
5898
+ })),
5899
+ ...sanitizePageInteraction(interaction)
5490
5900
  };
5491
5901
  }
5492
5902
  async function readPageState(page) {
@@ -5524,7 +5934,8 @@ async function readPageState(page) {
5524
5934
  } catch {
5525
5935
  cookies = null;
5526
5936
  }
5527
- return buildPageState(raw, cookies);
5937
+ const interaction = await page.evaluate(readPageInteraction).catch(() => null);
5938
+ return buildPageState(raw, cookies, interaction);
5528
5939
  } catch {
5529
5940
  return null;
5530
5941
  }
@@ -5616,6 +6027,8 @@ async function readPageBeforeClose(sink, page) {
5616
6027
  };
5617
6028
  if (status === "failed" || status === "timedOut" || status === "interrupted") {
5618
6029
  await sampleAria();
6030
+ if (sink.failureScreenshot)
6031
+ sink.stashedScreenshot = await readFailureScreenshot(page, sink.failureScreenshot) ?? sink.stashedScreenshot;
5619
6032
  } else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
5620
6033
  await sampleAria();
5621
6034
  }
@@ -5765,9 +6178,6 @@ function startElementCapture(sink, page, target, seq, callerLocation, used) {
5765
6178
  })();
5766
6179
  sink.capturePromises.push(resolveAttrs);
5767
6180
  }
5768
- function callMethod(target, prop, args) {
5769
- return target[prop](...args);
5770
- }
5771
6181
  function recordLocatorPage(sink, page, locator, location, arrival) {
5772
6182
  try {
5773
6183
  sink.locatorPages.record({ location, locator: String(locator), url: page.url(), arrival });
@@ -5934,6 +6344,7 @@ function instrumentPage(page) {
5934
6344
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
5935
6345
  INSTRUMENTED_PAGES.add(page);
5936
6346
  void maybeStartCodeReach(page);
6347
+ observeOwnApiRequests(page);
5937
6348
  const probeSink = currentSink;
5938
6349
  if (probeSink?.probeItem && !probeSink.probeInterception) {
5939
6350
  void installProbeInterception(page, probeSink.probeItem).then((interception) => {
@@ -6040,59 +6451,115 @@ function instrumentPage(page) {
6040
6451
  } catch {
6041
6452
  }
6042
6453
  }
6043
- const recordRequest = (request, failed) => {
6454
+ page.on("response", (response) => {
6044
6455
  const sink = currentSink;
6045
6456
  if (!sink) return;
6457
+ try {
6458
+ const request = response.request();
6459
+ if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
6460
+ } catch {
6461
+ }
6462
+ });
6463
+ const onRequestEnd = (request, failed) => {
6464
+ const sink = currentSink;
6465
+ if (!sink || RECORDED_OPEN.has(request)) return;
6046
6466
  sink.lastActivePage = page;
6047
- const p = (async () => {
6048
- try {
6049
- const url = request.url();
6050
- if (url.startsWith("data:") || url.startsWith("blob:")) return;
6051
- const resourceType = request.resourceType();
6052
- if (!TRACKED_REQUEST_TYPES.includes(resourceType)) return;
6053
- const failure = failed ? request.failure()?.errorText ?? "failed" : null;
6054
- if (failure && CANCELLED_REQUEST.test(failure)) return;
6055
- const timing = request.timing();
6056
- const response = failed ? null : await request.response();
6057
- const entry = {
6058
- method: request.method(),
6059
- url,
6060
- status: response ? response.status() : 0,
6061
- duration: requestDuration(timing, failed),
6062
- startTime: timing.startTime,
6063
- resourceType
6064
- };
6065
- if (failure) entry.failure = failure;
6066
- if (response) {
6067
- if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
6068
- if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
6069
- }
6070
- const headers = response.headers();
6071
- const contentType = headers["content-type"];
6072
- if (contentType) entry.contentType = contentType.split(";")[0].trim();
6073
- const logHeader = headers["x-piwi-logs"];
6074
- if (logHeader) {
6075
- try {
6076
- entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
6077
- } catch {
6078
- }
6079
- }
6080
- const traceHeader = headers["x-piwi-trace"];
6081
- if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
6082
- try {
6083
- entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
6084
- } catch {
6085
- }
6086
- }
6467
+ const response = sink.openResponses.get(request) ?? null;
6468
+ sink.openResponses.delete(request);
6469
+ recordRequest(sink, request, failed ? "failed" : "finished", response);
6470
+ };
6471
+ page.on("requestfinished", (request) => onRequestEnd(request, false));
6472
+ page.on("requestfailed", (request) => onRequestEnd(request, true));
6473
+ }
6474
+ var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
6475
+ function isTrackedRequest(url, resourceType) {
6476
+ if (url.startsWith("data:") || url.startsWith("blob:")) return false;
6477
+ return TRACKED_REQUEST_TYPES.includes(resourceType);
6478
+ }
6479
+ function recordRequest(sink, request, end, response) {
6480
+ const p = (async () => {
6481
+ try {
6482
+ const url = request.url();
6483
+ const resourceType = request.resourceType();
6484
+ if (!isTrackedRequest(url, resourceType)) return;
6485
+ let failure = end === "failed" ? request.failure()?.errorText ?? "failed" : null;
6486
+ if (failure && CANCELLED_REQUEST.test(failure)) {
6487
+ if (!response) return;
6488
+ failure = null;
6489
+ end = "unfinished";
6490
+ }
6491
+ const timing = request.timing();
6492
+ if (end === "failed") response = null;
6493
+ else if (end === "finished") response ??= await request.response();
6494
+ const entry = {
6495
+ method: request.method(),
6496
+ url,
6497
+ status: response ? response.status() : 0,
6498
+ duration: requestDuration(timing, end),
6499
+ startTime: timing.startTime,
6500
+ resourceType
6501
+ };
6502
+ if (failure) entry.failure = failure;
6503
+ if (response) {
6504
+ if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
6505
+ if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
6087
6506
  }
6088
- sink.networkRequests.push(entry);
6089
- } catch {
6507
+ readResponseHeaders(sink, entry, url, response.headers());
6090
6508
  }
6091
- })();
6092
- sink.pendingHandlers.push(p);
6093
- };
6094
- page.on("requestfinished", (request) => recordRequest(request, false));
6095
- page.on("requestfailed", (request) => recordRequest(request, true));
6509
+ sink.networkRequests.push(entry);
6510
+ } catch {
6511
+ }
6512
+ })();
6513
+ sink.pendingHandlers.push(p);
6514
+ }
6515
+ function readResponseHeaders(sink, entry, url, headers) {
6516
+ const contentType = headers["content-type"];
6517
+ if (contentType) entry.contentType = contentType.split(";")[0].trim();
6518
+ const logHeader = headers["x-piwi-logs"];
6519
+ if (logHeader) {
6520
+ try {
6521
+ entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
6522
+ } catch {
6523
+ }
6524
+ }
6525
+ const coverageHeader = headers["x-piwi-coverage"];
6526
+ if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
6527
+ const traceHeader = headers["x-piwi-trace"];
6528
+ if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
6529
+ try {
6530
+ entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
6531
+ } catch {
6532
+ }
6533
+ }
6534
+ }
6535
+ function recordApiCall(sink, call) {
6536
+ try {
6537
+ const entry = apiCallEntry(call, sink.baseURL);
6538
+ if (!entry) return;
6539
+ if (call.response) readResponseHeaders(sink, entry, String(entry.url), call.response.headers());
6540
+ sink.networkRequests.push(entry);
6541
+ } catch {
6542
+ }
6543
+ }
6544
+ function apiCallRecorder() {
6545
+ const sink = currentSink;
6546
+ return sink ? (call) => recordApiCall(sink, call) : null;
6547
+ }
6548
+ function observeOwnApiRequests(owner) {
6549
+ try {
6550
+ const holder = owner;
6551
+ if (holder.request && typeof holder.request === "object") {
6552
+ holder.request = observeApiRequestContext(holder.request, apiCallRecorder);
6553
+ }
6554
+ } catch {
6555
+ }
6556
+ }
6557
+ function recordOpenRequests(sink) {
6558
+ for (const [request, response] of sink.openResponses) {
6559
+ RECORDED_OPEN.add(request);
6560
+ recordRequest(sink, request, "unfinished", response);
6561
+ }
6562
+ sink.openResponses.clear();
6096
6563
  }
6097
6564
  async function awaitFlakeConditions() {
6098
6565
  await currentSink?.flake?.conditions;
@@ -6100,6 +6567,7 @@ async function awaitFlakeConditions() {
6100
6567
  function instrumentContext(context) {
6101
6568
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
6102
6569
  INSTRUMENTED_CONTEXTS.add(context);
6570
+ observeOwnApiRequests(context);
6103
6571
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
6104
6572
  void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
6105
6573
  });
@@ -6159,12 +6627,36 @@ function readRootAriaJson(page) {
6159
6627
  return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
6160
6628
  }
6161
6629
  var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
6162
- function requestDuration(timing, failed) {
6630
+ var SERVER_COVERAGE_ID = /^[\x21-\x7e]{1,128}$/;
6631
+ function recordServerCoverageIds(sink, url, value) {
6632
+ let origin;
6633
+ try {
6634
+ const parsed2 = new URL(url);
6635
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") return;
6636
+ origin = parsed2.origin;
6637
+ } catch {
6638
+ return;
6639
+ }
6640
+ for (const id of value.split(/[\s,]+/)) {
6641
+ if (!SERVER_COVERAGE_ID.test(id)) continue;
6642
+ let ids = sink.serverCoverage.get(origin);
6643
+ if (!ids) {
6644
+ ids = /* @__PURE__ */ new Set();
6645
+ sink.serverCoverage.set(origin, ids);
6646
+ }
6647
+ ids.add(id);
6648
+ }
6649
+ }
6650
+ function requestDuration(timing, end) {
6651
+ if (end === "unfinished") {
6652
+ return timing.responseStart >= 0 ? Math.max(0, Math.round(timing.responseStart - timing.requestStart)) : 0;
6653
+ }
6163
6654
  if (timing.responseEnd > 0) return Math.round(timing.responseEnd - timing.requestStart);
6164
- if (failed && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
6655
+ if (end === "failed" && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
6165
6656
  return 0;
6166
6657
  }
6167
6658
  async function flushSink(sink, testInfo) {
6659
+ recordOpenRequests(sink);
6168
6660
  await Promise.allSettled(sink.pendingHandlers);
6169
6661
  let drainDeadline;
6170
6662
  await Promise.race([
@@ -6193,6 +6685,14 @@ async function flushSink(sink, testInfo) {
6193
6685
  body: Buffer.from(JSON.stringify(sink.codeReach.files))
6194
6686
  });
6195
6687
  }
6688
+ if (sink.serverCoverage.size > 0) {
6689
+ const requests = {};
6690
+ for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
6691
+ await testInfo.attach(ATTACHMENT_NAMES.serverCoverageRequests, {
6692
+ contentType: "application/json",
6693
+ body: Buffer.from(JSON.stringify(requests))
6694
+ });
6695
+ }
6196
6696
  if (sink.locatorPages.size > 0) {
6197
6697
  await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
6198
6698
  contentType: "application/json",
@@ -6230,6 +6730,16 @@ async function flushSink(sink, testInfo) {
6230
6730
  } catch {
6231
6731
  }
6232
6732
  }
6733
+ if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
6734
+ const option = sink.failureScreenshot;
6735
+ const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
6736
+ if (shot) {
6737
+ try {
6738
+ await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
6739
+ } catch {
6740
+ }
6741
+ }
6742
+ }
6233
6743
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
6234
6744
  try {
6235
6745
  const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
@@ -6312,14 +6822,17 @@ async function flushSink(sink, testInfo) {
6312
6822
  }
6313
6823
  }
6314
6824
  var piwiFixtures = {
6315
- // Auto, worker-scoped: the worker's resource ledger. Set up before any hook
6316
- // (worker fixtures come first), so a `beforeAll` context is in it too, and
6317
- // torn down after the browser closed, so its census sees what only the
6318
- // worker's shutdown closed. Depends on `playwright` alone, which launches nothing.
6825
+ // Auto, worker-scoped: the worker's resource ledger, and the API request
6826
+ // contexts its tests create, handed out behind the call proxy. Set up before
6827
+ // any hook (worker fixtures come first), so a `beforeAll` context is in the
6828
+ // ledger and records the calls the tests make through it, and torn down after
6829
+ // the browser closed, so its census sees what only the worker's shutdown
6830
+ // closed. Depends on `playwright` alone, which launches nothing.
6319
6831
  piwiResources: [
6320
6832
  async ({ playwright }, use, workerInfo) => {
6321
6833
  boxCaptureFrames();
6322
6834
  startResourceLedger(playwright, workerInfo.workerIndex);
6835
+ observeApiRequests(playwright, apiCallRecorder);
6323
6836
  try {
6324
6837
  await use();
6325
6838
  } finally {
@@ -6350,12 +6863,14 @@ var piwiFixtures = {
6350
6863
  // (attach the collected data) at teardown. Runs for every test without being
6351
6864
  // requested, so suites that never destructure `page` are still captured.
6352
6865
  piwiCapture: [
6353
- async ({}, use, testInfo) => {
6866
+ async ({ screenshot, baseURL }, use, testInfo) => {
6354
6867
  boxCaptureFrames();
6355
6868
  const conflict = labModeConflict();
6356
6869
  if (conflict) throw new Error(conflict);
6357
6870
  const sink = createSink();
6358
6871
  sink.testInfo = testInfo;
6872
+ sink.baseURL = baseURL ?? null;
6873
+ if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
6359
6874
  const pauseSet = currentPauseSet();
6360
6875
  if (pauseSet.size) {
6361
6876
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
@@ -6461,6 +6976,7 @@ function extendPiwiFixtures(test) {
6461
6976
  pageMapFetcher,
6462
6977
  piwiFixtures,
6463
6978
  probeElementAttrs,
6979
+ readPageInteraction,
6464
6980
  recordResourceCensus,
6465
6981
  resolveCodeReach,
6466
6982
  startCodeReach,