@piwitests/reporter 0.52.0 → 0.53.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"));
@@ -3962,6 +4051,16 @@ var fs6 = __toESM(require("fs"));
3962
4051
  var path10 = __toESM(require("path"));
3963
4052
  var import_node_child_process = require("child_process");
3964
4053
 
4054
+ // ../core/src/coverage.ts
4055
+ var MAX_PATH_LENGTH = 500;
4056
+ function normalizeCoveragePath(raw) {
4057
+ if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4058
+ const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4059
+ if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
4060
+ if (/(^|\/)node_modules\//.test(path14)) return null;
4061
+ return path14;
4062
+ }
4063
+
3965
4064
  // ../core/src/source-map.ts
3966
4065
  var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3967
4066
  var BASE64_VALUE = new Int8Array(128).fill(-1);
@@ -4173,10 +4272,8 @@ function reachedSources(entry, map) {
4173
4272
  function finalizeCodeReach(files) {
4174
4273
  const out = /* @__PURE__ */ new Set();
4175
4274
  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);
4275
+ const file = normalizeCoveragePath(raw);
4276
+ if (file !== null) out.add(file);
4180
4277
  }
4181
4278
  return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
4182
4279
  }
@@ -4277,8 +4374,8 @@ function fileForUrl(url, roots, bases) {
4277
4374
  function relativeToRepo(file, repoRoot) {
4278
4375
  return path10.relative(repoRoot, file).split(path10.sep).join("/");
4279
4376
  }
4280
- function scriptHash(entry) {
4281
- return crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
4377
+ function scriptVersion(entry) {
4378
+ return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
4282
4379
  }
4283
4380
  async function loadMap(entry, fetchMap) {
4284
4381
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -4293,7 +4390,7 @@ async function loadMap(entry, fetchMap) {
4293
4390
  } catch {
4294
4391
  return null;
4295
4392
  }
4296
- const version = scriptHash(entry);
4393
+ const version = scriptVersion(entry);
4297
4394
  let pending = mapCache.get(absolute, version);
4298
4395
  if (!pending) {
4299
4396
  pending = fetchMap(absolute).then((text) => text ? parseMap(text) : null);
@@ -4309,29 +4406,34 @@ function parseMap(text) {
4309
4406
  }
4310
4407
  }
4311
4408
  async function resolveScript(entry, roots, bases, fetchMap) {
4312
- const version = scriptHash(entry);
4409
+ const version = scriptVersion(entry);
4313
4410
  const cached = scriptCache.get(entry.url, version);
4314
4411
  if (cached) return cached;
4315
- let resolution;
4316
4412
  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);
4413
+ const lookup = await loadMap(entry, fetchMap);
4414
+ let resolution;
4415
+ if (file) resolution = { kind: "file", file, lookup, source: entry.source };
4416
+ else resolution = lookup ? { kind: "map", lookup, files: [], source: entry.source } : { kind: "none" };
4417
+ if (entry.source !== void 0) scriptCache.set(entry.url, version, resolution);
4323
4418
  return resolution;
4324
4419
  }
4420
+ function withSource(entry, resolution) {
4421
+ if (entry.source !== void 0 || resolution.source === void 0) return entry;
4422
+ return { ...entry, source: resolution.source };
4423
+ }
4325
4424
  async function resolveCodeReach(entries, roots, fetchMap) {
4326
4425
  const files = /* @__PURE__ */ new Set();
4327
4426
  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);
4427
+ const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
4428
+ const known = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
4429
+ for (const [i, covered] of scripts.entries()) {
4430
+ const resolution = known[i] ?? await resolveScript(covered, roots, bases, fetchMap);
4331
4431
  if (resolution.kind === "file") {
4432
+ const entry = withSource(covered, resolution);
4332
4433
  const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
4333
4434
  if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
4334
4435
  } else if (resolution.kind === "map") {
4436
+ const entry = withSource(covered, resolution);
4335
4437
  for (const index of reachedSources(entry, resolution.lookup)) {
4336
4438
  if (resolution.files[index] === void 0) {
4337
4439
  const source = resolution.lookup.sources[index] ?? "";
@@ -4373,23 +4475,85 @@ function isChromiumPage(page) {
4373
4475
  return false;
4374
4476
  }
4375
4477
  }
4478
+ var captures = /* @__PURE__ */ new WeakMap();
4479
+ function versionKey(url, hash) {
4480
+ return `${hash} ${url}`;
4481
+ }
4482
+ function onScriptParsed(page, capture, event) {
4483
+ if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
4484
+ capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash });
4485
+ const key = versionKey(event.url, event.hash);
4486
+ if (capture.sources.has(key) || scriptCache.get(event.url, event.hash)) return;
4487
+ const source = internalCall(
4488
+ page,
4489
+ () => capture.cdp.send("Debugger.getScriptSource", { scriptId: event.scriptId })
4490
+ ).then(
4491
+ (response) => response.scriptSource,
4492
+ () => {
4493
+ if (capture.sources.get(key) === source) capture.sources.delete(key);
4494
+ return null;
4495
+ }
4496
+ );
4497
+ capture.sources.set(key, source);
4498
+ }
4499
+ async function detach(page, cdp) {
4500
+ try {
4501
+ await internalCall(page, () => cdp.detach());
4502
+ } catch {
4503
+ }
4504
+ }
4376
4505
  async function startCodeReach(page) {
4377
- if (!isChromiumPage(page) || !page.coverage) return false;
4506
+ if (!isChromiumPage(page) || captures.has(page)) return false;
4507
+ let cdp = null;
4378
4508
  try {
4509
+ cdp = await internalCall(page, () => page.context().newCDPSession(page));
4510
+ const capture = { cdp, scripts: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
4511
+ cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
4512
+ cdp.on("Debugger.paused", () => {
4513
+ internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
4514
+ });
4515
+ });
4379
4516
  await internalCall(
4380
4517
  page,
4381
- () => page.coverage.startJSCoverage({ resetOnNavigation: false, reportAnonymousScripts: false })
4518
+ () => Promise.all([
4519
+ capture.cdp.send("Debugger.enable"),
4520
+ capture.cdp.send("Debugger.setSkipAllPauses", { skip: true }),
4521
+ capture.cdp.send("Profiler.enable"),
4522
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
4523
+ ])
4382
4524
  );
4525
+ captures.set(page, capture);
4383
4526
  return true;
4384
4527
  } catch {
4528
+ if (cdp) await detach(page, cdp);
4385
4529
  return false;
4386
4530
  }
4387
4531
  }
4388
4532
  async function stopCodeReach(page) {
4533
+ const capture = captures.get(page);
4534
+ if (!capture) return null;
4535
+ captures.delete(page);
4536
+ const { cdp } = capture;
4389
4537
  try {
4390
- return await internalCall(page, () => page.coverage.stopJSCoverage());
4538
+ const coverage = await internalCall(page, async () => {
4539
+ const taken = await cdp.send("Profiler.takePreciseCoverage");
4540
+ await cdp.send("Profiler.stopPreciseCoverage");
4541
+ return taken;
4542
+ });
4543
+ const entries = [];
4544
+ for (const script of coverage.result) {
4545
+ const parsed2 = capture.scripts.get(script.scriptId);
4546
+ if (!parsed2) continue;
4547
+ const entry = { url: parsed2.url, hash: parsed2.hash, functions: script.functions };
4548
+ const source = await capture.sources.get(versionKey(parsed2.url, parsed2.hash));
4549
+ if (source != null) entry.source = source;
4550
+ entries.push(entry);
4551
+ }
4552
+ return entries;
4391
4553
  } catch {
4392
4554
  return null;
4555
+ } finally {
4556
+ await detach(page, cdp);
4393
4557
  }
4394
4558
  }
4395
4559
 
@@ -5258,6 +5422,106 @@ async function recordResourceCensus(testInfo) {
5258
5422
  if (ledger.leakCheck === "fail" && leaks.length > 0) throw new Error(leakFailureMessage(leaks));
5259
5423
  }
5260
5424
 
5425
+ // src/internal/capture/page-interaction.ts
5426
+ function readPageInteraction() {
5427
+ const doc = globalThis.document;
5428
+ const SKIPPED_TEXT = ["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"];
5429
+ const pathOf = (el) => {
5430
+ const levels = [];
5431
+ for (let node = el; node; ) {
5432
+ const steps = [];
5433
+ let root = null;
5434
+ for (let cur = node; cur; cur = cur.parentElement) {
5435
+ let position = 1;
5436
+ for (let s = cur.previousElementSibling; s; s = s.previousElementSibling) {
5437
+ if (s.localName === cur.localName) position++;
5438
+ }
5439
+ steps.unshift(`${cur.localName}:${position}`);
5440
+ if (!cur.parentElement) root = cur.parentNode;
5441
+ }
5442
+ levels.unshift(steps.join("/"));
5443
+ node = root && root.host ? root.host : null;
5444
+ }
5445
+ return levels.join("|");
5446
+ };
5447
+ const isField = (el) => /^(input|textarea|select)$/.test(el.localName) || !!el.isContentEditable;
5448
+ const label = (el) => {
5449
+ const tag = el.localName;
5450
+ if (el.id) return `${tag}#${el.id}`;
5451
+ for (const attr2 of ["name", "aria-label"]) {
5452
+ const value = el.getAttribute(attr2);
5453
+ if (value) return `${tag}[${attr2}="${value}"]`;
5454
+ }
5455
+ const text = isField(el) ? "" : String(el.textContent || "").replace(/\s+/g, " ").trim();
5456
+ return text ? `${tag} "${text.length > 40 ? `${text.slice(0, 40)}\u2026` : text}"` : tag;
5457
+ };
5458
+ const ref = (el) => ({ path: pathOf(el), label: label(el) });
5459
+ const textOffset = (el, stop, extra) => {
5460
+ const walker = doc.createTreeWalker(el, 4);
5461
+ let offset = 0;
5462
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
5463
+ if (stop && (text === stop || stop.contains(text) || stop.compareDocumentPosition(text) & 4)) break;
5464
+ if (!SKIPPED_TEXT.includes(text.parentElement?.tagName)) offset += text.data.length;
5465
+ }
5466
+ return offset + extra;
5467
+ };
5468
+ const textPoint2 = (node, offset) => {
5469
+ if (node.nodeType === 3)
5470
+ return { path: pathOf(node.parentElement), offset: textOffset(node.parentElement, node, offset) };
5471
+ return { path: pathOf(node), offset: textOffset(node, node.childNodes[offset] ?? null, 0) };
5472
+ };
5473
+ const out = {};
5474
+ let active = doc.activeElement;
5475
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
5476
+ if (active && active !== doc.body && active !== doc.documentElement) {
5477
+ let visible = false;
5478
+ try {
5479
+ visible = active.matches(":focus-visible");
5480
+ } catch {
5481
+ }
5482
+ out.focus = { element: ref(active), visible };
5483
+ try {
5484
+ if (typeof active.selectionStart === "number" && typeof active.selectionEnd === "number") {
5485
+ out.focus.selection = [active.selectionStart, active.selectionEnd];
5486
+ }
5487
+ } catch {
5488
+ }
5489
+ }
5490
+ let scope = doc;
5491
+ for (; ; ) {
5492
+ const chain = scope.querySelectorAll(":hover");
5493
+ const innermost = chain[chain.length - 1];
5494
+ if (!innermost) break;
5495
+ out.hover = ref(innermost);
5496
+ if (!innermost.shadowRoot) break;
5497
+ scope = innermost.shadowRoot;
5498
+ }
5499
+ const selection = doc.getSelection?.();
5500
+ if (selection && selection.rangeCount > 0 && !selection.isCollapsed && !(active && isField(active))) {
5501
+ const range = selection.getRangeAt(0);
5502
+ const start = range.startContainer.nodeType === 3 ? range.startContainer.parentElement : range.startContainer;
5503
+ const end = range.endContainer.nodeType === 3 ? range.endContainer.parentElement : range.endContainer;
5504
+ if (start?.nodeType === 1 && end?.nodeType === 1) {
5505
+ out.selection = {
5506
+ start: textPoint2(range.startContainer, range.startOffset),
5507
+ end: textPoint2(range.endContainer, range.endOffset),
5508
+ length: String(selection).length
5509
+ };
5510
+ }
5511
+ }
5512
+ const boxes = [];
5513
+ const roots = [doc];
5514
+ for (let i = 0; i < roots.length && boxes.length < 20; i++) {
5515
+ for (const el of roots[i].querySelectorAll("*")) {
5516
+ if (el.shadowRoot) roots.push(el.shadowRoot);
5517
+ if (el.localName === "input" && el.type === "checkbox" && el.indeterminate && boxes.length < 20)
5518
+ boxes.push(ref(el));
5519
+ }
5520
+ }
5521
+ if (boxes.length) out.indeterminate = boxes;
5522
+ return out;
5523
+ }
5524
+
5261
5525
  // src/internal/capture/capture-fixtures.ts
5262
5526
  function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
5263
5527
  const lastLcp = lcpEntries && lcpEntries.length > 0 ? lcpEntries[lcpEntries.length - 1] : null;
@@ -5291,6 +5555,7 @@ function computeCoreVitals(lcpEntries, shiftEntries, eventEntries) {
5291
5555
  function createSink() {
5292
5556
  return {
5293
5557
  networkRequests: [],
5558
+ openResponses: /* @__PURE__ */ new Map(),
5294
5559
  consoleEntries: [],
5295
5560
  dialogs: [],
5296
5561
  pendingHandlers: [],
@@ -5304,6 +5569,8 @@ function createSink() {
5304
5569
  stashedPageState: null,
5305
5570
  stashedAria: null,
5306
5571
  stashedAriaJson: null,
5572
+ failureScreenshot: null,
5573
+ stashedScreenshot: null,
5307
5574
  pageInventories: [],
5308
5575
  probeItem: null,
5309
5576
  probePageErrors: 0,
@@ -5314,12 +5581,13 @@ function createSink() {
5314
5581
  userPick: null,
5315
5582
  pause: null,
5316
5583
  locatorPages: new LocatorPageLog(),
5317
- codeReach: null
5584
+ codeReach: null,
5585
+ serverCoverage: /* @__PURE__ */ new Map()
5318
5586
  };
5319
5587
  }
5320
5588
  function maybeStartCodeReach(page) {
5321
5589
  const sink = currentSink;
5322
- if (!sink || process.env.PIWI_CAPTURE_CODE_REACH !== "true") return Promise.resolve(false);
5590
+ if (!sink || !browserCoverageInWorker()) return Promise.resolve(false);
5323
5591
  if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
5324
5592
  const started = startCodeReach(page);
5325
5593
  sink.codeReach = { page, started, stopped: false, files: null };
@@ -5460,7 +5728,7 @@ var PAGE_STATE_MAX_STORAGE_KEYS = 50;
5460
5728
  var PAGE_STATE_MAX_COOKIES = 30;
5461
5729
  var PAGE_STATE_HISTORY_CAP = 2048;
5462
5730
  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) {
5731
+ function buildPageState(raw, cookies, interaction) {
5464
5732
  let historyState = raw.historyState;
5465
5733
  if (historyState) {
5466
5734
  for (const re of TOKEN_MASK_RES) historyState = historyState.replace(re, "[masked]");
@@ -5486,7 +5754,8 @@ function buildPageState(raw, cookies) {
5486
5754
  secure: Boolean(c.secure),
5487
5755
  ...c.sameSite !== void 0 ? { sameSite: String(c.sameSite) } : {},
5488
5756
  ...typeof c.expires === "number" ? { expires: c.expires } : {}
5489
- }))
5757
+ })),
5758
+ ...sanitizePageInteraction(interaction)
5490
5759
  };
5491
5760
  }
5492
5761
  async function readPageState(page) {
@@ -5524,7 +5793,8 @@ async function readPageState(page) {
5524
5793
  } catch {
5525
5794
  cookies = null;
5526
5795
  }
5527
- return buildPageState(raw, cookies);
5796
+ const interaction = await page.evaluate(readPageInteraction).catch(() => null);
5797
+ return buildPageState(raw, cookies, interaction);
5528
5798
  } catch {
5529
5799
  return null;
5530
5800
  }
@@ -5616,6 +5886,8 @@ async function readPageBeforeClose(sink, page) {
5616
5886
  };
5617
5887
  if (status === "failed" || status === "timedOut" || status === "interrupted") {
5618
5888
  await sampleAria();
5889
+ if (sink.failureScreenshot)
5890
+ sink.stashedScreenshot = await readFailureScreenshot(page, sink.failureScreenshot) ?? sink.stashedScreenshot;
5619
5891
  } else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
5620
5892
  await sampleAria();
5621
5893
  }
@@ -6040,59 +6312,91 @@ function instrumentPage(page) {
6040
6312
  } catch {
6041
6313
  }
6042
6314
  }
6043
- const recordRequest = (request, failed) => {
6315
+ page.on("response", (response) => {
6044
6316
  const sink = currentSink;
6045
6317
  if (!sink) return;
6318
+ try {
6319
+ const request = response.request();
6320
+ if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
6321
+ } catch {
6322
+ }
6323
+ });
6324
+ const onRequestEnd = (request, failed) => {
6325
+ const sink = currentSink;
6326
+ if (!sink || RECORDED_OPEN.has(request)) return;
6046
6327
  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
- }
6328
+ const response = sink.openResponses.get(request) ?? null;
6329
+ sink.openResponses.delete(request);
6330
+ recordRequest(sink, request, failed ? "failed" : "finished", response);
6331
+ };
6332
+ page.on("requestfinished", (request) => onRequestEnd(request, false));
6333
+ page.on("requestfailed", (request) => onRequestEnd(request, true));
6334
+ }
6335
+ var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
6336
+ function isTrackedRequest(url, resourceType) {
6337
+ if (url.startsWith("data:") || url.startsWith("blob:")) return false;
6338
+ return TRACKED_REQUEST_TYPES.includes(resourceType);
6339
+ }
6340
+ function recordRequest(sink, request, end, response) {
6341
+ const p = (async () => {
6342
+ try {
6343
+ const url = request.url();
6344
+ const resourceType = request.resourceType();
6345
+ if (!isTrackedRequest(url, resourceType)) return;
6346
+ let failure = end === "failed" ? request.failure()?.errorText ?? "failed" : null;
6347
+ if (failure && CANCELLED_REQUEST.test(failure)) {
6348
+ if (!response) return;
6349
+ failure = null;
6350
+ end = "unfinished";
6351
+ }
6352
+ const timing = request.timing();
6353
+ if (end === "failed") response = null;
6354
+ else if (end === "finished") response ??= await request.response();
6355
+ const entry = {
6356
+ method: request.method(),
6357
+ url,
6358
+ status: response ? response.status() : 0,
6359
+ duration: requestDuration(timing, end),
6360
+ startTime: timing.startTime,
6361
+ resourceType
6362
+ };
6363
+ if (failure) entry.failure = failure;
6364
+ if (response) {
6365
+ if (timing.requestStart < 0 && typeof response.serverAddr === "function") {
6366
+ if (await response.serverAddr().catch(() => void 0) === null) entry.fulfilled = true;
6367
+ }
6368
+ const headers = response.headers();
6369
+ const contentType = headers["content-type"];
6370
+ if (contentType) entry.contentType = contentType.split(";")[0].trim();
6371
+ const logHeader = headers["x-piwi-logs"];
6372
+ if (logHeader) {
6373
+ try {
6374
+ entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
6375
+ } catch {
6079
6376
  }
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
- }
6377
+ }
6378
+ const coverageHeader = headers["x-piwi-coverage"];
6379
+ if (coverageHeader && serverCoverageInWorker()) recordServerCoverageIds(sink, url, coverageHeader);
6380
+ const traceHeader = headers["x-piwi-trace"];
6381
+ if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
6382
+ try {
6383
+ entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
6384
+ } catch {
6086
6385
  }
6087
6386
  }
6088
- sink.networkRequests.push(entry);
6089
- } catch {
6090
6387
  }
6091
- })();
6092
- sink.pendingHandlers.push(p);
6093
- };
6094
- page.on("requestfinished", (request) => recordRequest(request, false));
6095
- page.on("requestfailed", (request) => recordRequest(request, true));
6388
+ sink.networkRequests.push(entry);
6389
+ } catch {
6390
+ }
6391
+ })();
6392
+ sink.pendingHandlers.push(p);
6393
+ }
6394
+ function recordOpenRequests(sink) {
6395
+ for (const [request, response] of sink.openResponses) {
6396
+ RECORDED_OPEN.add(request);
6397
+ recordRequest(sink, request, "unfinished", response);
6398
+ }
6399
+ sink.openResponses.clear();
6096
6400
  }
6097
6401
  async function awaitFlakeConditions() {
6098
6402
  await currentSink?.flake?.conditions;
@@ -6159,12 +6463,36 @@ function readRootAriaJson(page) {
6159
6463
  return internalCall(page, () => ariaSnapshotJSONBestEffort(page.locator(":root")));
6160
6464
  }
6161
6465
  var TRACKED_REQUEST_TYPES = ["fetch", "xhr", "document", "other"];
6162
- function requestDuration(timing, failed) {
6466
+ var SERVER_COVERAGE_ID = /^[\x21-\x7e]{1,128}$/;
6467
+ function recordServerCoverageIds(sink, url, value) {
6468
+ let origin;
6469
+ try {
6470
+ const parsed2 = new URL(url);
6471
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") return;
6472
+ origin = parsed2.origin;
6473
+ } catch {
6474
+ return;
6475
+ }
6476
+ for (const id of value.split(/[\s,]+/)) {
6477
+ if (!SERVER_COVERAGE_ID.test(id)) continue;
6478
+ let ids = sink.serverCoverage.get(origin);
6479
+ if (!ids) {
6480
+ ids = /* @__PURE__ */ new Set();
6481
+ sink.serverCoverage.set(origin, ids);
6482
+ }
6483
+ ids.add(id);
6484
+ }
6485
+ }
6486
+ function requestDuration(timing, end) {
6487
+ if (end === "unfinished") {
6488
+ return timing.responseStart >= 0 ? Math.max(0, Math.round(timing.responseStart - timing.requestStart)) : 0;
6489
+ }
6163
6490
  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));
6491
+ if (end === "failed" && timing.startTime > 0) return Math.max(0, Math.round(Date.now() - timing.startTime));
6165
6492
  return 0;
6166
6493
  }
6167
6494
  async function flushSink(sink, testInfo) {
6495
+ recordOpenRequests(sink);
6168
6496
  await Promise.allSettled(sink.pendingHandlers);
6169
6497
  let drainDeadline;
6170
6498
  await Promise.race([
@@ -6193,6 +6521,14 @@ async function flushSink(sink, testInfo) {
6193
6521
  body: Buffer.from(JSON.stringify(sink.codeReach.files))
6194
6522
  });
6195
6523
  }
6524
+ if (sink.serverCoverage.size > 0) {
6525
+ const requests = {};
6526
+ for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
6527
+ await testInfo.attach(ATTACHMENT_NAMES.serverCoverageRequests, {
6528
+ contentType: "application/json",
6529
+ body: Buffer.from(JSON.stringify(requests))
6530
+ });
6531
+ }
6196
6532
  if (sink.locatorPages.size > 0) {
6197
6533
  await testInfo.attach(ATTACHMENT_NAMES.locatorPages, {
6198
6534
  contentType: "application/json",
@@ -6230,6 +6566,16 @@ async function flushSink(sink, testInfo) {
6230
6566
  } catch {
6231
6567
  }
6232
6568
  }
6569
+ if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
6570
+ const option = sink.failureScreenshot;
6571
+ const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
6572
+ if (shot) {
6573
+ try {
6574
+ await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
6575
+ } catch {
6576
+ }
6577
+ }
6578
+ }
6233
6579
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
6234
6580
  try {
6235
6581
  const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
@@ -6350,12 +6696,13 @@ var piwiFixtures = {
6350
6696
  // (attach the collected data) at teardown. Runs for every test without being
6351
6697
  // requested, so suites that never destructure `page` are still captured.
6352
6698
  piwiCapture: [
6353
- async ({}, use, testInfo) => {
6699
+ async ({ screenshot }, use, testInfo) => {
6354
6700
  boxCaptureFrames();
6355
6701
  const conflict = labModeConflict();
6356
6702
  if (conflict) throw new Error(conflict);
6357
6703
  const sink = createSink();
6358
6704
  sink.testInfo = testInfo;
6705
+ if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
6359
6706
  const pauseSet = currentPauseSet();
6360
6707
  if (pauseSet.size) {
6361
6708
  const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
@@ -6461,6 +6808,7 @@ function extendPiwiFixtures(test) {
6461
6808
  pageMapFetcher,
6462
6809
  piwiFixtures,
6463
6810
  probeElementAttrs,
6811
+ readPageInteraction,
6464
6812
  recordResourceCensus,
6465
6813
  resolveCodeReach,
6466
6814
  startCodeReach,