@piwitests/reporter 0.60.0 → 0.62.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.
@@ -42,6 +42,8 @@ __export(capture_fixtures_exports, {
42
42
  computeCoreVitals: () => computeCoreVitals,
43
43
  configuredCodeReachRoots: () => configuredCodeReachRoots,
44
44
  extendPiwiFixtures: () => extendPiwiFixtures,
45
+ failureError: () => failureError,
46
+ intentAt: () => intentAt,
45
47
  internalCall: () => internalCall,
46
48
  noteLocatorCall: () => noteLocatorCall,
47
49
  noteNavigation: () => noteNavigation,
@@ -823,14 +825,14 @@ function showAnchorPicker(arg) {
823
825
  } catch {
824
826
  shadowRoots.length = 0;
825
827
  }
826
- const within = (node2, scope) => {
828
+ const within2 = (node2, scope) => {
827
829
  if (scope === doc) return true;
828
830
  for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
829
831
  return false;
830
832
  };
831
833
  const queryAll = (scope, sel) => {
832
834
  const found = Array.from(scope.querySelectorAll(sel));
833
- for (const sr of shadowRoots) if (within(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
835
+ for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
834
836
  return found;
835
837
  };
836
838
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
@@ -911,8 +913,8 @@ function showAnchorPicker(arg) {
911
913
  return;
912
914
  }
913
915
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
914
- const outline = doc.createElement("div");
915
- outline.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
916
+ const outline2 = doc.createElement("div");
917
+ outline2.style.cssText = `position:fixed;pointer-events:none;z-index:${Z};box-sizing:border-box;border:2px solid #22c55e;background:rgba(34,197,94,.10);border-radius:4px;display:none;box-shadow:0 0 0 1px rgba(255,255,255,.9),0 0 0 3px rgba(5,46,22,.5);`;
916
918
  const outlineLabel = doc.createElement("div");
917
919
  outlineLabel.style.cssText = `position:fixed;pointer-events:none;z-index:${Z + 1};display:none;box-sizing:border-box;max-width:min(420px,90vw);background:#0b1120;color:#f9fafb;border:1px solid #22c55e;border-radius:6px;padding:3px 7px;font:12px/1.45 ${MONO};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 4px 18px rgba(0,0,0,.5);`;
918
920
  const pickedOutline = doc.createElement("div");
@@ -1001,11 +1003,11 @@ function showAnchorPicker(arg) {
1001
1003
  line.appendChild(text);
1002
1004
  line.addEventListener("mouseenter", () => {
1003
1005
  const r = row.node.getBoundingClientRect();
1004
- outline.style.display = "block";
1005
- outline.style.left = r.left + "px";
1006
- outline.style.top = r.top + "px";
1007
- outline.style.width = r.width + "px";
1008
- outline.style.height = r.height + "px";
1006
+ outline2.style.display = "block";
1007
+ outline2.style.left = r.left + "px";
1008
+ outline2.style.top = r.top + "px";
1009
+ outline2.style.width = r.width + "px";
1010
+ outline2.style.height = r.height + "px";
1009
1011
  outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
1010
1012
  outlineLabel.style.display = "block";
1011
1013
  const lr = outlineLabel.getBoundingClientRect();
@@ -1015,7 +1017,7 @@ function showAnchorPicker(arg) {
1015
1017
  outlineLabel.style.top = top + "px";
1016
1018
  });
1017
1019
  line.addEventListener("mouseleave", () => {
1018
- outline.style.display = "none";
1020
+ outline2.style.display = "none";
1019
1021
  outlineLabel.style.display = "none";
1020
1022
  });
1021
1023
  box.addEventListener("change", () => {
@@ -1029,7 +1031,7 @@ function showAnchorPicker(arg) {
1029
1031
  const cleanup = () => {
1030
1032
  doc.removeEventListener("keydown", onKey, true);
1031
1033
  panel.remove();
1032
- outline.remove();
1034
+ outline2.remove();
1033
1035
  outlineLabel.remove();
1034
1036
  pickedOutline.remove();
1035
1037
  if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
@@ -1094,7 +1096,7 @@ function showAnchorPicker(arg) {
1094
1096
  );
1095
1097
  }
1096
1098
  };
1097
- mount(pickedOutline, outline, outlineLabel, panel);
1099
+ mount(pickedOutline, outline2, outlineLabel, panel);
1098
1100
  }
1099
1101
 
1100
1102
  // ../picker-dom/src/overlay-confirm.ts
@@ -1198,7 +1200,7 @@ function showPauseBar(arg) {
1198
1200
  bar.style.cssText = `position:fixed;top:0;left:0;right:0;z-index:${Z + 4};display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:8px 12px;background:#111827;color:#f9fafb;border-bottom:2px solid #a78bfa;box-shadow:0 4px 20px rgba(0,0,0,.35);font:13px/1.5 system-ui,sans-serif;`;
1199
1201
  const where = doc.createElement("div");
1200
1202
  where.style.cssText = "font-weight:600;white-space:nowrap;";
1201
- where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
1203
+ where.textContent = `${arg.failure ? "Failed at" : "Paused at"} ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
1202
1204
  const what = doc.createElement("code");
1203
1205
  what.style.cssText = "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#d8b4fe;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;";
1204
1206
  what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
@@ -1221,13 +1223,13 @@ function showPauseBar(arg) {
1221
1223
  delete g.__piwiPauseCleanup;
1222
1224
  };
1223
1225
  const buttons = [
1224
- ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
1225
- ["Step", "step", "Pause again before the next call", false],
1226
+ arg.failure ? ["Resume", "resume", "Let the test end as it failed (Esc)", true] : ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
1227
+ ...arg.failure ? [] : [["Step", "step", "Pause again before the next call", false]],
1226
1228
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
1227
1229
  ...arg.record ? [
1228
1230
  ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
1229
1231
  ] : [],
1230
- ["Finish", "finish", "Run on and pause no more in this test", false]
1232
+ ...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]]
1231
1233
  ];
1232
1234
  for (const [label, choice, hint, primary] of buttons) {
1233
1235
  const btn = doc.createElement("button");
@@ -1678,7 +1680,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
1678
1680
  function generateAlternatives(attrs, options = {}) {
1679
1681
  const alts = [];
1680
1682
  const seen = /* @__PURE__ */ new Set();
1681
- const add = (loc) => {
1683
+ const add2 = (loc) => {
1682
1684
  if (!seen.has(loc.locator)) {
1683
1685
  seen.add(loc.locator);
1684
1686
  alts.push(loc);
@@ -1695,7 +1697,7 @@ function generateAlternatives(attrs, options = {}) {
1695
1697
  const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
1696
1698
  const testId = attr(attrs, testIdAttribute);
1697
1699
  if (testId && isUnique(counts?.testId)) {
1698
- add({
1700
+ add2({
1699
1701
  locator: `getByTestId(${quoteJsString(testId)})`,
1700
1702
  method: "getByTestId",
1701
1703
  args: { testId },
@@ -1706,7 +1708,7 @@ function generateAlternatives(attrs, options = {}) {
1706
1708
  const levelPart = level != null ? `, level: ${level}` : "";
1707
1709
  const withLevel = (base) => level != null ? { ...base, level } : base;
1708
1710
  if (role && accessibleName) {
1709
- add({
1711
+ add2({
1710
1712
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
1711
1713
  method: "getByRole",
1712
1714
  args: withLevel({ role, name: accessibleName }),
@@ -1715,7 +1717,7 @@ function generateAlternatives(attrs, options = {}) {
1715
1717
  }
1716
1718
  const ariaLabel = attr(attrs, "aria-label");
1717
1719
  if (role && ariaLabel && ariaLabel !== accessibleName) {
1718
- add({
1720
+ add2({
1719
1721
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
1720
1722
  method: "getByRole",
1721
1723
  args: withLevel({ role, name: ariaLabel }),
@@ -1726,7 +1728,7 @@ function generateAlternatives(attrs, options = {}) {
1726
1728
  const label = attr(attrs, "aria-label");
1727
1729
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
1728
1730
  if (labelBacked) {
1729
- add({
1731
+ add2({
1730
1732
  locator: `getByLabel(${quoteJsString(accessibleName)})`,
1731
1733
  method: "getByLabel",
1732
1734
  args: { label: accessibleName },
@@ -1736,7 +1738,7 @@ function generateAlternatives(attrs, options = {}) {
1736
1738
  }
1737
1739
  const placeholder = attr(attrs, "placeholder");
1738
1740
  if (placeholder) {
1739
- add({
1741
+ add2({
1740
1742
  locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
1741
1743
  method: "getByPlaceholder",
1742
1744
  args: { placeholder },
@@ -1744,7 +1746,7 @@ function generateAlternatives(attrs, options = {}) {
1744
1746
  });
1745
1747
  }
1746
1748
  if (text && text.length < 80) {
1747
- add({
1749
+ add2({
1748
1750
  locator: `getByText(${quoteJsString(text)})`,
1749
1751
  method: "getByText",
1750
1752
  args: { text },
@@ -1754,7 +1756,7 @@ function generateAlternatives(attrs, options = {}) {
1754
1756
  const id = attr(attrs, "id");
1755
1757
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1756
1758
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1757
- add({
1759
+ add2({
1758
1760
  locator: `locator(${quoteJsString(selector)})`,
1759
1761
  method: "locator",
1760
1762
  args: { selector },
@@ -1764,7 +1766,7 @@ function generateAlternatives(attrs, options = {}) {
1764
1766
  const name = attr(attrs, "name");
1765
1767
  if (name && selectable("name") && isUnique(counts?.name)) {
1766
1768
  const selector = `[name="${escCssAttrValue(name)}"]`;
1767
- add({
1769
+ add2({
1768
1770
  locator: `locator(${quoteJsString(selector)})`,
1769
1771
  method: "locator",
1770
1772
  args: { selector },
@@ -1773,7 +1775,7 @@ function generateAlternatives(attrs, options = {}) {
1773
1775
  }
1774
1776
  const alt = attr(attrs, "alt");
1775
1777
  if (alt) {
1776
- add({
1778
+ add2({
1777
1779
  locator: `getByAltText(${quoteJsString(alt)})`,
1778
1780
  method: "getByAltText",
1779
1781
  args: { text: alt },
@@ -1782,7 +1784,7 @@ function generateAlternatives(attrs, options = {}) {
1782
1784
  }
1783
1785
  const title = attr(attrs, "title");
1784
1786
  if (title) {
1785
- add({
1787
+ add2({
1786
1788
  locator: `getByTitle(${quoteJsString(title)})`,
1787
1789
  method: "getByTitle",
1788
1790
  args: { title },
@@ -1800,7 +1802,7 @@ function generateAlternatives(attrs, options = {}) {
1800
1802
  if (scopedCount(anc) !== 1) continue;
1801
1803
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
1802
1804
  testIdAnchorDone = true;
1803
- add({
1805
+ add2({
1804
1806
  locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
1805
1807
  method: leaf.method,
1806
1808
  args: { ...leaf.args, anchorTestId: anc.testId },
@@ -1810,7 +1812,7 @@ function generateAlternatives(attrs, options = {}) {
1810
1812
  if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1811
1813
  idAnchorDone = true;
1812
1814
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1813
- add({
1815
+ add2({
1814
1816
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1815
1817
  method: leaf.method,
1816
1818
  args: { ...leaf.args, anchorSelector },
@@ -1821,7 +1823,7 @@ function generateAlternatives(attrs, options = {}) {
1821
1823
  dataAnchorDone = true;
1822
1824
  const { name: name2, value } = anc.dataAttr;
1823
1825
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
1824
- add({
1826
+ add2({
1825
1827
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1826
1828
  method: leaf.method,
1827
1829
  args: { ...leaf.args, anchorSelector },
@@ -1831,7 +1833,7 @@ function generateAlternatives(attrs, options = {}) {
1831
1833
  const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
1832
1834
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
1833
1835
  roleAnchorDone = true;
1834
- add({
1836
+ add2({
1835
1837
  locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
1836
1838
  method: leaf.method,
1837
1839
  args: { ...leaf.args, anchorRole: ancestorRole },
@@ -1840,7 +1842,7 @@ function generateAlternatives(attrs, options = {}) {
1840
1842
  }
1841
1843
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
1842
1844
  filterAnchorDone = true;
1843
- add({
1845
+ add2({
1844
1846
  locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
1845
1847
  method: leaf.method,
1846
1848
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
@@ -1859,7 +1861,7 @@ function generateAlternatives(attrs, options = {}) {
1859
1861
  );
1860
1862
  const pos = attrs.rolePosition;
1861
1863
  if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
1862
- add({
1864
+ add2({
1863
1865
  locator: `getByRole(${rolePart})`,
1864
1866
  method: "getByRole",
1865
1867
  args: leafArgs,
@@ -1878,7 +1880,7 @@ function generateAlternatives(attrs, options = {}) {
1878
1880
  const countsClasses = counts?.classes;
1879
1881
  const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
1880
1882
  for (const { cls, score } of classes) {
1881
- add({
1883
+ add2({
1882
1884
  locator: `locator(${quoteJsString(`.${cls}`)})`,
1883
1885
  method: "locator",
1884
1886
  args: { selector: `.${cls}` },
@@ -1935,7 +1937,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1935
1937
  const seen = /* @__PURE__ */ new Set();
1936
1938
  const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
1937
1939
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
1938
- const add = (l) => {
1940
+ const add2 = (l) => {
1939
1941
  if (!seen.has(l.locator)) {
1940
1942
  seen.add(l.locator);
1941
1943
  out.push(l);
@@ -1945,7 +1947,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1945
1947
  if (anchor.scopedLeafCount !== 1) continue;
1946
1948
  const seg = anchorSegment(anchor);
1947
1949
  if (!seg) continue;
1948
- add({
1950
+ add2({
1949
1951
  locator: `${seg.code}.getByRole(${rolePart})`,
1950
1952
  method: "getByRole",
1951
1953
  args: { ...leafArgs, ...seg.flatArgs },
@@ -1958,7 +1960,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1958
1960
  if (segments.every((s) => s !== null)) {
1959
1961
  const innermost = segments[segments.length - 1];
1960
1962
  const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
1961
- add({
1963
+ add2({
1962
1964
  locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
1963
1965
  method: "getByRole",
1964
1966
  // Flat args describe the innermost anchor (what the healing fingerprint
@@ -2401,6 +2403,8 @@ var ATTACHMENT_NAMES = {
2401
2403
  codeReach: "piwi-code-reach",
2402
2404
  browserCoverage: "piwi-browser-coverage",
2403
2405
  browserFunctions: "piwi-browser-functions",
2406
+ cssCoverage: "piwi-css-coverage",
2407
+ cssRules: "piwi-css-rules",
2404
2408
  serverCoverageRequests: "piwi-server-coverage-requests",
2405
2409
  webVitals: "piwi-web-vitals",
2406
2410
  locatorSuggestion: "piwi-locator-suggestion",
@@ -2412,6 +2416,7 @@ var ATTACHMENT_NAMES = {
2412
2416
  pauses: "piwi-pauses"
2413
2417
  };
2414
2418
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
2419
+ var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
2415
2420
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
2416
2421
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
2417
2422
 
@@ -2718,6 +2723,7 @@ var PIWI_ENV_KEYS = {
2718
2723
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2719
2724
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
2720
2725
  coverageServer: "PIWI_COVERAGE_SERVER",
2726
+ coverageCss: "PIWI_COVERAGE_CSS",
2721
2727
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
2722
2728
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
2723
2729
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -2743,6 +2749,10 @@ var PIWI_LOCAL_DEBUG_ENV = {
2743
2749
  pauseAt: "PIWI_PAUSE_AT",
2744
2750
  editorSend: "PIWI_EDITOR_SEND"
2745
2751
  };
2752
+ var PIWI_ORIGIN_ENV = {
2753
+ kind: "PIWI_ORIGIN",
2754
+ ref: "PIWI_ORIGIN_REF"
2755
+ };
2746
2756
  var PIWI_PROBE_ENV = {
2747
2757
  flag: "PIWI_PROBE",
2748
2758
  plan: "PIWI_PROBE_PLAN",
@@ -2872,9 +2882,9 @@ function loadPlan() {
2872
2882
  function probeItemForTest(test) {
2873
2883
  const plan = loadPlan();
2874
2884
  if (!plan) return null;
2875
- const filePath = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
2885
+ const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
2876
2886
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2877
- return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2887
+ return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
2878
2888
  }
2879
2889
  function classifyProbeHandled(signals) {
2880
2890
  const blank = signals.blankPage === true;
@@ -3807,10 +3817,10 @@ function joinRoot(root, source) {
3807
3817
  function decodeLines(raw) {
3808
3818
  const sources = [];
3809
3819
  const decoded = [];
3810
- const add = (map, lineOffset, columnOffset) => {
3820
+ const add2 = (map, lineOffset, columnOffset) => {
3811
3821
  if (map.sections) {
3812
3822
  for (const section of map.sections) {
3813
- if (!add(section.map, section.offset.line, section.offset.column)) return false;
3823
+ if (!add2(section.map, section.offset.line, section.offset.column)) return false;
3814
3824
  }
3815
3825
  return true;
3816
3826
  }
@@ -3831,7 +3841,7 @@ function decodeLines(raw) {
3831
3841
  });
3832
3842
  return true;
3833
3843
  };
3834
- return add(raw, 0, 0) ? { sources, lines: decoded } : null;
3844
+ return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
3835
3845
  }
3836
3846
  function compactSegments(lines) {
3837
3847
  let total = 0;
@@ -4007,7 +4017,7 @@ function functionLines(entry, map, text, all) {
4007
4017
  const read = textOf(entry, text);
4008
4018
  const ranges = executedFunctionRanges(entry, read?.length, all);
4009
4019
  if (!ranges.length || !read) return out;
4010
- const add = (source, range) => {
4020
+ const add2 = (source, range) => {
4011
4021
  const list = out.get(source);
4012
4022
  if (list) list.push(range);
4013
4023
  else out.set(source, [range]);
@@ -4017,7 +4027,7 @@ function functionLines(entry, map, text, all) {
4017
4027
  const start = offsetToPosition(starts, range.start);
4018
4028
  if (!map) {
4019
4029
  const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
4020
- add(-1, [start.line + 1, last.line + 1]);
4030
+ add2(-1, [start.line + 1, last.line + 1]);
4021
4031
  continue;
4022
4032
  }
4023
4033
  const at = map.originalAt(start.line, start.column);
@@ -4027,7 +4037,7 @@ function functionLines(entry, map, text, all) {
4027
4037
  if (first === null) continue;
4028
4038
  const header = first === at.line || first === at.line + 1 ? at.line : first;
4029
4039
  const lines = map.originalLinesBetween(at.source, start, end, header);
4030
- if (lines) add(at.source, [header + 1, lines[1] + 1]);
4040
+ if (lines) add2(at.source, [header + 1, lines[1] + 1]);
4031
4041
  }
4032
4042
  return out;
4033
4043
  }
@@ -4063,6 +4073,258 @@ function maskUrlFragment(hash) {
4063
4073
  const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
4064
4074
  return hash.startsWith("#") ? `#${out}` : out;
4065
4075
  }
4076
+ var HIDDEN_VALUE = "<hidden>";
4077
+ var SECRET_AUTOCOMPLETE = [
4078
+ "current-password",
4079
+ "new-password",
4080
+ "one-time-code",
4081
+ "cc-number",
4082
+ "cc-csc"
4083
+ ];
4084
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
4085
+ "pass",
4086
+ "password",
4087
+ "passwd",
4088
+ "passphrase",
4089
+ "passcode",
4090
+ "pwd",
4091
+ "secret",
4092
+ "secrets",
4093
+ "token",
4094
+ "tokens",
4095
+ "jwt",
4096
+ "bearer",
4097
+ "sig",
4098
+ "signature",
4099
+ "key",
4100
+ "keys",
4101
+ "apikey",
4102
+ "auth",
4103
+ "oauth",
4104
+ "authorization",
4105
+ "session",
4106
+ "sessionid",
4107
+ "jsessionid",
4108
+ "sid",
4109
+ "cookie",
4110
+ "cookies",
4111
+ "credential",
4112
+ "credentials",
4113
+ "otp",
4114
+ "refresh",
4115
+ "csrf",
4116
+ "xsrf"
4117
+ ]);
4118
+ var SECRET_NAMES = /* @__PURE__ */ new Set(["code", "pin"]);
4119
+ function nameWords(name) {
4120
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
4121
+ }
4122
+ function isSecretName(name) {
4123
+ let secret = namesRead.get(name);
4124
+ if (secret === void 0) {
4125
+ secret = SECRET_NAMES.has(name.trim().toLowerCase()) || nameWords(name).some((word) => SECRET_WORDS.has(word));
4126
+ if (namesRead.size >= 1024) namesRead.clear();
4127
+ namesRead.set(name, secret);
4128
+ }
4129
+ return secret;
4130
+ }
4131
+ var namesRead = /* @__PURE__ */ new Map();
4132
+ var SECRET_SHAPES = [
4133
+ /\beyJ[\w-]{5,}\.[\w-]{5,}\.[\w-]*/g,
4134
+ /\bBearer(?:\s+|%20)[\w.~+/-]{8,}=*/gi,
4135
+ /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
4136
+ /\bwhsec_[A-Za-z0-9]{20,}/g,
4137
+ /\bsk-(?=[\w-]{0,200}\d)(?=[\w-]{0,200}[A-Z])[\w-]{32,}/g,
4138
+ /\bglpat-[\w-]{20,}/g,
4139
+ /\bnpm_[A-Za-z0-9]{36}\b/g,
4140
+ /\bSK[0-9a-f]{32}\b/g,
4141
+ /\bAKIA[0-9A-Z]{16}\b/g,
4142
+ /\bAIza[\w-]{30,}/g,
4143
+ /\bgh[opsu]_[A-Za-z0-9]{20,}/g,
4144
+ /\bgithub_pat_\w{20,}/g,
4145
+ /\bxox[abprs]-[\w-]{8,}/g
4146
+ ];
4147
+ var WHOLE_SHAPES = SECRET_SHAPES.map(
4148
+ (re) => new RegExp(`^(?:${re.source})$`, re.flags.replace("g", ""))
4149
+ );
4150
+ var BASIC = /\bBasic\s+([A-Za-z0-9+/]{4,}={0,2})(?![\w+/=])/g;
4151
+ var PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]{0,40}PRIVATE KEY)-----/g;
4152
+ var SET_COOKIE = /(\bSet-Cookie:[ \t]*[^=;\s]{1,256}=)([^;\r\n]{1,4096})/gi;
4153
+ var COOKIE_LINE = /((?<![\w-])Cookie:[ \t]*)([^\r\n]{1,16384})/gi;
4154
+ var JSON_FIELD = /("((?:[^"\\\r\n]|\\.){1,256})"\s*:\s*")((?:[^"\\\r\n]|\\.){1,4096})"/g;
4155
+ var TOKEN_RUN = /[A-Za-z0-9+/_-]{40,}={0,2}/g;
4156
+ var MIN_RANDOM = 40;
4157
+ var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4158
+ function base64OfText(run) {
4159
+ let bits = 0;
4160
+ let acc = 0;
4161
+ let bytes = 0;
4162
+ let printable = 0;
4163
+ for (const c of run.replace(/=+$/, "")) {
4164
+ const digit = BASE64_DIGITS.indexOf(c === "-" ? "+" : c === "_" ? "/" : c);
4165
+ if (digit < 0) return false;
4166
+ acc = (acc << 6 | digit) & 65535;
4167
+ bits += 6;
4168
+ if (bits < 8) continue;
4169
+ bits -= 8;
4170
+ const byte = acc >> bits & 255;
4171
+ bytes++;
4172
+ if (byte >= 32 && byte < 127 || byte === 9 || byte === 10 || byte === 13) printable++;
4173
+ }
4174
+ return bytes > 0 && printable * 10 >= bytes * 9;
4175
+ }
4176
+ function bitsPerChar(text) {
4177
+ const counts = /* @__PURE__ */ new Map();
4178
+ for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
4179
+ let bits = 0;
4180
+ for (const n of counts.values()) bits -= n / text.length * Math.log2(n / text.length);
4181
+ return bits;
4182
+ }
4183
+ function looksRandom(text) {
4184
+ if (text.length < MIN_RANDOM || !/[A-Z]/.test(text) || !/[a-z]/.test(text) || !/\d/.test(text)) return false;
4185
+ let letters = 0;
4186
+ for (const c of text) if (c >= "a" && c <= "z" || c >= "A" && c <= "Z") letters++;
4187
+ let wordy = 0;
4188
+ for (const run of text.matchAll(/[a-z]{6,}|[A-Z]{6,}/g)) wordy += run[0].length;
4189
+ return wordy * 5 <= letters && bitsPerChar(text) >= 4 && !base64OfText(text);
4190
+ }
4191
+ function isBasicCredential(encoded) {
4192
+ const text = encoded.replace(/=+$/, "");
4193
+ if (text.length % 4 === 1) return false;
4194
+ let decoded = "";
4195
+ let bits = 0;
4196
+ let acc = 0;
4197
+ for (const c of text) {
4198
+ const digit = BASE64_DIGITS.indexOf(c);
4199
+ if (digit < 0) return false;
4200
+ acc = (acc << 6 | digit) & 65535;
4201
+ bits += 6;
4202
+ if (bits < 8) continue;
4203
+ bits -= 8;
4204
+ decoded += String.fromCharCode(acc >> bits & 255);
4205
+ }
4206
+ return /^[\x20-\x7e]+:[\x20-\x7e]*$/.test(decoded);
4207
+ }
4208
+ function isSecretValue(value) {
4209
+ const text = value.trim();
4210
+ if (text.length < 10 || text.length > 4096) return false;
4211
+ if (WHOLE_SHAPES.some((re) => re.test(text))) return true;
4212
+ const basic = /^Basic\s+([A-Za-z0-9+/]{4,}={0,2})$/.exec(text);
4213
+ if (basic && isBasicCredential(basic[1])) return true;
4214
+ if (/^-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/.test(text)) return true;
4215
+ return /^[A-Za-z0-9+/_-]{40,}={0,2}$/.test(text) && looksRandom(text);
4216
+ }
4217
+ var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s"'<>`]+/gi;
4218
+ var URL_PARTS = /^(?:([a-z][a-z0-9+.-]*:)?\/\/([^/?#]*))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?$/i;
4219
+ function decodeSafe(text) {
4220
+ try {
4221
+ return decodeURIComponent(text);
4222
+ } catch {
4223
+ return text;
4224
+ }
4225
+ }
4226
+ function hider(count) {
4227
+ return () => {
4228
+ count.n++;
4229
+ return HIDDEN_VALUE;
4230
+ };
4231
+ }
4232
+ function hideParams(params, count) {
4233
+ const hide = hider(count);
4234
+ return params.split(/([&;])/).map((part, i) => {
4235
+ const eq = part.indexOf("=");
4236
+ if (i % 2 === 1 || eq < 0) return part;
4237
+ const name = part.slice(0, eq);
4238
+ const value = part.slice(eq + 1);
4239
+ const decoded = decodeSafe(value);
4240
+ if (!value || decoded === HIDDEN_VALUE) return part;
4241
+ return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
4242
+ }).join("");
4243
+ }
4244
+ function hidePath(path15, count) {
4245
+ const hide = hider(count);
4246
+ return path15.split("/").map((segment) => {
4247
+ const [own = "", ...params] = segment.split(";");
4248
+ const decoded = decodeSafe(own);
4249
+ const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
4250
+ return params.length ? `${kept};${hideParams(params.join(";"), count)}` : kept;
4251
+ }).join("/");
4252
+ }
4253
+ function hideSecretsInUrl(url, count = { n: 0 }) {
4254
+ const parts = URL_PARTS.exec(url);
4255
+ if (!parts) return url;
4256
+ const [, scheme = "", authority, path15 = "", query = "", fragment = ""] = parts;
4257
+ let out = "";
4258
+ if (authority !== void 0) {
4259
+ const at = authority.lastIndexOf("@");
4260
+ const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
4261
+ out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
4262
+ }
4263
+ out += hidePath(path15, count);
4264
+ if (query) out += `?${hideParams(query.slice(1), count)}`;
4265
+ if (fragment) {
4266
+ const body = fragment.slice(1);
4267
+ const ask = body.indexOf("?");
4268
+ out += `#${ask >= 0 ? `${hidePath(body.slice(0, ask), count)}?${hideParams(body.slice(ask + 1), count)}` : body.includes("=") ? hideParams(body, count) : hidePath(body, count)}`;
4269
+ }
4270
+ return out;
4271
+ }
4272
+ var QUERY_PAIR = /([?&;#])([^=&#?;\s"'<>]{1,256})=([^&;#\s"'<>]*)/g;
4273
+ var URL_PASSWORD = /(\/\/[^/:@\s"'<>]{1,256}:)([^@/\s"'<>]{1,256})(@)/g;
4274
+ function hidePem(text, hide) {
4275
+ let out = "";
4276
+ let last = 0;
4277
+ let nextEnd = -2;
4278
+ PEM_BEGIN.lastIndex = 0;
4279
+ for (let begin = PEM_BEGIN.exec(text); begin; begin = PEM_BEGIN.exec(text)) {
4280
+ if (begin.index < last) continue;
4281
+ if (nextEnd !== -1 && nextEnd < begin.index) nextEnd = text.indexOf("-----END ", begin.index);
4282
+ const footer = nextEnd >= 0 ? /^-----END [A-Z0-9 ]{0,40}-----/.exec(text.slice(nextEnd, nextEnd + 60)) : null;
4283
+ const until = footer ? nextEnd + footer[0].length : begin.index + begin[0].length + (/^(?:\s*[A-Za-z0-9+/=]+)*/.exec(text.slice(begin.index + begin[0].length))?.[0].length ?? 0);
4284
+ out += `${text.slice(last, begin.index)}${hide()}`;
4285
+ last = until;
4286
+ PEM_BEGIN.lastIndex = until;
4287
+ }
4288
+ return out + text.slice(last);
4289
+ }
4290
+ function hideInText(text, count) {
4291
+ if (text.length < 4 || !/[A-Za-z0-9]/.test(text)) return text;
4292
+ const hide = hider(count);
4293
+ let out = hidePem(text, hide);
4294
+ out = out.replace(QUERY_PAIR, (all, sep9, name, value) => {
4295
+ const decoded = decodeSafe(value);
4296
+ if (!value || decoded === HIDDEN_VALUE) return all;
4297
+ return isSecretName(name) || isSecretValue(decoded) ? `${sep9}${name}=${hide()}` : all;
4298
+ });
4299
+ out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
4300
+ out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
4301
+ out = out.replace(COOKIE_LINE, (_all, before, pairs) => {
4302
+ const values = pairs.replace(
4303
+ /([^=;\s]{1,256}=)([^;]*)/g,
4304
+ (pair, name, value) => value.trim() && value.trim() !== HIDDEN_VALUE ? `${name}${hide()}` : pair
4305
+ );
4306
+ return `${before}${values}`;
4307
+ });
4308
+ out = out.replace(
4309
+ JSON_FIELD,
4310
+ (all, before, name, value) => isSecretName(name) && value !== HIDDEN_VALUE ? `${before}${hide()}"` : all
4311
+ );
4312
+ out = out.replace(BASIC, (all, encoded) => isBasicCredential(encoded) ? hide() : all);
4313
+ for (const shape of SECRET_SHAPES) out = out.replace(shape, () => hide());
4314
+ return out.replace(
4315
+ TOKEN_RUN,
4316
+ (run) => run.includes("/") && !/[+=]/.test(run) ? run.split("/").map((part) => looksRandom(part) ? hide() : part).join("/") : looksRandom(run) ? hide() : run
4317
+ );
4318
+ }
4319
+ function hideSecretsInText(text, count = { n: 0 }) {
4320
+ let out = "";
4321
+ let last = 0;
4322
+ for (const url of text.matchAll(URL_IN_TEXT)) {
4323
+ out += hideInText(text.slice(last, url.index), count) + hideSecretsInUrl(url[0], count);
4324
+ last = url.index + url[0].length;
4325
+ }
4326
+ return out + hideInText(text.slice(last), count);
4327
+ }
4066
4328
 
4067
4329
  // src/internal/capture/inspect-on-failure.ts
4068
4330
  function isCi(ci) {
@@ -4318,6 +4580,7 @@ var path7 = __toESM(require("path"));
4318
4580
  // src/internal/support/reporter-version.ts
4319
4581
  var fs5 = __toESM(require("fs"));
4320
4582
  var path6 = __toESM(require("path"));
4583
+ var cachedVersion = null;
4321
4584
  var CANDIDATE_OFFSETS = ["..", "../../.."];
4322
4585
  function findOwnPackageJson(fromDir) {
4323
4586
  for (const offset of CANDIDATE_OFFSETS) {
@@ -4332,6 +4595,11 @@ function findOwnPackageJson(fromDir) {
4332
4595
  }
4333
4596
  return void 0;
4334
4597
  }
4598
+ function getReporterVersion() {
4599
+ if (cachedVersion) return cachedVersion;
4600
+ cachedVersion = findOwnPackageJson(__dirname)?.version ?? "unknown";
4601
+ return cachedVersion;
4602
+ }
4335
4603
 
4336
4604
  // src/internal/capture/quiet-capture.ts
4337
4605
  var BOXED_PREFIX_EXPORTS = [
@@ -4457,11 +4725,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
4457
4725
  return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
4458
4726
  }
4459
4727
  var parsed = null;
4728
+ var sent = null;
4460
4729
  function currentPauseSet() {
4730
+ if (sent) return sent;
4461
4731
  const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
4462
4732
  if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
4463
4733
  return parsed.set;
4464
4734
  }
4735
+ function replacePauseSet(places, cwd = process.cwd()) {
4736
+ if (!places) {
4737
+ sent = null;
4738
+ return currentPauseSet();
4739
+ }
4740
+ const lines = new Set(places.map((place) => placeLine(place, cwd)));
4741
+ sent = lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
4742
+ return sent;
4743
+ }
4744
+ function callSitePlace(callSite) {
4745
+ const m = /^(.+):(\d+):(\d+)$/.exec(callSite);
4746
+ if (!m || Number(m[2]) < 1) return null;
4747
+ const column = Number(m[3]);
4748
+ return column > 0 ? { file: m[1], line: Number(m[2]), column } : { file: m[1], line: Number(m[2]) };
4749
+ }
4750
+ function placeLine(place, cwd = process.cwd()) {
4751
+ return `${callSiteFile(place.file, cwd)}:${place.line}`;
4752
+ }
4465
4753
  function pauseGateFromTestInfo(testInfo) {
4466
4754
  const use = testInfo.project?.use ?? {};
4467
4755
  const debug = testInfo._configInternal?.configCLIOverrides?.debug;
@@ -4469,7 +4757,7 @@ function pauseGateFromTestInfo(testInfo) {
4469
4757
  }
4470
4758
  function pauseSkipReason(gate) {
4471
4759
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
4472
- if (gate.headless !== false)
4760
+ if (gate.headless !== false && !gate.channel)
4473
4761
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
4474
4762
  if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
4475
4763
  return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
@@ -4477,20 +4765,42 @@ function pauseSkipReason(gate) {
4477
4765
  return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
4478
4766
  return null;
4479
4767
  }
4768
+ function pauseState(set, pause = false) {
4769
+ return { set, step: false, finished: false, pause, runTo: null };
4770
+ }
4771
+ function wantsStops(state) {
4772
+ return state.step || state.pause || state.runTo !== null || !state.finished && state.set.size > 0;
4773
+ }
4774
+ function atBreakpoint(state, callSite) {
4775
+ return !state.finished && state.set.matches(callSite);
4776
+ }
4480
4777
  function atStop(state, callSite) {
4481
- if (callSite && !state.finished) {
4482
- if (state.step) {
4483
- state.step = false;
4484
- return { show: "step" };
4778
+ if (callSite) {
4779
+ const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && callSiteLine(callSite) === state.runTo ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
4780
+ if (reason) {
4781
+ stopShown(state);
4782
+ return { show: reason };
4485
4783
  }
4486
- if (state.set.matches(callSite)) return { show: "breakpoint" };
4487
4784
  }
4488
4785
  return { go: goOn(state) };
4489
4786
  }
4787
+ function stopShown(state) {
4788
+ state.step = false;
4789
+ state.pause = false;
4790
+ state.runTo = null;
4791
+ }
4792
+ function askedOnce(state) {
4793
+ return { step: state.step, pause: state.pause, runTo: state.runTo };
4794
+ }
4795
+ function askAgain(state, asked) {
4796
+ state.step ||= asked.step;
4797
+ state.pause ||= asked.pause;
4798
+ state.runTo ??= asked.runTo;
4799
+ }
4490
4800
  function goOn(state, answer) {
4491
4801
  if (answer === "step") state.step = true;
4492
4802
  else if (answer === "finish") state.finished = true;
4493
- return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
4803
+ return wantsStops(state) ? "next" : "resume";
4494
4804
  }
4495
4805
  function contextDebugger(context) {
4496
4806
  const dbg = context.debugger;
@@ -4626,38 +4936,83 @@ function logPauseSkipOnce(reason) {
4626
4936
  var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
4627
4937
  var MAX_NAVIGATED = 10;
4628
4938
  var HIGHLIGHT_MS = 2e3;
4629
- async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4630
- let shown = null;
4939
+ var BarIntents = class {
4940
+ constructor() {
4941
+ this.take = null;
4942
+ this.queued = null;
4943
+ this.busy = false;
4944
+ }
4945
+ /** Hand the bar an intent; false while it runs one, or holds one already. */
4946
+ offer(intent) {
4947
+ if (this.busy || this.queued) return false;
4948
+ const take = this.take;
4949
+ this.take = null;
4950
+ if (take) take(intent);
4951
+ else this.queued = intent;
4952
+ return true;
4953
+ }
4954
+ /** The bar waits for its answer: settles with the next intent handed to it. */
4955
+ wait() {
4956
+ this.busy = false;
4957
+ const queued = this.queued;
4958
+ this.queued = null;
4959
+ if (queued) return Promise.resolve(queued);
4960
+ return new Promise((resolve5) => {
4961
+ this.take = resolve5;
4962
+ });
4963
+ }
4964
+ /** The bar got its answer: it takes no intent until it waits again. */
4965
+ answered() {
4966
+ this.take = null;
4967
+ this.busy = true;
4968
+ }
4969
+ };
4970
+ var TAKE_DOWN_MS = 1e3;
4971
+ async function within(run, ms) {
4972
+ let timer;
4973
+ try {
4974
+ return await Promise.race([
4975
+ run,
4976
+ new Promise((resolve5) => timer = setTimeout(() => resolve5(void 0), ms))
4977
+ ]);
4978
+ } finally {
4979
+ clearTimeout(timer);
4980
+ }
4981
+ }
4982
+ async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
4983
+ let shown2 = null;
4631
4984
  try {
4632
4985
  const highlight = point.target?.highlight;
4633
4986
  if (typeof highlight === "function") {
4634
- let timer;
4635
- shown = await Promise.race([
4636
- Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
4637
- new Promise((resolve5) => {
4638
- timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
4639
- })
4640
- ]);
4641
- clearTimeout(timer);
4987
+ const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
4988
+ shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
4642
4989
  }
4643
4990
  const arg = {
4644
4991
  place: pausePlace(point.location),
4645
4992
  attempt: point.attempt,
4646
4993
  action: point.call,
4647
4994
  locator: null,
4648
- record: !!onRecord
4995
+ record: !!onRecord,
4996
+ ...point.failure ? { failure: true } : {}
4649
4997
  };
4650
4998
  console.log(
4651
- `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
4999
+ point.failure ? `[piwi] The test failed at ${callSiteLine(point.location)}: Resume, Pick a locator${onRecord ? " or Record here" : ""} in the browser (Esc resumes, and the test ends as it failed).` : `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
4652
5000
  );
4653
5001
  let navigated = 0;
4654
5002
  for (; ; ) {
4655
5003
  try {
4656
5004
  if (ended?.aborted) return "resume";
4657
- await page.evaluate(showPauseBar, arg);
4658
- await unlessEnded(page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }), ended);
5005
+ await unlessEnded(page.evaluate(showPauseBar, arg), ended);
5006
+ const answered = page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }).then(() => null);
5007
+ const intent = await unlessEnded(intents ? Promise.race([answered, intents.wait()]) : answered, ended);
5008
+ intents?.answered();
5009
+ if (ended?.aborted) return "resume";
5010
+ const taken = await unlessEnded(page.evaluate(takePauseState), ended);
5011
+ if (ended?.aborted) return "resume";
5012
+ const choice = taken ?? intent ?? null;
5013
+ const raised = intent && choice === intent ? page.bringToFront().catch(() => void 0) : null;
5014
+ if (raised) await unlessEnded(raised, ended);
4659
5015
  if (ended?.aborted) return "resume";
4660
- const choice = await page.evaluate(takePauseState);
4661
5016
  navigated = 0;
4662
5017
  if (choice === "resume" || choice === "step" || choice === "finish") return choice;
4663
5018
  if (choice === "record" && onRecord) {
@@ -4667,26 +5022,285 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4667
5022
  );
4668
5023
  continue;
4669
5024
  }
5025
+ if ((choice === "assert" || choice === "assert-region") && onIntent) {
5026
+ await unlessEnded(
5027
+ onIntent(choice, ended).catch(() => void 0),
5028
+ ended
5029
+ );
5030
+ continue;
5031
+ }
4670
5032
  if (choice !== "pick") continue;
4671
5033
  const picked = await pickLocator(page, probe, null, ended).catch(() => null);
4672
5034
  if (picked && !ended?.aborted) await onPick(picked);
4673
5035
  } catch (error) {
4674
5036
  if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
4675
- await page.waitForLoadState("domcontentloaded").catch(() => void 0);
5037
+ await unlessEnded(
5038
+ page.waitForLoadState("domcontentloaded").catch(() => void 0),
5039
+ ended
5040
+ );
4676
5041
  }
4677
5042
  }
4678
5043
  } catch {
4679
5044
  return "resume";
4680
5045
  } finally {
5046
+ const takeDown = async () => {
5047
+ try {
5048
+ if (ended?.aborted) await page.evaluate(takePauseState);
5049
+ const dispose = shown2?.dispose;
5050
+ const hide = page.hideHighlight;
5051
+ if (typeof dispose === "function") await dispose.call(shown2);
5052
+ else if (typeof hide === "function") await hide.call(page);
5053
+ } catch {
5054
+ }
5055
+ };
5056
+ await within(takeDown(), TAKE_DOWN_MS);
5057
+ }
5058
+ }
5059
+
5060
+ // src/internal/capture/chain-locator.ts
5061
+ function argValue(page, arg) {
5062
+ switch (arg.type) {
5063
+ case "string":
5064
+ case "number":
5065
+ case "boolean":
5066
+ return arg.value;
5067
+ case "regex":
5068
+ return new RegExp(arg.source, arg.flags);
5069
+ case "object":
5070
+ return Object.fromEntries(arg.entries.map(([key, value]) => [key, argValue(page, value)]));
5071
+ case "chain":
5072
+ return chainOn(page, arg.chain);
5073
+ }
5074
+ }
5075
+ function chainOn(page, chain) {
5076
+ let current = page;
5077
+ for (const call of chain.calls) {
5078
+ if (typeof current[call.method] !== "function") {
5079
+ throw new Error(`${call.method}() cannot be called there.`);
5080
+ }
5081
+ current = callMethod(
5082
+ current,
5083
+ call.method,
5084
+ call.args.map((arg) => argValue(page, arg))
5085
+ );
5086
+ }
5087
+ if (!current || typeof current.count !== "function") {
5088
+ throw new Error("The chain does not end on a locator.");
5089
+ }
5090
+ return current;
5091
+ }
5092
+ function locatorOn(page, source) {
5093
+ const chain = tryParseLocatorChain(source);
5094
+ if (!chain?.calls.length) return null;
5095
+ try {
5096
+ return chainOn(page, chain);
5097
+ } catch {
5098
+ return null;
5099
+ }
5100
+ }
5101
+
5102
+ // src/internal/capture/region-snapshot.ts
5103
+ function regionSnapshotWithoutSecrets(snapshot, secrets) {
5104
+ const hidden = new Set(secrets.filter(Boolean));
5105
+ const kept = [];
5106
+ for (const line of snapshot.split("\n")) {
5107
+ const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
5108
+ if (box && hidden.size && hidden.has(yamlScalar(box[2]))) {
5109
+ kept.push({ line: box[1], lostChild: false });
5110
+ continue;
5111
+ }
5112
+ const url = /^\s*- \/url: (.*)$/.exec(line);
5113
+ if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
5114
+ let parent = kept.length - 1;
5115
+ while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
5116
+ if (parent >= 0) kept[parent].lostChild = true;
5117
+ continue;
5118
+ }
5119
+ kept.push({ line, lostChild: false });
5120
+ }
5121
+ return kept.map(({ line, lostChild }, i) => {
5122
+ const next = kept[i + 1];
5123
+ const childless = !next || indentOf(next.line) <= indentOf(line);
5124
+ return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
5125
+ }).join("\n");
5126
+ }
5127
+ function indentOf(line) {
5128
+ return line.length - line.trimStart().length;
5129
+ }
5130
+ function yamlScalar(text) {
5131
+ const value = text.trim();
5132
+ if (value.startsWith('"')) {
4681
5133
  try {
4682
- if (ended?.aborted) await page.evaluate(takePauseState);
4683
- const dispose = shown?.dispose;
4684
- const hide = page.hideHighlight;
4685
- if (typeof dispose === "function") await dispose.call(shown);
4686
- else if (typeof hide === "function") await hide.call(page);
5134
+ return String(JSON.parse(value));
4687
5135
  } catch {
5136
+ return value;
4688
5137
  }
4689
5138
  }
5139
+ if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
5140
+ return value;
5141
+ }
5142
+
5143
+ // ../core/src/run-channel.ts
5144
+ var RUN_CHANNEL_ENV = "PIWI_RUN_CHANNEL";
5145
+ var RUN_HOLDS = ["failure"];
5146
+ var RUN_INTENTS = ["pick", "record", "assert", "assert-region"];
5147
+ var RUN_TAKES = [...RUN_INTENTS, "requests", "mock", "locator"];
5148
+ var RUN_INTENT_NAMES = {
5149
+ pick: "Pick a locator",
5150
+ record: "Record here",
5151
+ assert: "Assert",
5152
+ "assert-region": "Assert a region"
5153
+ };
5154
+ var MAX_KEPT_RESPONSES = 50;
5155
+ var MAX_KEPT_BODY = 256 * 1024;
5156
+ var MAX_REGION_SNAPSHOT = 1024 * 1024;
5157
+ var MAX_STOP_ERROR = 1e3;
5158
+ function isObject2(value) {
5159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5160
+ }
5161
+ function readJson(line) {
5162
+ try {
5163
+ const value = JSON.parse(line);
5164
+ return isObject2(value) ? value : null;
5165
+ } catch {
5166
+ return null;
5167
+ }
5168
+ }
5169
+ function isInt(value, min) {
5170
+ return typeof value === "number" && Number.isInteger(value) && value >= min;
5171
+ }
5172
+ function filePath(value) {
5173
+ return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
5174
+ }
5175
+ function readPlace(value) {
5176
+ if (!isObject2(value) || !filePath(value.file) || !isInt(value.line, 1)) return null;
5177
+ if (value.column === void 0) return { file: value.file, line: value.line };
5178
+ return isInt(value.column, 1) ? { file: value.file, line: value.line, column: value.column } : null;
5179
+ }
5180
+ function isIntent(value) {
5181
+ return RUN_INTENTS.includes(value);
5182
+ }
5183
+ function readHolds(value) {
5184
+ if (!Array.isArray(value)) return void 0;
5185
+ return value.length > 16 ? [] : RUN_HOLDS.filter((hold) => value.includes(hold));
5186
+ }
5187
+ function isLocatorText(value) {
5188
+ return typeof value === "string" && value.length > 0 && value.length <= 1e3 && !/[\r\n\u2028\u2029]/.test(value);
5189
+ }
5190
+ var MAX_PLACES = 1e4;
5191
+ function parseHubToRun(line) {
5192
+ const value = readJson(line);
5193
+ if (!value) return null;
5194
+ const unknown = (what) => ({ type: "unknown", what: what.slice(0, 64) });
5195
+ switch (value.type) {
5196
+ case "attached": {
5197
+ const holds = readHolds(value.holds);
5198
+ return holds ? { type: "attached", holds } : null;
5199
+ }
5200
+ case "breakpoints": {
5201
+ if (!Array.isArray(value.places) || value.places.length > MAX_PLACES) return null;
5202
+ const places = value.places.map(readPlace);
5203
+ return places.every((p) => p !== null) ? { type: "breakpoints", places } : null;
5204
+ }
5205
+ case "control": {
5206
+ const { action, stop } = value;
5207
+ if (action === "pause") return { type: "control", action };
5208
+ if (action === "step" || action === "resume" || action === "finish") {
5209
+ return isInt(stop, 1) ? { type: "control", action, stop } : null;
5210
+ }
5211
+ return typeof action === "string" ? unknown(`control ${action}`) : null;
5212
+ }
5213
+ case "run-to": {
5214
+ const place = readPlace(value.place);
5215
+ return isInt(value.stop, 1) && place ? { type: "run-to", stop: value.stop, place } : null;
5216
+ }
5217
+ case "request": {
5218
+ const { request, what, stop } = value;
5219
+ if (!isInt(request, 1)) return null;
5220
+ if (what === "image") return { type: "request", request, what: "image" };
5221
+ if (typeof what !== "string") return null;
5222
+ if (!isIntent(what) && what !== "requests" && what !== "mock" && what !== "locator") {
5223
+ return unknown(`request ${what}`);
5224
+ }
5225
+ if (!isInt(stop, 1)) return null;
5226
+ if (isIntent(what) || what === "requests") return { type: "request", request, what, stop };
5227
+ if (what === "mock") return isInt(value.id, 1) ? { type: "request", request, what, stop, id: value.id } : null;
5228
+ const { locator } = value;
5229
+ return locator === null || isLocatorText(locator) ? { type: "request", request, what, stop, locator } : null;
5230
+ }
5231
+ default:
5232
+ return typeof value.type === "string" && value.type ? unknown(value.type) : null;
5233
+ }
5234
+ }
5235
+
5236
+ // src/internal/capture/kept-responses.ts
5237
+ var keeping = null;
5238
+ var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
5239
+ function mimeOf(contentType) {
5240
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
5241
+ }
5242
+ function textOf2(body) {
5243
+ try {
5244
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
5245
+ return text.includes("\0") ? null : text;
5246
+ } catch {
5247
+ return null;
5248
+ }
5249
+ }
5250
+ function startKeeping(now = Date.now()) {
5251
+ keeping = { startedAt: now, kept: [], last: 0 };
5252
+ }
5253
+ function stopKeeping() {
5254
+ keeping = null;
5255
+ }
5256
+ async function keepResponse(request, response) {
5257
+ const into = keeping;
5258
+ if (!into || !response) return;
5259
+ try {
5260
+ const kind = request.resourceType();
5261
+ if (kind !== "fetch" && kind !== "xhr") return;
5262
+ const url = hideSecretsInUrl(request.url());
5263
+ const method = request.method().toUpperCase();
5264
+ if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
5265
+ const headers = response.headers();
5266
+ const type = mimeOf(headers["content-type"]);
5267
+ if (type && !TEXT_TYPE.test(type)) return;
5268
+ const status = response.status();
5269
+ if (status < 100 || status > 599) return;
5270
+ const time = Math.max(0, Math.round(Date.now() - into.startedAt));
5271
+ const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
5272
+ if (size !== null && size > MAX_KEPT_BODY) return;
5273
+ into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
5274
+ if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
5275
+ } catch {
5276
+ }
5277
+ }
5278
+ function keptRequests() {
5279
+ return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
5280
+ id,
5281
+ method,
5282
+ url,
5283
+ status,
5284
+ type,
5285
+ size,
5286
+ time
5287
+ }));
5288
+ }
5289
+ async function keptResponse(id) {
5290
+ const found = keeping?.kept.find((kept) => kept.id === id);
5291
+ if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
5292
+ let body;
5293
+ try {
5294
+ body = await internalCall(found.response, () => found.response.body());
5295
+ } catch {
5296
+ return {
5297
+ refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
5298
+ };
5299
+ }
5300
+ if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
5301
+ const text = textOf2(body);
5302
+ if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
5303
+ return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
4690
5304
  }
4691
5305
 
4692
5306
  // src/internal/capture/record-handoff.ts
@@ -4707,7 +5321,7 @@ function parseRecordChannel(value) {
4707
5321
  const path15 = text.slice(0, hash);
4708
5322
  return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
4709
5323
  }
4710
- function isObject2(value) {
5324
+ function isObject3(value) {
4711
5325
  return typeof value === "object" && value !== null && !Array.isArray(value);
4712
5326
  }
4713
5327
  function parseHandoffToTest(line) {
@@ -4717,7 +5331,7 @@ function parseHandoffToTest(line) {
4717
5331
  } catch {
4718
5332
  return null;
4719
5333
  }
4720
- if (!isObject2(value)) return null;
5334
+ if (!isObject3(value)) return null;
4721
5335
  switch (value.type) {
4722
5336
  case "refused":
4723
5337
  return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
@@ -4780,8 +5394,8 @@ async function recordFromHere(options) {
4780
5394
  const use = testInfo.project?.use ?? {};
4781
5395
  const context = page.context();
4782
5396
  return new Promise((resolve5) => {
4783
- const socket = net.connect(channel.path);
4784
- socket.setEncoding("utf8");
5397
+ const socket2 = net.connect(channel.path);
5398
+ socket2.setEncoding("utf8");
4785
5399
  const answers = /* @__PURE__ */ new Map();
4786
5400
  let calls = 0;
4787
5401
  let attached = false;
@@ -4789,7 +5403,7 @@ async function recordFromHere(options) {
4789
5403
  let settled = false;
4790
5404
  let initScript;
4791
5405
  const send = (message) => {
4792
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
5406
+ if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
4793
5407
  `);
4794
5408
  };
4795
5409
  const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
@@ -4839,7 +5453,7 @@ async function recordFromHere(options) {
4839
5453
  page.off("close", onGone);
4840
5454
  page.off("crash", onGone);
4841
5455
  ended?.removeEventListener("abort", onEnded);
4842
- socket.end();
5456
+ socket2.end();
4843
5457
  resolve5(recording ? null : said);
4844
5458
  };
4845
5459
  const relayed = (source, payload) => new Promise((answer) => {
@@ -4879,7 +5493,7 @@ async function recordFromHere(options) {
4879
5493
  context.on("page", onPage);
4880
5494
  send({ type: "loaded" });
4881
5495
  };
4882
- socket.on("connect", () => {
5496
+ socket2.on("connect", () => {
4883
5497
  attached = true;
4884
5498
  send({
4885
5499
  type: "attach",
@@ -4888,10 +5502,11 @@ async function recordFromHere(options) {
4888
5502
  line: place.line,
4889
5503
  reason: options.reason,
4890
5504
  testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
4891
- baseURL: typeof use.baseURL === "string" ? use.baseURL : null
5505
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
5506
+ ...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
4892
5507
  });
4893
5508
  });
4894
- socket.on(
5509
+ socket2.on(
4895
5510
  "data",
4896
5511
  lineReader(
4897
5512
  (line) => {
@@ -4924,16 +5539,263 @@ async function recordFromHere(options) {
4924
5539
  () => finish("the editor sent a message too long to read.")
4925
5540
  )
4926
5541
  );
4927
- socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
4928
- socket.on("close", () => finish("the editor closed the recording."));
5542
+ socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
5543
+ socket2.on("close", () => finish("the editor closed the recording."));
4929
5544
  });
4930
5545
  }
4931
5546
 
5547
+ // src/internal/capture/run-channel.ts
5548
+ var net2 = __toESM(require("net"));
5549
+ var CONNECT_MS = 2e3;
5550
+ var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
5551
+ var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
5552
+ var WENT_ON = "The test went on.";
5553
+ var socket = null;
5554
+ var control = null;
5555
+ var shown = /* @__PURE__ */ new Set();
5556
+ var stops = 0;
5557
+ var held = null;
5558
+ var outline = null;
5559
+ var counting = Promise.resolve();
5560
+ async function openRunChannel(worker, handlers, env = process.env) {
5561
+ const channel = parseRecordChannel(env[RUN_CHANNEL_ENV]);
5562
+ const ref = env[PIWI_ORIGIN_ENV.ref];
5563
+ if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
5564
+ const opening = net2.connect(channel.path);
5565
+ opening.setEncoding("utf8");
5566
+ const opened = await new Promise((resolve5) => {
5567
+ const timer = setTimeout(() => resolve5(false), CONNECT_MS);
5568
+ opening.once("connect", () => {
5569
+ clearTimeout(timer);
5570
+ resolve5(true);
5571
+ });
5572
+ opening.once("error", () => {
5573
+ clearTimeout(timer);
5574
+ resolve5(false);
5575
+ });
5576
+ });
5577
+ if (!opened || opening.destroyed) {
5578
+ opening.destroy();
5579
+ return false;
5580
+ }
5581
+ socket = opening;
5582
+ control = handlers;
5583
+ shown = /* @__PURE__ */ new Set();
5584
+ opening.unref();
5585
+ opening.on(
5586
+ "data",
5587
+ lineReader(
5588
+ (line) => onMessage(parseHubToRun(line)),
5589
+ () => opening.destroy()
5590
+ )
5591
+ );
5592
+ opening.on("error", () => opening.destroy());
5593
+ opening.on("close", () => {
5594
+ if (socket === opening) socket = null;
5595
+ if (!held || held.bar) return;
5596
+ console.log("[piwi] The connection to the editor closed: the test goes on.");
5597
+ held.answer("resume");
5598
+ });
5599
+ tellHub({
5600
+ type: "run-attach",
5601
+ token: channel.token,
5602
+ ref,
5603
+ cwd: process.cwd(),
5604
+ workerIndex: worker.workerIndex,
5605
+ parallelIndex: worker.parallelIndex,
5606
+ pid: process.pid,
5607
+ playwright: worker.config.version,
5608
+ reporter: getReporterVersion(),
5609
+ takes: [...RUN_TAKES],
5610
+ holds: [...RUN_HOLDS]
5611
+ });
5612
+ return true;
5613
+ }
5614
+ function editorShows(what) {
5615
+ return runChannelOpen() && shown.has(what);
5616
+ }
5617
+ var HIDDEN_ENV = [RUN_CHANNEL_ENV, PIWI_LOCAL_DEBUG_ENV.editorSend];
5618
+ var hiddenEditorSend;
5619
+ function hideRunChannel(env = process.env) {
5620
+ const hidden = HIDDEN_ENV.flatMap((name) => {
5621
+ const value = env[name];
5622
+ return value === void 0 ? [] : [{ name, value }];
5623
+ });
5624
+ for (const { name } of hidden) delete env[name];
5625
+ hiddenEditorSend = hidden.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
5626
+ return () => {
5627
+ for (const { name, value } of hidden) env[name] = value;
5628
+ hiddenEditorSend = void 0;
5629
+ };
5630
+ }
5631
+ function editorSendAddress(env = process.env) {
5632
+ return env[PIWI_LOCAL_DEBUG_ENV.editorSend] ?? hiddenEditorSend;
5633
+ }
5634
+ function closeRunChannel() {
5635
+ socket?.end();
5636
+ socket = null;
5637
+ control = null;
5638
+ }
5639
+ function runChannelOpen() {
5640
+ return !!socket && !socket.destroyed;
5641
+ }
5642
+ function tellHub(message) {
5643
+ if (socket && !socket.destroyed) socket.write(`${JSON.stringify(message)}
5644
+ `);
5645
+ }
5646
+ function holdStop(stop, onAnswer) {
5647
+ const id = ++stops;
5648
+ let answered = false;
5649
+ const holding = {
5650
+ stop: id,
5651
+ page: stop.page,
5652
+ bar: stop.bar,
5653
+ answer: (choice) => {
5654
+ if (answered) return;
5655
+ answered = true;
5656
+ onAnswer(choice);
5657
+ },
5658
+ intent: (what) => answered ? { ok: false, message: WENT_ON } : stop.intent?.(what) ?? {
5659
+ ok: false,
5660
+ message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
5661
+ }
5662
+ };
5663
+ held = holding;
5664
+ tellHub({
5665
+ type: "stopped",
5666
+ stop: id,
5667
+ reason: stop.reason,
5668
+ title: stop.title,
5669
+ place: stop.place,
5670
+ locator: stop.locator,
5671
+ bar: stop.bar,
5672
+ ...stop.error ? { error: stop.error } : {}
5673
+ });
5674
+ if (!runChannelOpen() && !stop.bar) holding.answer("resume");
5675
+ return {
5676
+ stop: id,
5677
+ release: async () => {
5678
+ answered = true;
5679
+ if (held === holding) held = null;
5680
+ await counting;
5681
+ if (outline?.stop === id) await removeOutline();
5682
+ tellHub({ type: "running", stop: id });
5683
+ }
5684
+ };
5685
+ }
5686
+ async function removeOutline() {
5687
+ const shown2 = outline;
5688
+ outline = null;
5689
+ await shown2?.remove().catch(() => void 0);
5690
+ }
5691
+ async function countLocator(holding, source) {
5692
+ await removeOutline();
5693
+ if (source === null) return { ok: true, message: "The outline is gone.", count: null };
5694
+ const page = holding.page;
5695
+ if (!page || page.isClosed()) return { ok: false, message: "This stop has no page to count on.", count: null };
5696
+ const locator = locatorOn(page, source);
5697
+ if (!locator) return { ok: false, message: `${source} is not a locator Piwi can build.`, count: null };
5698
+ try {
5699
+ const count = await internalCall(page, () => locator.count());
5700
+ if (holding.bar && count > 0 && held === holding) {
5701
+ const shown2 = await internalCall(page, () => locator.highlight({ style: OUTLINE_STYLE }));
5702
+ const dispose = shown2?.dispose;
5703
+ outline = {
5704
+ stop: holding.stop,
5705
+ remove: () => internalCall(page, () => typeof dispose === "function" ? dispose.call(shown2) : page.hideHighlight())
5706
+ };
5707
+ }
5708
+ return {
5709
+ ok: true,
5710
+ message: count === 0 ? "No element matches." : count === 1 ? "1 element matches." : `${count} elements match.`,
5711
+ count
5712
+ };
5713
+ } catch (error) {
5714
+ return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
5715
+ }
5716
+ }
5717
+ function onMessage(message) {
5718
+ if (!message) return;
5719
+ switch (message.type) {
5720
+ case "attached":
5721
+ shown = new Set(message.holds);
5722
+ return;
5723
+ case "breakpoints":
5724
+ control?.breakpoints(message.places);
5725
+ return;
5726
+ case "control":
5727
+ if (message.action === "pause") {
5728
+ if (!held) control?.pause();
5729
+ } else if (held?.stop === message.stop) held.answer(message.action);
5730
+ return;
5731
+ case "run-to":
5732
+ if (held?.stop !== message.stop) return;
5733
+ control?.runTo(message.place);
5734
+ held.answer("resume");
5735
+ return;
5736
+ case "request": {
5737
+ const { request } = message;
5738
+ const at = message.what !== "image" && held?.stop === message.stop ? held : null;
5739
+ switch (message.what) {
5740
+ case "image":
5741
+ void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request, what: "image", value }));
5742
+ return;
5743
+ case "requests": {
5744
+ const requests = at ? keptRequests() : [];
5745
+ const said = !at ? WENT_ON : requests.length ? `The test kept ${requests.length === 1 ? "1 response" : `${requests.length} responses`}.` : "The test kept no response yet: Piwi keeps the text answers of its fetch and XHR requests.";
5746
+ tellHub({ type: "answer", request, what: "requests", value: { ok: !!at, message: said, requests } });
5747
+ return;
5748
+ }
5749
+ case "mock": {
5750
+ void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
5751
+ const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
5752
+ tellHub({ type: "answer", request, what: "mock", value });
5753
+ });
5754
+ return;
5755
+ }
5756
+ case "locator": {
5757
+ const counted = at ? counting.then(
5758
+ () => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
5759
+ ) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
5760
+ counting = counted.catch(() => void 0);
5761
+ void counted.then((value) => tellHub({ type: "answer", request, what: "locator", value }));
5762
+ return;
5763
+ }
5764
+ default: {
5765
+ const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
5766
+ tellHub({ type: "answer", request, what: message.what, value });
5767
+ return;
5768
+ }
5769
+ }
5770
+ }
5771
+ case "unknown":
5772
+ tellHub({
5773
+ type: "notice",
5774
+ message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
5775
+ });
5776
+ }
5777
+ }
5778
+ async function imageOf(page) {
5779
+ if (!page || page.isClosed()) return null;
5780
+ try {
5781
+ const shot = await internalCall(
5782
+ page,
5783
+ () => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN, timeout: 5e3 })
5784
+ );
5785
+ return { mime: "image/jpeg", data: shot.toString("base64") };
5786
+ } catch {
5787
+ return null;
5788
+ }
5789
+ }
5790
+
4932
5791
  // src/internal/collect/coverage-settings.ts
4933
5792
  var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
4934
5793
  function isServerGranularity(value) {
4935
5794
  return typeof value === "string" && SERVER_VALUES.has(value);
4936
5795
  }
5796
+ function cssCoverageInWorker() {
5797
+ return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
5798
+ }
4937
5799
  function browserCoverageInWorker() {
4938
5800
  const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
4939
5801
  return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || browser === "files" || browser === "functions";
@@ -4947,7 +5809,7 @@ function browserFunctionCoverage(files) {
4947
5809
  files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
4948
5810
  });
4949
5811
  }
4950
- function newKnownFunctions(known, sent) {
5812
+ function newKnownFunctions(known, sent2) {
4951
5813
  const parts = [];
4952
5814
  let files = [];
4953
5815
  let budget = MAX_COVERAGE_RANGES;
@@ -4959,10 +5821,10 @@ function newKnownFunctions(known, sent) {
4959
5821
  for (const [raw, ranges] of known) {
4960
5822
  const path15 = normalizeCoveragePath(raw);
4961
5823
  if (path15 === null) continue;
4962
- let named = sent.get(path15);
5824
+ let named = sent2.get(path15);
4963
5825
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4964
5826
  if (fresh.length === 0) continue;
4965
- if (!named) sent.set(path15, named = /* @__PURE__ */ new Set());
5827
+ if (!named) sent2.set(path15, named = /* @__PURE__ */ new Set());
4966
5828
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4967
5829
  while (fresh.length > 0) {
4968
5830
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
@@ -4985,15 +5847,15 @@ var os3 = __toESM(require("os"));
4985
5847
  var fs7 = __toESM(require("fs"));
4986
5848
 
4987
5849
  // src/internal/support/instance-id.ts
4988
- var crypto = __toESM(require("crypto"));
5850
+ var crypto2 = __toESM(require("crypto"));
4989
5851
  var os2 = __toESM(require("os"));
4990
5852
  function hashForProject(projectName) {
4991
- return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
5853
+ return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
4992
5854
  }
4993
5855
 
4994
5856
  // src/internal/support/aria-sampling.ts
4995
- function ariaSampleIdentity(filePath, title) {
4996
- return `${filePath}\0${title}`;
5857
+ function ariaSampleIdentity(filePath2, title) {
5858
+ return `${filePath2}\0${title}`;
4997
5859
  }
4998
5860
  function getAriaSampleFilePath(projectName) {
4999
5861
  return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
@@ -5025,7 +5887,7 @@ function isDueForAriaSample(testInfo) {
5025
5887
  }
5026
5888
 
5027
5889
  // src/internal/capture/code-reach.ts
5028
- var crypto2 = __toESM(require("crypto"));
5890
+ var crypto3 = __toESM(require("crypto"));
5029
5891
  var fs8 = __toESM(require("fs"));
5030
5892
  var path11 = __toESM(require("path"));
5031
5893
  var import_node_child_process = require("child_process");
@@ -5113,9 +5975,9 @@ function resolveSourcePath(source, roots) {
5113
5975
  const normalized = normalizeSourcePath(source);
5114
5976
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
5115
5977
  if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
5116
- const relative7 = normalized.replace(/^\/+/, "");
5978
+ const relative8 = normalized.replace(/^\/+/, "");
5117
5979
  for (const root of roots) {
5118
- const candidate = path11.resolve(root, relative7);
5980
+ const candidate = path11.resolve(root, relative8);
5119
5981
  if (isFile(candidate)) return candidate;
5120
5982
  }
5121
5983
  return null;
@@ -5150,7 +6012,7 @@ function relativeToRepo(file, repoRoot) {
5150
6012
  return path11.relative(repoRoot, file).split(path11.sep).join("/");
5151
6013
  }
5152
6014
  function scriptVersion(entry) {
5153
- return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
6015
+ return entry.hash ?? crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
5154
6016
  }
5155
6017
  async function loadMap(entry, fetchMap) {
5156
6018
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -5193,7 +6055,7 @@ async function resolveScript(entry, roots, bases, fetchMap) {
5193
6055
  if (text) scriptCache.set(entry.url, version, resolution);
5194
6056
  return resolution;
5195
6057
  }
5196
- function textOf2(entry, resolution) {
6058
+ function textOf3(entry, resolution) {
5197
6059
  return resolution.text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
5198
6060
  }
5199
6061
  async function resolveCodeReach(entries, roots, fetchMap, pageUrl) {
@@ -5215,7 +6077,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5215
6077
  if (listed) listed.push(...functions);
5216
6078
  else into.set(path15, [...functions]);
5217
6079
  };
5218
- const add = (file, functions) => addTo(files, file, functions);
6080
+ const add2 = (file, functions) => addTo(files, file, functions);
5219
6081
  const bases = viteBases(entries);
5220
6082
  const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
5221
6083
  const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
@@ -5239,17 +6101,17 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5239
6101
  };
5240
6102
  if (resolution.kind === "file") {
5241
6103
  if (!resolution.file) continue;
5242
- const text = textOf2(covered, resolution);
6104
+ const text = textOf3(covered, resolution);
5243
6105
  if (resolution.lookup) {
5244
6106
  const reached = reachedFunctionLines(covered, resolution.lookup, text);
5245
- if (reached.size > 0) add(resolution.file, [...reached.values()].flat());
6107
+ if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
5246
6108
  } else if (scriptRan(covered, text)) {
5247
- add(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
6109
+ add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
5248
6110
  }
5249
6111
  } else if (resolution.kind === "map") {
5250
- for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
6112
+ for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf3(covered, resolution))) {
5251
6113
  const file = mapFile(resolution, index);
5252
- if (file) add(file, functions);
6114
+ if (file) add2(file, functions);
5253
6115
  }
5254
6116
  }
5255
6117
  if (!withKnown || resolution.kind === "none") continue;
@@ -5257,7 +6119,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5257
6119
  let listed = knownCache.get(covered.url, version);
5258
6120
  if (!listed || listed.count < covered.functions.length) {
5259
6121
  const byFile = /* @__PURE__ */ new Map();
5260
- const text = textOf2(covered, resolution);
6122
+ const text = textOf3(covered, resolution);
5261
6123
  if (resolution.kind === "file" && resolution.file) {
5262
6124
  const lines = knownFunctionLines(covered, resolution.lookup, text);
5263
6125
  const all = resolution.lookup ? [...lines.values()].flat() : lines.get(-1) ?? [];
@@ -5328,10 +6190,13 @@ async function markCurrentDocument(page, cdp) {
5328
6190
  }
5329
6191
  function onPaused(page, capture, event) {
5330
6192
  const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
5331
- const read = leaving ? readCoverage(page, capture.cdp) : null;
6193
+ const read = leaving ? Promise.all([
6194
+ capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
6195
+ capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
6196
+ ]) : null;
5332
6197
  if (read) {
5333
6198
  const merging = read.then(
5334
- (coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
6199
+ ([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
5335
6200
  );
5336
6201
  capture.reading.add(merging);
5337
6202
  void merging.finally(() => capture.reading.delete(merging));
@@ -5345,7 +6210,7 @@ function versionKey(url, hash) {
5345
6210
  return `${hash} ${url}`;
5346
6211
  }
5347
6212
  function onScriptParsed(page, capture, event) {
5348
- if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
6213
+ if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
5349
6214
  capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
5350
6215
  if (NEVER_REACHED.test(event.url)) return;
5351
6216
  const key = versionKey(event.url, event.hash);
@@ -5374,6 +6239,53 @@ function readCoverage(page, cdp) {
5374
6239
  );
5375
6240
  return Promise.race([taken, late]).finally(() => clearTimeout(timer));
5376
6241
  }
6242
+ function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
6243
+ if (header.isConstructed) return;
6244
+ const sheet = {
6245
+ sourceURL: header.sourceURL,
6246
+ ...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
6247
+ isInline: header.isInline,
6248
+ startLine: header.startLine,
6249
+ pageUrl,
6250
+ used: /* @__PURE__ */ new Map()
6251
+ };
6252
+ css.sheets.set(header.styleSheetId, sheet);
6253
+ const reading = (async () => {
6254
+ try {
6255
+ if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
6256
+ const { node } = await internalCall(
6257
+ page,
6258
+ () => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
6259
+ );
6260
+ const attributes = node.attributes ?? [];
6261
+ const at = attributes.indexOf("data-vite-dev-id");
6262
+ if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
6263
+ }
6264
+ if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
6265
+ const { text } = await internalCall(
6266
+ page,
6267
+ () => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
6268
+ );
6269
+ sheet.text = text;
6270
+ } catch {
6271
+ }
6272
+ })();
6273
+ css.reading.add(reading);
6274
+ void reading.finally(() => css.reading.delete(reading));
6275
+ }
6276
+ async function readCssUsage(page, cdp, css) {
6277
+ try {
6278
+ const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
6279
+ for (const rule of coverage) {
6280
+ if (!rule.used) continue;
6281
+ css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
6282
+ startOffset: rule.startOffset,
6283
+ endOffset: rule.endOffset
6284
+ });
6285
+ }
6286
+ } catch {
6287
+ }
6288
+ }
5377
6289
  function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
5378
6290
  for (const covered of read) {
5379
6291
  const script = scripts.get(covered.scriptId);
@@ -5408,11 +6320,15 @@ async function detach(page, cdp) {
5408
6320
  }
5409
6321
  async function startCodeReach(page) {
5410
6322
  if (!isChromiumPage(page) || captures.has(page)) return false;
6323
+ const withCss = cssCoverageInWorker();
6324
+ const js = browserCoverageInWorker() || !withCss;
5411
6325
  let cdp = null;
5412
6326
  try {
5413
6327
  cdp = await internalCall(page, () => page.context().newCDPSession(page));
5414
6328
  const capture = {
5415
6329
  cdp,
6330
+ js,
6331
+ css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
5416
6332
  scripts: /* @__PURE__ */ new Map(),
5417
6333
  sources: /* @__PURE__ */ new Map(),
5418
6334
  ran: /* @__PURE__ */ new Map(),
@@ -5422,6 +6338,9 @@ async function startCodeReach(page) {
5422
6338
  };
5423
6339
  const marks = marksOf(page);
5424
6340
  cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
6341
+ const css = capture.css;
6342
+ if (css)
6343
+ cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
5425
6344
  cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
5426
6345
  cdp.on("Page.frameNavigated", (event) => {
5427
6346
  if (event.frame.parentId) return;
@@ -5432,8 +6351,17 @@ async function startCodeReach(page) {
5432
6351
  page,
5433
6352
  () => Promise.all([
5434
6353
  capture.cdp.send("Debugger.enable"),
5435
- capture.cdp.send("Profiler.enable"),
5436
- capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false }),
6354
+ ...js ? [
6355
+ capture.cdp.send("Profiler.enable"),
6356
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
6357
+ ] : [],
6358
+ // The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
6359
+ // scripts' coverage running.
6360
+ ...css ? [
6361
+ capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
6362
+ capture.css = null;
6363
+ })
6364
+ ] : [],
5437
6365
  capture.cdp.send("Page.enable"),
5438
6366
  capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
5439
6367
  capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
@@ -5448,17 +6376,28 @@ async function startCodeReach(page) {
5448
6376
  }
5449
6377
  }
5450
6378
  async function stopCodeReach(page) {
6379
+ return (await stopPageCoverage(page)).scripts;
6380
+ }
6381
+ async function stopPageCoverage(page) {
5451
6382
  const capture = captures.get(page);
5452
- if (!capture) return null;
6383
+ if (!capture) return { scripts: null, sheets: [] };
5453
6384
  captures.delete(page);
5454
6385
  const { cdp } = capture;
5455
- try {
6386
+ const sheets = async () => {
6387
+ if (!capture.css) return [];
6388
+ await readCssUsage(page, cdp, capture.css);
6389
+ await Promise.all(capture.css.reading);
6390
+ await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
6391
+ });
6392
+ return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
6393
+ };
6394
+ const scripts = async () => {
6395
+ if (!capture.js) return null;
5456
6396
  const last = await internalCall(page, async () => {
5457
6397
  const taken = await cdp.send("Profiler.takePreciseCoverage");
5458
6398
  await cdp.send("Profiler.stopPreciseCoverage");
5459
6399
  return taken.result;
5460
6400
  });
5461
- await Promise.all(capture.reading);
5462
6401
  mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
5463
6402
  const entries = [];
5464
6403
  for (const [key, { script, functions }] of capture.ran) {
@@ -5468,15 +6407,281 @@ async function stopCodeReach(page) {
5468
6407
  entries.push(entry);
5469
6408
  }
5470
6409
  return entries;
5471
- } catch {
5472
- return null;
6410
+ };
6411
+ try {
6412
+ await Promise.all(capture.reading);
6413
+ const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
6414
+ return { scripts: read, sheets: styles };
5473
6415
  } finally {
5474
6416
  await detach(page, cdp);
5475
6417
  }
5476
6418
  }
5477
6419
 
6420
+ // src/internal/capture/css-coverage.ts
6421
+ var crypto4 = __toESM(require("crypto"));
6422
+ var fs9 = __toESM(require("fs"));
6423
+
6424
+ // ../core/src/css-rules.ts
6425
+ var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
6426
+ function parseCssRules(text) {
6427
+ const rules = [];
6428
+ const open = [];
6429
+ let preludeStart = -1;
6430
+ for (let i = 0; i < text.length; i++) {
6431
+ const c = text[i];
6432
+ if (c === "/" && text[i + 1] === "*") {
6433
+ const end = text.indexOf("*/", i + 2);
6434
+ i = end < 0 ? text.length : end + 1;
6435
+ continue;
6436
+ }
6437
+ if (c === '"' || c === "'") {
6438
+ if (preludeStart < 0) preludeStart = i;
6439
+ for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
6440
+ continue;
6441
+ }
6442
+ if (c === "\\") {
6443
+ if (preludeStart < 0) preludeStart = i;
6444
+ i++;
6445
+ continue;
6446
+ }
6447
+ if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
6448
+ let j = i + 4;
6449
+ while (j < text.length && /\s/.test(text[j])) j++;
6450
+ if (text[j] !== '"' && text[j] !== "'") {
6451
+ if (preludeStart < 0) preludeStart = i;
6452
+ for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
6453
+ i = j;
6454
+ continue;
6455
+ }
6456
+ }
6457
+ if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
6458
+ i += text.startsWith("<!--", i) ? 3 : 2;
6459
+ continue;
6460
+ }
6461
+ if (c === "{") {
6462
+ const start = preludeStart < 0 ? i : preludeStart;
6463
+ const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
6464
+ const parent = open[open.length - 1];
6465
+ const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
6466
+ open.push({ kind, start, open: i });
6467
+ preludeStart = -1;
6468
+ continue;
6469
+ }
6470
+ if (c === "}") {
6471
+ const block = open.pop();
6472
+ if (block && block.kind !== "other")
6473
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
6474
+ preludeStart = -1;
6475
+ continue;
6476
+ }
6477
+ if (c === ";") {
6478
+ preludeStart = -1;
6479
+ continue;
6480
+ }
6481
+ if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
6482
+ }
6483
+ for (const block of open) {
6484
+ if (block.kind !== "other")
6485
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
6486
+ }
6487
+ return rules.sort((a, b) => a.start - b.start);
6488
+ }
6489
+ function usedCssRules(rules, used) {
6490
+ const styleAt = /* @__PURE__ */ new Map();
6491
+ const groups = [];
6492
+ for (const rule of rules) {
6493
+ if (rule.kind === "style") styleAt.set(rule.start, rule);
6494
+ else groups.push(rule);
6495
+ }
6496
+ const out = /* @__PURE__ */ new Map();
6497
+ for (const range of used) {
6498
+ const style = styleAt.get(range.startOffset);
6499
+ if (style) {
6500
+ out.set(`${style.start}`, [style.start, style.end]);
6501
+ continue;
6502
+ }
6503
+ const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
6504
+ if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
6505
+ }
6506
+ return [...out.values()].sort((a, b) => a[0] - b[0]);
6507
+ }
6508
+ function allCssRules(rules) {
6509
+ return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
6510
+ }
6511
+ function cssRuleLines(text, offsets, options = {}) {
6512
+ const starts = lineStarts(text);
6513
+ const lineOffset = options.lineOffset ?? 0;
6514
+ const out = /* @__PURE__ */ new Map();
6515
+ const add2 = (source, range) => {
6516
+ const ranges = out.get(source);
6517
+ if (ranges) ranges.push(range);
6518
+ else out.set(source, [range]);
6519
+ };
6520
+ for (const [startOffset, endOffset] of offsets) {
6521
+ const start = offsetToPosition(starts, startOffset);
6522
+ const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
6523
+ if (!options.lookup) {
6524
+ add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
6525
+ continue;
6526
+ }
6527
+ const first = options.lookup.originalAt(start.line, start.column);
6528
+ if (!first) continue;
6529
+ const last = options.lookup.originalAt(end.line, end.column);
6530
+ const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
6531
+ add2(first.source, [first.line + 1, lastLine + 1]);
6532
+ }
6533
+ return out;
6534
+ }
6535
+
6536
+ // src/internal/capture/css-coverage.ts
6537
+ var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
6538
+ var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
6539
+ var resolutions = /* @__PURE__ */ new Map();
6540
+ var MAX_CACHED_SHEETS = 2e3;
6541
+ function fileForStyleUrl(url, roots) {
6542
+ let pathname;
6543
+ try {
6544
+ const parsed2 = new URL(url);
6545
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
6546
+ pathname = parsed2.pathname;
6547
+ } catch {
6548
+ return null;
6549
+ }
6550
+ return sourceFile(decodeURIComponent(pathname), roots);
6551
+ }
6552
+ function sourceFile(source, roots) {
6553
+ const bare = source.replace(/[?#].*$/, "");
6554
+ if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
6555
+ const file = resolveSourcePath(bare, roots);
6556
+ if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
6557
+ return file;
6558
+ }
6559
+ function styleBlockLine(file, index) {
6560
+ let text;
6561
+ try {
6562
+ text = fs9.readFileSync(file, "utf-8");
6563
+ } catch {
6564
+ return 0;
6565
+ }
6566
+ const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
6567
+ const block = blocks[index];
6568
+ if (!block) return 0;
6569
+ return text.slice(0, block.index + block[0].length).split("\n").length - 1;
6570
+ }
6571
+ async function sheetMap(sheet, text, fetchMap) {
6572
+ const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
6573
+ if (!ref) return null;
6574
+ let raw;
6575
+ if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
6576
+ else {
6577
+ try {
6578
+ raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
6579
+ } catch {
6580
+ raw = null;
6581
+ }
6582
+ }
6583
+ if (!raw) return null;
6584
+ try {
6585
+ return decodeSourceMap(JSON.parse(raw));
6586
+ } catch {
6587
+ return null;
6588
+ }
6589
+ }
6590
+ function sameLines(text, rules, file, lineOffset) {
6591
+ let source;
6592
+ try {
6593
+ source = fs9.readFileSync(file, "utf-8");
6594
+ } catch {
6595
+ return false;
6596
+ }
6597
+ const fileLines = source.split(/\r?\n/);
6598
+ const sheetLineStarts = [0];
6599
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
6600
+ let low = 0;
6601
+ for (const rule of rules) {
6602
+ while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
6603
+ const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
6604
+ if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
6605
+ }
6606
+ return true;
6607
+ }
6608
+ async function resolveSheet(sheet, text, dirs, fetchMap) {
6609
+ const lookup = await sheetMap(sheet, text, fetchMap);
6610
+ const rules = parseCssRules(text);
6611
+ if (lookup) {
6612
+ const sources = lookup.sources.map((source) => {
6613
+ const direct = sourceFile(source, dirs);
6614
+ if (direct) return direct;
6615
+ try {
6616
+ return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
6617
+ } catch {
6618
+ return null;
6619
+ }
6620
+ });
6621
+ return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
6622
+ }
6623
+ if (sheet.devId) {
6624
+ const file2 = sourceFile(sheet.devId, dirs);
6625
+ if (!file2) return null;
6626
+ const index = /[?&]index=(\d+)/.exec(sheet.devId);
6627
+ const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
6628
+ return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
6629
+ }
6630
+ const file = fileForStyleUrl(sheet.sourceURL, dirs);
6631
+ if (!file) return null;
6632
+ const lineOffset = sheet.isInline ? sheet.startLine : 0;
6633
+ return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
6634
+ }
6635
+ function cachedResolution(sheet, text, dirs, fetchMap) {
6636
+ const version = crypto4.createHash("sha1").update(text).digest("hex");
6637
+ const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
6638
+ " "
6639
+ );
6640
+ let pending = resolutions.get(key);
6641
+ if (!pending) {
6642
+ pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
6643
+ resolutions.set(key, pending);
6644
+ if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
6645
+ }
6646
+ return pending;
6647
+ }
6648
+ function add(into, file, ranges, repoRoot, fileLevel = false) {
6649
+ if (ranges.length === 0 && !fileLevel) return;
6650
+ const key = relativeToRepo(file, repoRoot);
6651
+ into.set(key, [...into.get(key) ?? [], ...ranges]);
6652
+ }
6653
+ async function resolveCssCoverage(sheets, roots, fetchMap) {
6654
+ const reached = /* @__PURE__ */ new Map();
6655
+ const known = /* @__PURE__ */ new Map();
6656
+ for (const sheet of sheets) {
6657
+ if (sheet.text === void 0) continue;
6658
+ const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
6659
+ const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
6660
+ if (!resolution) continue;
6661
+ const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
6662
+ if (!resolution.exact) {
6663
+ if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
6664
+ continue;
6665
+ }
6666
+ const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
6667
+ const fileOf = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
6668
+ const matched = cssRuleLines(sheet.text, used, options);
6669
+ for (const [source, ranges] of matched) {
6670
+ const file = fileOf(source);
6671
+ if (file) add(reached, file, ranges, roots.repoRoot);
6672
+ }
6673
+ for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
6674
+ const file = fileOf(source);
6675
+ if (file) add(known, file, ranges, roots.repoRoot);
6676
+ }
6677
+ }
6678
+ return { reached, known };
6679
+ }
6680
+
5478
6681
  // src/internal/capture/resource-ledger.ts
5479
- var fs10 = __toESM(require("fs"));
6682
+ var childProcess = __toESM(require("child_process"));
6683
+ var fs11 = __toESM(require("fs"));
6684
+ var net3 = __toESM(require("net"));
5480
6685
  var path13 = __toESM(require("path"));
5481
6686
  var import_node_module2 = require("module");
5482
6687
 
@@ -5484,12 +6689,12 @@ var import_node_module2 = require("module");
5484
6689
  var import_node_perf_hooks = require("perf_hooks");
5485
6690
 
5486
6691
  // src/internal/support/system-readers.ts
5487
- var fs9 = __toESM(require("fs"));
6692
+ var fs10 = __toESM(require("fs"));
5488
6693
  var path12 = __toESM(require("path"));
5489
6694
  var CLK_TCK = 100;
5490
6695
  function readText(file) {
5491
6696
  try {
5492
- return fs9.readFileSync(file, "utf8");
6697
+ return fs10.readFileSync(file, "utf8");
5493
6698
  } catch {
5494
6699
  return null;
5495
6700
  }
@@ -5571,14 +6776,14 @@ var ProcFs = class {
5571
6776
  }
5572
6777
  /** Whether this looks like a Linux `/proc` at all. */
5573
6778
  available() {
5574
- return fs9.existsSync(path12.join(this.root, "stat"));
6779
+ return fs10.existsSync(path12.join(this.root, "stat"));
5575
6780
  }
5576
6781
  /** Every process, keyed by pid. */
5577
6782
  table() {
5578
6783
  const out = /* @__PURE__ */ new Map();
5579
6784
  let names;
5580
6785
  try {
5581
- names = fs9.readdirSync(this.root);
6786
+ names = fs10.readdirSync(this.root);
5582
6787
  } catch {
5583
6788
  return out;
5584
6789
  }
@@ -5609,7 +6814,7 @@ var ProcFs = class {
5609
6814
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5610
6815
  async pssKb(pid) {
5611
6816
  try {
5612
- return parseKbField(await fs9.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
6817
+ return parseKbField(await fs10.promises.readFile(path12.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5613
6818
  } catch {
5614
6819
  return null;
5615
6820
  }
@@ -5617,7 +6822,7 @@ var ProcFs = class {
5617
6822
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5618
6823
  resetPeakRss(pid) {
5619
6824
  try {
5620
- fs9.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
6825
+ fs10.writeFileSync(path12.join(this.root, String(pid), "clear_refs"), "5");
5621
6826
  return true;
5622
6827
  } catch {
5623
6828
  return false;
@@ -5639,7 +6844,7 @@ var ProcFs = class {
5639
6844
  /** Open file descriptors of a process. */
5640
6845
  fdCount(pid) {
5641
6846
  try {
5642
- return fs9.readdirSync(path12.join(this.root, String(pid), "fd")).length;
6847
+ return fs10.readdirSync(path12.join(this.root, String(pid), "fd")).length;
5643
6848
  } catch {
5644
6849
  return null;
5645
6850
  }
@@ -5656,6 +6861,8 @@ var WorkerMetrics = class {
5656
6861
  constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
5657
6862
  this.proc = proc;
5658
6863
  this.pid = pid;
6864
+ /** The lowest heap read right after a full garbage collection in the current window. */
6865
+ this.heapAfterGc = null;
5659
6866
  this.loop = null;
5660
6867
  this.usage = null;
5661
6868
  /** Each process's CPU and wait at the start of the current window; null before the first. */
@@ -5670,6 +6877,28 @@ var WorkerMetrics = class {
5670
6877
  histogram = null;
5671
6878
  }
5672
6879
  this.histogram = histogram;
6880
+ this.gcObserver = this.observeFullCollections();
6881
+ }
6882
+ /**
6883
+ * Read the heap after each full (major) garbage collection V8 runs on its
6884
+ * own: nothing is forced, so a test that triggers none reads null.
6885
+ */
6886
+ observeFullCollections() {
6887
+ try {
6888
+ const observer = new import_node_perf_hooks.PerformanceObserver((list) => {
6889
+ for (const entry of list.getEntries()) {
6890
+ const detail = entry.detail;
6891
+ const kind = detail?.kind ?? entry.kind;
6892
+ if (kind !== import_node_perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR) continue;
6893
+ const heap = process.memoryUsage().heapUsed;
6894
+ this.heapAfterGc = Math.min(this.heapAfterGc ?? Infinity, heap);
6895
+ }
6896
+ });
6897
+ observer.observe({ entryTypes: ["gc"] });
6898
+ return observer;
6899
+ } catch {
6900
+ return null;
6901
+ }
5673
6902
  }
5674
6903
  /** The worker's processes under it, keyed by pid and start time. */
5675
6904
  tree() {
@@ -5703,6 +6932,7 @@ var WorkerMetrics = class {
5703
6932
  start(processes = true) {
5704
6933
  try {
5705
6934
  this.histogram?.reset();
6935
+ this.heapAfterGc = null;
5706
6936
  this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
5707
6937
  this.usage = process.resourceUsage();
5708
6938
  if (this.linux && processes && !this.subtree) this.snapshot();
@@ -5725,6 +6955,7 @@ var WorkerMetrics = class {
5725
6955
  loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
5726
6956
  loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
5727
6957
  heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
6958
+ heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
5728
6959
  fds: this.linux ? this.proc.fdCount(this.pid) : null
5729
6960
  }
5730
6961
  };
@@ -5759,6 +6990,7 @@ var WorkerMetrics = class {
5759
6990
  }
5760
6991
  dispose() {
5761
6992
  this.histogram?.disable();
6993
+ this.gcObserver?.disconnect();
5762
6994
  }
5763
6995
  };
5764
6996
  var PageReader = class {
@@ -5780,12 +7012,36 @@ var PageReader = class {
5780
7012
  try {
5781
7013
  const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
5782
7014
  const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
5783
- return {
7015
+ const main = {
5784
7016
  cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
5785
7017
  heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
5786
7018
  nodes: values.get("Nodes") ?? 0,
5787
7019
  listeners: values.get("JSEventListeners") ?? 0
5788
7020
  };
7021
+ const elements = await this.countElements(page, opened);
7022
+ if (elements !== null) main.elements = elements;
7023
+ return main;
7024
+ } catch {
7025
+ return null;
7026
+ }
7027
+ }
7028
+ /**
7029
+ * The elements attached to the page's main document, through the page's own
7030
+ * DevTools session: no Playwright call, so no step and no trace action. The
7031
+ * count runs in the page's main world and changes nothing in it.
7032
+ */
7033
+ async countElements(page, session) {
7034
+ try {
7035
+ const result = await internalCall(
7036
+ page,
7037
+ () => session.send("Runtime.evaluate", {
7038
+ expression: "document.getElementsByTagName('*').length",
7039
+ returnByValue: true,
7040
+ silent: true
7041
+ })
7042
+ );
7043
+ const value = result.result?.value;
7044
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
5789
7045
  } catch {
5790
7046
  return null;
5791
7047
  }
@@ -5794,6 +7050,9 @@ var PageReader = class {
5794
7050
 
5795
7051
  // src/internal/capture/resource-ledger.ts
5796
7052
  var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
7053
+ var COUNTED_WITH_RUNNING = ["FSEventWrap", "StatWatcher"];
7054
+ var PLAYWRIGHT_ROLES = /* @__PURE__ */ new Set(["worker", "runner", "browser", "renderer", "gpu", "utility", "ffmpeg"]);
7055
+ var HANDLE_STACK_LIMIT = 40;
5797
7056
  var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
5798
7057
  var CLOSE_TIMEOUT_MS = 5e3;
5799
7058
  var PAGE_READ_CAP = 50;
@@ -5869,20 +7128,41 @@ function playwrightTestInfoSource() {
5869
7128
  function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
5870
7129
  return value === "fail" || value === "close" ? value : "report";
5871
7130
  }
5872
- function countHandles() {
7131
+ function countHandles(types = TRACKED_HANDLE_TYPES) {
5873
7132
  const counts = {};
5874
7133
  const read = process.getActiveResourcesInfo;
5875
7134
  if (typeof read !== "function") return counts;
5876
7135
  for (const type of read.call(process)) {
5877
- if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7136
+ if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
5878
7137
  }
5879
7138
  return counts;
5880
7139
  }
5881
- async function settledHandles(start) {
5882
- const end = countHandles();
7140
+ async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
7141
+ const end = countHandles(types);
5883
7142
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
5884
7143
  await new Promise((resolve5) => setTimeout(resolve5, 0));
5885
- return countHandles();
7144
+ return countHandles(types);
7145
+ }
7146
+ function deepUserSite() {
7147
+ const limit = Error.stackTraceLimit;
7148
+ Error.stackTraceLimit = HANDLE_STACK_LIMIT;
7149
+ try {
7150
+ return userSite(new Error().stack);
7151
+ } finally {
7152
+ Error.stackTraceLimit = limit;
7153
+ }
7154
+ }
7155
+ var SHELLS = /^(?:sh|bash|dash|zsh|cmd|cmd\.exe|powershell|powershell\.exe|pwsh)$/i;
7156
+ function commandOf(spawnfile, spawnargs) {
7157
+ const name = (file) => path13.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
7158
+ const program = typeof spawnfile === "string" ? name(spawnfile) : "";
7159
+ const args = Array.isArray(spawnargs) ? spawnargs.map(String) : [];
7160
+ const script = args.findIndex((arg) => arg === "-c" || /^\/c$/i.test(arg));
7161
+ if (SHELLS.test(program) && script >= 0 && args[script + 1]) {
7162
+ const first = args[script + 1].trim().split(/\s+/)[0];
7163
+ if (first) return name(first);
7164
+ }
7165
+ return program || "a child process";
5886
7166
  }
5887
7167
  function settleWithin(promise, ms) {
5888
7168
  let timer;
@@ -5894,7 +7174,7 @@ function settleWithin(promise, ms) {
5894
7174
  ]).finally(() => clearTimeout(timer));
5895
7175
  }
5896
7176
  var ResourceLedger = class {
5897
- constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
7177
+ constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}, nodeHandles = false) {
5898
7178
  this.playwright = playwright;
5899
7179
  this.workerIndex = workerIndex;
5900
7180
  this.leakCheck = leakCheck;
@@ -5902,6 +7182,7 @@ var ResourceLedger = class {
5902
7182
  this.testInfoSource = testInfoSource;
5903
7183
  this.now = now;
5904
7184
  this.readers = readers;
7185
+ this.nodeHandles = nodeHandles;
5905
7186
  this.nextId = 1;
5906
7187
  this.entries = /* @__PURE__ */ new Map();
5907
7188
  this.byObject = /* @__PURE__ */ new WeakMap();
@@ -5918,6 +7199,11 @@ var ResourceLedger = class {
5918
7199
  this.listener = null;
5919
7200
  /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
5920
7201
  this.sawBrowser = false;
7202
+ /** The servers and child processes still running, while the ledger tracks them. */
7203
+ this.handles = /* @__PURE__ */ new Set();
7204
+ this.byHandle = /* @__PURE__ */ new WeakMap();
7205
+ this.trackingHandles = false;
7206
+ this.restores = [];
5921
7207
  }
5922
7208
  /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
5923
7209
  install() {
@@ -5950,13 +7236,107 @@ var ResourceLedger = class {
5950
7236
  for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
5951
7237
  if (type) this.wrapBrowserType(type);
5952
7238
  }
7239
+ if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
5953
7240
  return true;
5954
7241
  }
5955
- /** Stop listening; the objects already tracked stay in the ledger. */
7242
+ /** Stop listening, and put Node's `listen` and `spawn` back; the objects already tracked stay in the ledger. */
5956
7243
  uninstall() {
5957
7244
  const remove = this.playwright?._instrumentation?.removeListener;
5958
7245
  if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
5959
7246
  this.listener = null;
7247
+ for (const restore of this.restores.splice(0)) restore();
7248
+ this.trackingHandles = false;
7249
+ }
7250
+ /**
7251
+ * Wrap `net.Server.prototype.listen` (every HTTP, HTTPS and TCP server) and
7252
+ * `ChildProcess.prototype.spawn` (`spawn`, `exec`, `execFile` and `fork`), so
7253
+ * each server and child process the worker starts carries the line and the
7254
+ * runnable that started it, as a context does. A worker-scoped fixture's
7255
+ * server is then the worker's, never the first test's. Each wrapper calls the
7256
+ * original on its receiver and only records; it never throws.
7257
+ */
7258
+ wrapNodeHandles() {
7259
+ const onServer = (server2, site) => this.guard(() => this.onServer(server2, site));
7260
+ const onChild = (child2, site) => this.guard(() => this.onChild(child2, site));
7261
+ const server = net3.Server.prototype;
7262
+ const listen = server.listen;
7263
+ if (typeof listen === "function") {
7264
+ const wrapped = function(...args) {
7265
+ onServer(this, deepUserSite());
7266
+ return listen.apply(this, args);
7267
+ };
7268
+ server.listen = wrapped;
7269
+ this.restores.push(() => {
7270
+ if (server.listen === wrapped) server.listen = listen;
7271
+ });
7272
+ }
7273
+ const child = childProcess.ChildProcess?.prototype;
7274
+ const spawn = child?.spawn;
7275
+ if (child && typeof spawn === "function") {
7276
+ const wrapped = function(...args) {
7277
+ const site = deepUserSite();
7278
+ const result = spawn.apply(this, args);
7279
+ onChild(this, site);
7280
+ return result;
7281
+ };
7282
+ child.spawn = wrapped;
7283
+ this.restores.push(() => {
7284
+ if (child.spawn === wrapped) child.spawn = spawn;
7285
+ });
7286
+ }
7287
+ this.trackingHandles = true;
7288
+ }
7289
+ trackHandle(object, entry) {
7290
+ const info = this.testInfoSource();
7291
+ const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
7292
+ const tracked = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
7293
+ this.handles.add(tracked);
7294
+ this.byHandle.set(object, tracked);
7295
+ return tracked;
7296
+ }
7297
+ onServer(server, site) {
7298
+ const known = this.byHandle.get(server);
7299
+ if (known && this.handles.has(known)) return;
7300
+ let bound = false;
7301
+ let closed = false;
7302
+ const entry = this.trackHandle(server, {
7303
+ type: "TCPServerWrap",
7304
+ site,
7305
+ // `close()` unbinds at once, while its 'close' event waits for open connections.
7306
+ running: () => server.listening,
7307
+ pending: () => !bound && !closed && !server.listening
7308
+ });
7309
+ server.once("listening", () => {
7310
+ bound = true;
7311
+ });
7312
+ server.once("close", () => {
7313
+ closed = true;
7314
+ this.handles.delete(entry);
7315
+ });
7316
+ }
7317
+ onChild(child, site) {
7318
+ const command = commandOf(child.spawnfile, child.spawnargs);
7319
+ const role = roleOf(command, (child.spawnargs ?? []).join(" "));
7320
+ if (role && PLAYWRIGHT_ROLES.has(role) || child.pid === void 0) return;
7321
+ const entry = this.trackHandle(child, {
7322
+ type: "ChildProcess",
7323
+ site,
7324
+ command,
7325
+ running: () => child.exitCode === null && child.signalCode === null
7326
+ });
7327
+ child.once("exit", () => this.handles.delete(entry));
7328
+ }
7329
+ /**
7330
+ * The servers and child processes a test started (its body, its each-hooks
7331
+ * or a test-scoped fixture) that still run. A server whose `listen` has not
7332
+ * bound yet is read again one tick later.
7333
+ */
7334
+ async runningOf(testId) {
7335
+ const own = () => [...this.handles].filter(
7336
+ (entry) => entry.test?.id === testId && !entry.fixture?.worker && (entry.phase === "test" || entry.phase === "beforeEach" || entry.phase === "afterEach")
7337
+ );
7338
+ if (own().some((entry) => entry.pending?.())) await new Promise((resolve5) => setTimeout(resolve5, 0));
7339
+ return own().filter((entry) => entry.running()).map((entry) => ({ type: entry.type, site: entry.site, ...entry.command ? { command: entry.command } : {} }));
5960
7340
  }
5961
7341
  /** Mark a page used: a locator was built on it. */
5962
7342
  notePageUse(page) {
@@ -5966,7 +7346,7 @@ var ResourceLedger = class {
5966
7346
  /** Open the census window of a test. */
5967
7347
  testStarted(info) {
5968
7348
  this.window = testRef2(info);
5969
- this.handlesAtStart = countHandles();
7349
+ this.handlesAtStart = countHandles(this.countedHandleTypes());
5970
7350
  this.openAtStart = { contexts: 0, pages: 0 };
5971
7351
  for (const entry of this.openEntries()) {
5972
7352
  if (entry.birth.kind === "page") this.openAtStart.pages++;
@@ -5986,9 +7366,14 @@ var ResourceLedger = class {
5986
7366
  if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
5987
7367
  const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
5988
7368
  const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
5989
- const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
7369
+ const handles = {
7370
+ start: this.handlesAtStart,
7371
+ end: await settledHandles(this.handlesAtStart, this.countedHandleTypes())
7372
+ };
7373
+ const running = this.trackingHandles && test ? await this.runningOf(test.id) : null;
5990
7374
  const census = this.census(ref, handles, await this.readPages());
5991
7375
  if (metrics) census.metrics = metrics;
7376
+ if (running) census.running = running;
5992
7377
  if (test) {
5993
7378
  census.openAtStart = this.openAtStart;
5994
7379
  if (!failed) census.leftOpen = leaks.length;
@@ -6018,7 +7403,7 @@ var ResourceLedger = class {
6018
7403
  const census = this.census(null);
6019
7404
  if (this.resultsFile) {
6020
7405
  try {
6021
- fs10.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
7406
+ fs11.appendFileSync(this.resultsFile, `${JSON.stringify(census)}
6022
7407
  `);
6023
7408
  } catch {
6024
7409
  }
@@ -6027,6 +7412,10 @@ var ResourceLedger = class {
6027
7412
  this.uninstall();
6028
7413
  return census;
6029
7414
  }
7415
+ /** The handle types counted at each boundary: servers leave the count once they are tracked one by one. */
7416
+ countedHandleTypes() {
7417
+ return this.trackingHandles ? COUNTED_WITH_RUNNING : TRACKED_HANDLE_TYPES;
7418
+ }
6030
7419
  guard(run) {
6031
7420
  try {
6032
7421
  run();
@@ -6249,6 +7638,7 @@ var ResourceLedger = class {
6249
7638
  open
6250
7639
  };
6251
7640
  if (handles) census.handles = handles;
7641
+ for (const close of this.closed) this.entries.delete(close.id);
6252
7642
  this.born = [];
6253
7643
  this.closed = [];
6254
7644
  return census;
@@ -6287,7 +7677,8 @@ function startResourceLedger(playwright, workerIndex) {
6287
7677
  process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
6288
7678
  playwrightTestInfoSource(),
6289
7679
  Date.now,
6290
- { metrics, pages: new PageReader() }
7680
+ { metrics, pages: new PageReader() },
7681
+ true
6291
7682
  );
6292
7683
  if (!ledger.install()) {
6293
7684
  metrics.dispose();
@@ -6638,21 +8029,29 @@ function createSink() {
6638
8029
  locatorCalls: /* @__PURE__ */ new Map(),
6639
8030
  stops: [],
6640
8031
  shownStops: /* @__PURE__ */ new Map(),
8032
+ failureHeld: false,
8033
+ frozenAt: null,
8034
+ stopEnds: 0,
8035
+ ending: false,
6641
8036
  released: [],
6642
8037
  releasedRequests: /* @__PURE__ */ new Set(),
6643
8038
  onReleasedRequestsEnded: null,
6644
8039
  releasedQuietSince: 0,
8040
+ releasedUntil: null,
6645
8041
  locatorPages: new LocatorPageLog(),
6646
- codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
8042
+ codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
6647
8043
  serverCoverage: /* @__PURE__ */ new Map(),
6648
8044
  baseURL: null
6649
8045
  };
6650
8046
  }
6651
8047
  var MAX_CODE_REACH_PAGES = 16;
6652
8048
  var CODE_REACH_START_WAIT_MS = 2e3;
8049
+ function pageCoverageInWorker() {
8050
+ return browserCoverageInWorker() || cssCoverageInWorker();
8051
+ }
6653
8052
  function maybeStartCodeReach(page, alreadyOpen = false) {
6654
8053
  const sink = currentSink;
6655
- if (!sink || !browserCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
8054
+ if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
6656
8055
  const known = sink.codeReach.sessions.get(page);
6657
8056
  if (known) return known.started;
6658
8057
  if (alreadyOpen) {
@@ -6665,7 +8064,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
6665
8064
  return started;
6666
8065
  }
6667
8066
  async function startCodeReachOnOpenPages() {
6668
- if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
8067
+ if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
6669
8068
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
6670
8069
  await Promise.race([started, new Promise((resolve5) => setTimeout(resolve5, CODE_REACH_START_WAIT_MS).unref())]);
6671
8070
  }
@@ -6708,22 +8107,28 @@ async function stopSinkCodeReach(sink, closing) {
6708
8107
  async function stopPageCodeReach(sink, page, session, roots) {
6709
8108
  try {
6710
8109
  if (!await session.started || isPageClosed(page)) return;
6711
- const entries = await stopCodeReach(page);
6712
- if (!entries) return;
6713
- const { reached, known } = await resolveCodeReachFunctions(entries, roots, pageMapFetcher(page), page.url(), {
8110
+ const { scripts, sheets } = await stopPageCoverage(page);
8111
+ const fetchMap = pageMapFetcher(page);
8112
+ const append = (into, from) => {
8113
+ for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
8114
+ };
8115
+ if (sheets.length > 0) {
8116
+ const rules = await resolveCssCoverage(sheets, roots, fetchMap);
8117
+ append(sink.codeReach.cssFiles, rules.reached);
8118
+ append(sink.codeReach.cssKnown, rules.known);
8119
+ }
8120
+ if (!scripts) return;
8121
+ const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
6714
8122
  known: browserFunctionsInWorker()
6715
8123
  });
6716
- for (const [file, functions] of reached) {
6717
- sink.codeReach.files.set(file, [...sink.codeReach.files.get(file) ?? [], ...functions]);
6718
- }
6719
- for (const [file, functions] of known) {
6720
- sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
6721
- }
8124
+ append(sink.codeReach.files, reached);
8125
+ append(sink.codeReach.known, known);
6722
8126
  } catch {
6723
8127
  }
6724
8128
  }
6725
8129
  var currentSink = null;
6726
8130
  var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
8131
+ var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
6727
8132
  var PENDING_PROBES = /* @__PURE__ */ new Set();
6728
8133
  async function drainPendingProbes(capMs) {
6729
8134
  if (PENDING_PROBES.size === 0) return;
@@ -6736,6 +8141,25 @@ async function drainPendingProbes(capMs) {
6736
8141
  ]);
6737
8142
  clearTimeout(cap);
6738
8143
  }
8144
+ async function settleBeforeClose(closing) {
8145
+ const drained = drainPendingProbes(1e3);
8146
+ const sink = currentSink;
8147
+ if (sink) {
8148
+ await endShownStops(sink, "closed", closing);
8149
+ await awaitReleasedCalls(sink);
8150
+ await stashPageState(sink, closing);
8151
+ }
8152
+ await drained;
8153
+ if (sink) {
8154
+ await maybeOpenPicker(sink, closing);
8155
+ await stopSinkCodeReach(sink, closing);
8156
+ }
8157
+ }
8158
+ async function settleContextClose(context) {
8159
+ CLOSING_CONTEXTS.add(context);
8160
+ await settleBeforeClose({ context });
8161
+ if (contextDebugger(context)?.pausedDetails()) await steerDebugger(context, "resume");
8162
+ }
6739
8163
  function isPageClosed(page) {
6740
8164
  try {
6741
8165
  return typeof page.isClosed === "function" ? page.isClosed() : false;
@@ -6858,7 +8282,7 @@ function buildPageState(raw, cookies, interaction) {
6858
8282
  }
6859
8283
  async function readPageState(page) {
6860
8284
  try {
6861
- const raw = await page.evaluate(() => {
8285
+ const readRaw = page.evaluate(() => {
6862
8286
  const listStorage = (s) => {
6863
8287
  const out = [];
6864
8288
  try {
@@ -6884,14 +8308,16 @@ async function readPageState(page) {
6884
8308
  sessionStorage: listStorage(globalThis.sessionStorage)
6885
8309
  };
6886
8310
  });
8311
+ const readCookies = (async () => {
8312
+ try {
8313
+ return await pageContext(page)?.cookies() ?? null;
8314
+ } catch {
8315
+ return null;
8316
+ }
8317
+ })();
8318
+ const readInteraction = page.evaluate(readPageInteraction).catch(() => null);
8319
+ const [raw, cookies, interaction] = await Promise.all([readRaw, readCookies, readInteraction]);
6887
8320
  if (!raw) return null;
6888
- let cookies = null;
6889
- try {
6890
- cookies = await pageContext(page)?.cookies() ?? null;
6891
- } catch {
6892
- cookies = null;
6893
- }
6894
- const interaction = await page.evaluate(readPageInteraction).catch(() => null);
6895
8321
  return buildPageState(raw, cookies, interaction);
6896
8322
  } catch {
6897
8323
  return null;
@@ -6955,7 +8381,7 @@ async function recordPageWindow(sink, page) {
6955
8381
  }
6956
8382
  async function stashPageState(sink, closing) {
6957
8383
  const page = sink.lastActivePage;
6958
- if (!page || isPageClosed(page)) return;
8384
+ if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
6959
8385
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
6960
8386
  if (!belongsToClosing) return;
6961
8387
  await internalCall(page, () => readPageBeforeClose(sink, page));
@@ -6967,13 +8393,12 @@ async function readBlankPage(page) {
6967
8393
  return aria != null && aria.trim() === "";
6968
8394
  }
6969
8395
  async function readPageBeforeClose(sink, page) {
6970
- const vitals = await readWebVitals(page);
8396
+ const readBlank = sink.probeItem?.level === "server" ? readBlankPage(page).catch(() => null) : null;
8397
+ const readState = process.env.PIWI_CAPTURE_PAGE_STATE !== "false" ? Promise.resolve(readBlank).then(() => readPageState(page)) : null;
8398
+ const [vitals, blankPage, pageState] = await Promise.all([readWebVitals(page), readBlank, readState]);
6971
8399
  if (vitals) sink.stashedWebVitals = vitals;
6972
- if (sink.probeItem?.level === "server") sink.probeBlankPage = await readBlankPage(page);
6973
- if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
6974
- const pageState = await readPageState(page);
6975
- if (pageState) sink.stashedPageState = pageState;
6976
- }
8400
+ if (blankPage !== null) sink.probeBlankPage = blankPage;
8401
+ if (pageState) sink.stashedPageState = pageState;
6977
8402
  const status = sink.testInfo?.status;
6978
8403
  const sampleAria = async () => {
6979
8404
  const root = page.locator(":root");
@@ -7167,16 +8592,35 @@ function locatorSource(target, method, args) {
7167
8592
  return renderFailing({ method, args });
7168
8593
  }
7169
8594
  var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
8595
+ var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
8596
+ var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
7170
8597
  var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
7171
- async function armBreakpoints(contexts) {
8598
+ function stopPageOf(context, call, paused) {
8599
+ const named = [call?.page, paused].find((page) => !!page && pageContext(page) === context);
8600
+ if (named) return { page: named };
8601
+ const open = openPages(context);
8602
+ return open.length === 1 ? { page: open[0] } : { among: open };
8603
+ }
8604
+ function openPages(context) {
8605
+ return context.pages().filter((page) => !isPageClosed(page));
8606
+ }
8607
+ function stopPages(of) {
8608
+ return "page" in of ? [of.page] : of.among;
8609
+ }
8610
+ function stopCanShow(context, of) {
8611
+ if (CLOSING_CONTEXTS.has(context)) return false;
8612
+ return !stopPages(of).some((page) => isPageClosed(page) || CLOSING_PAGES.has(page));
8613
+ }
8614
+ async function armStops(contexts) {
7172
8615
  const state = currentSink?.pause;
7173
- if (!state || state.finished || state.set.size === 0) return;
8616
+ if (!state || !wantsStops(state)) return;
7174
8617
  await Promise.all(
7175
8618
  [...contexts].map(
7176
8619
  (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
7177
8620
  )
7178
8621
  );
7179
8622
  }
8623
+ var stopTurn = Promise.resolve();
7180
8624
  function onDebuggerStop(context) {
7181
8625
  const details = contextDebugger(context)?.pausedDetails();
7182
8626
  if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
@@ -7187,13 +8631,38 @@ function onDebuggerStop(context) {
7187
8631
  try {
7188
8632
  if (!sink || !state) return;
7189
8633
  const callSite = stopSite(sink, details.location);
8634
+ const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
7190
8635
  const pagePause = PAGE_PAUSES.get(context);
8636
+ const asked = askedOnce(state);
7191
8637
  const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
7192
8638
  if ("go" in decided) {
7193
8639
  how = decided.go;
7194
8640
  return;
7195
8641
  }
7196
- how = goOn(state, await showStop(sink, context, details.title, callSite ?? pagePause, decided.show));
8642
+ if (pagePause) stopShown(state);
8643
+ if (sink.ending || CLOSING_CONTEXTS.has(context)) {
8644
+ askAgain(state, asked);
8645
+ how = goOn(state, sink.ending ? endedWaiting(sink, call) : void 0);
8646
+ return;
8647
+ }
8648
+ const of = stopPageOf(context, call, pagePause?.page);
8649
+ const ends = sink.stopEnds;
8650
+ const shown2 = stopTurn.then(async () => {
8651
+ const ended = currentSink !== sink || sink.ending || sink.stopEnds !== ends;
8652
+ const gone = decided.show === "breakpoint" && !atBreakpoint(state, callSite);
8653
+ if (!ended && !gone && stopCanShow(context, of)) {
8654
+ const location = callSite ?? pagePause.location;
8655
+ const answer = await showStop(sink, context, details.title, location, decided.show, call, of);
8656
+ if (answer !== void 0) return answer;
8657
+ }
8658
+ askAgain(state, asked);
8659
+ if (ended) return endedWaiting(sink, call);
8660
+ const closing = stopPages(of).flatMap((page) => CLOSING_PAGES.get(page) ?? []);
8661
+ await settledWithin(closing, END_STOP_MS);
8662
+ return void 0;
8663
+ });
8664
+ stopTurn = shown2.catch(() => void 0);
8665
+ how = goOn(state, await shown2);
7197
8666
  } catch {
7198
8667
  if (state) how = goOn(state);
7199
8668
  } finally {
@@ -7201,22 +8670,30 @@ function onDebuggerStop(context) {
7201
8670
  }
7202
8671
  })();
7203
8672
  }
8673
+ function endedWaiting(sink, call) {
8674
+ releaseCall(sink, call?.settled ?? Promise.resolve());
8675
+ return "finish";
8676
+ }
7204
8677
  function stopSite(sink, location) {
7205
8678
  if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
7206
8679
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
7207
8680
  }
7208
- function stopPage(sink, context, call) {
7209
- for (const page of [call?.page, sink.lastActivePage]) {
7210
- if (page && !isPageClosed(page) && pageContext(page) === context) return page;
7211
- }
7212
- const open = context.pages().filter((page) => !isPageClosed(page));
8681
+ function stopBarPage(sink, context, of) {
8682
+ if ("page" in of) return of.page;
8683
+ const last = sink.lastActivePage;
8684
+ if (last && !isPageClosed(last) && pageContext(last) === context) return last;
8685
+ const open = openPages(context);
7213
8686
  return open[open.length - 1] ?? null;
7214
8687
  }
7215
- async function showStop(sink, context, title, location, reason) {
8688
+ async function showStop(sink, context, title, location, reason, call, of, failure = null) {
7216
8689
  const testInfo = sink.testInfo;
7217
- const call = sink.locatorCalls.get(callSiteLine(location));
7218
- const page = stopPage(sink, context, call);
7219
- if (!testInfo || !page) return void 0;
8690
+ const page = stopBarPage(sink, context, of);
8691
+ const guessed = !("page" in of);
8692
+ if (!testInfo) return void 0;
8693
+ const bar = !!page && testInfo.project?.use?.headless === false;
8694
+ const place = runChannelOpen() ? callSitePlace(location) : null;
8695
+ if (!bar && !place) return void 0;
8696
+ const failing = failure?.failed ?? null;
7220
8697
  const onPick = async (picked) => {
7221
8698
  if (call) {
7222
8699
  call.picked = true;
@@ -7227,16 +8704,57 @@ async function showStop(sink, context, title, location, reason) {
7227
8704
  ...picked
7228
8705
  });
7229
8706
  sink.probedLocations.add(call.location);
8707
+ } else if (failing) {
8708
+ const pick = {
8709
+ failing: { method: failing.method, args: failing.args, rendered: renderFailing(failing), location },
8710
+ ...picked
8711
+ };
8712
+ sink.userPick = pick;
8713
+ applyPickToSnapshots(sink.capturedLocators, pick);
7230
8714
  }
7231
8715
  console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
7232
- await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
8716
+ await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
7233
8717
  };
8718
+ const handoffReason = reason === "page-pause" ? "pause" : "breakpoint";
8719
+ const onRecord = bar && page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
8720
+ const intents = bar && page ? new BarIntents() : null;
7234
8721
  const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
7235
8722
  sink.stops.push(record);
7236
8723
  const end = new AbortController();
7237
- const shown = withTestTimeoutHeld(
7238
- testInfo,
7239
- () => internalCall(
8724
+ const over = new AbortController();
8725
+ end.signal.addEventListener("abort", () => over.abort(), { once: true });
8726
+ let fromEditor;
8727
+ const held2 = place ? holdStop(
8728
+ {
8729
+ reason,
8730
+ title,
8731
+ place,
8732
+ locator: call ? locatorSource(call.target, call.origin.method, call.origin.args) : failing ? renderFailing(failing) : null,
8733
+ bar,
8734
+ page: guessed ? null : page,
8735
+ ...failure?.error ? { error: failure.error } : {},
8736
+ intent: intents ? (what) => intentAt(intents, what, {
8737
+ record: !!onRecord,
8738
+ guessed,
8739
+ // A failure's stop is held in the teardown it ends with.
8740
+ holds: () => !over.signal.aborted && (!!failure || !sink.ending && stopCanShow(context, of))
8741
+ }) : void 0
8742
+ },
8743
+ (answer2) => {
8744
+ fromEditor = answer2;
8745
+ over.abort();
8746
+ }
8747
+ ) : null;
8748
+ const onIntent = bar && page && held2 ? (intent, ended) => intent === "assert" ? checkAt(page, testInfo, location, handoffReason, held2.stop, ended) : regionAt(page, location, held2.stop, ended) : void 0;
8749
+ const shown2 = withTestTimeoutHeld(testInfo, async () => {
8750
+ if (!bar || !page) {
8751
+ console.log(
8752
+ failure ? `[piwi] The test failed at ${callSiteLine(location)}: Resume in the editor lets it end as it failed.` : `[piwi] Paused at ${callSiteLine(location)} before ${title}: Resume, Step or Finish in the editor.`
8753
+ );
8754
+ await abortion(over.signal);
8755
+ return "resume";
8756
+ }
8757
+ return internalCall(
7240
8758
  page,
7241
8759
  () => pauseHere(
7242
8760
  page,
@@ -7244,45 +8762,221 @@ async function showStop(sink, context, title, location, reason) {
7244
8762
  location,
7245
8763
  call: title,
7246
8764
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
7247
- target: call?.page === page ? call.target : null
8765
+ target: call?.page === page ? call.target : null,
8766
+ ...failure ? { failure: true } : {}
7248
8767
  },
7249
8768
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
7250
8769
  onPick,
7251
- recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
7252
- end.signal
8770
+ onRecord,
8771
+ over.signal,
8772
+ intents ?? void 0,
8773
+ onIntent
7253
8774
  )
7254
- )
7255
- ).finally(() => {
8775
+ );
8776
+ }).finally(async () => {
7256
8777
  if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
7257
8778
  else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
7258
8779
  sink.shownStops.delete(end);
8780
+ await held2?.release();
7259
8781
  });
7260
8782
  const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
7261
- sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
7262
- const answer = await shown;
7263
- if (!end.signal.aborted) return answer;
7264
- return end.signal.reason === "closed" ? "resume" : "finish";
8783
+ const stop = { page, guessed, context, record, done: shown2.catch(() => void 0), heldAt, after: null };
8784
+ if (!failure) sink.shownStops.set(end, stop);
8785
+ const answer = await shown2;
8786
+ if (!end.signal.aborted) return fromEditor ?? answer;
8787
+ if (end.signal.reason === "ended") return "finish";
8788
+ await settledWithin(stop.after ? [stop.after] : [], END_STOP_MS);
8789
+ return "resume";
8790
+ }
8791
+ function intentAt(intents, what, stop) {
8792
+ if (!stop.holds()) return { ok: false, message: "The test went on." };
8793
+ if (stop.guessed) {
8794
+ return {
8795
+ ok: false,
8796
+ message: `${RUN_INTENT_NAMES[what]} needs the page the test stopped in, which this call does not name among its context's pages: use the pause bar in the test's window.`
8797
+ };
8798
+ }
8799
+ if ((what === "record" || what === "assert") && !stop.record) {
8800
+ return {
8801
+ ok: false,
8802
+ message: `${RUN_INTENT_NAMES[what]} needs the editor that started the run: this run names no record channel.`
8803
+ };
8804
+ }
8805
+ if (!intents.offer(what)) {
8806
+ return {
8807
+ ok: false,
8808
+ message: `${RUN_INTENT_NAMES[what]} waits: a pick, a check or a recording runs in the test's window.`
8809
+ };
8810
+ }
8811
+ const started = {
8812
+ pick: "Pick an element in the test's window: the locator goes to the editor.",
8813
+ record: "Recording in the test's window: the editor writes the steps above the stopped line.",
8814
+ assert: "Check an element in the test's window: the editor writes the check above the stopped line.",
8815
+ "assert-region": "Pick the region in the test's window: the editor writes its ARIA snapshot above the stopped line."
8816
+ };
8817
+ return { ok: true, message: started[what] };
8818
+ }
8819
+ async function checkAt(page, testInfo, location, reason, stop, ended) {
8820
+ const ref = process.env[PIWI_ORIGIN_ENV.ref] ?? "";
8821
+ console.log(`[piwi] Checking an element at ${callSiteLine(location)}: the editor writes the check.`);
8822
+ const why = await recordFromHere({
8823
+ page,
8824
+ testInfo,
8825
+ location,
8826
+ reason,
8827
+ mode: "check",
8828
+ stop: { ref, worker: testInfo.workerIndex, stop },
8829
+ ended
8830
+ });
8831
+ if (ended?.aborted) console.log("[piwi] The check ended with the stop.");
8832
+ else console.log(why ? `[piwi] Nothing was checked: ${why}` : "[piwi] The check ended: the test is paused again.");
8833
+ }
8834
+ async function regionAt(page, location, stop, ended) {
8835
+ const picked = await pickLocator(page, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG }, null, ended).catch(
8836
+ () => null
8837
+ );
8838
+ if (!picked || ended?.aborted) return;
8839
+ const source = picked.picked.locator;
8840
+ const say = (message) => {
8841
+ console.log(`[piwi] ${message}`);
8842
+ tellHub({ type: "region", stop, refused: message });
8843
+ };
8844
+ const region = locatorOn(page, source);
8845
+ if (!region) return say(`The region's locator, ${source}, is not one Piwi can read.`);
8846
+ const count = await region.count().catch(() => 0);
8847
+ if (count !== 1) return say(`${source} finds ${count} elements: pick a region it finds alone.`);
8848
+ const read = await region.ariaSnapshot({ timeout: 5e3 }).catch(() => null);
8849
+ if (!read?.trim()) return say(`The ARIA snapshot of ${source} could not be read.`);
8850
+ const secrets = await regionSecrets(region).catch(() => null);
8851
+ if (secrets === null) return say(`The fields of ${source} that hold a secret could not be read.`);
8852
+ const snapshot = regionSnapshotWithoutSecrets(read, secrets);
8853
+ if (snapshot.length > MAX_REGION_SNAPSHOT) {
8854
+ return say(
8855
+ `The ARIA snapshot of ${source} is longer than ${MAX_REGION_SNAPSHOT} characters: pick a smaller region.`
8856
+ );
8857
+ }
8858
+ console.log(`[piwi] Region picked at ${callSiteLine(location)}: ${source}, its ARIA snapshot sent to the editor.`);
8859
+ tellHub({ type: "region", stop, locator: source, snapshot });
8860
+ }
8861
+ function regionSecrets(region) {
8862
+ return region.locator(":scope:is(input, textarea), input, textarea").evaluateAll(
8863
+ (elements, secret) => {
8864
+ return elements.flatMap((field) => {
8865
+ const tokens = (field.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
8866
+ const password = field.localName === "input" && String(field.type).toLowerCase() === "password";
8867
+ return password || tokens.some((token) => secret.includes(token)) ? [String(field.value ?? "")] : [];
8868
+ });
8869
+ },
8870
+ [...SECRET_AUTOCOMPLETE]
8871
+ );
8872
+ }
8873
+ async function holdFailure(sink, fallback) {
8874
+ const testInfo = sink.testInfo;
8875
+ if (sink.failureHeld || !sink.pause || !testInfo || !editorShows("failure")) return;
8876
+ const { status, expectedStatus } = testInfo;
8877
+ if (status !== "failed" && status !== "timedOut" || status === expectedStatus) return;
8878
+ const page = [sink.lastActivePage, fallback].find((open) => !!open && !isPageClosed(open));
8879
+ const context = page ? pageContext(page) : null;
8880
+ if (!page || !context) return;
8881
+ sink.failureHeld = true;
8882
+ const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
8883
+ const errors = testInfo.errors.length ? testInfo.errors : testInfo.error ? [testInfo.error] : [];
8884
+ const declared = (place2) => place2.file === testInfo.file && place2.line === testInfo.line;
8885
+ const placed = errors.flatMap((error2) => {
8886
+ const place2 = errorPlace(error2);
8887
+ return place2 && !declared(place2) ? [{ error: error2, place: place2 }] : [];
8888
+ })[0];
8889
+ const error = placed?.error ?? errors[0];
8890
+ const place = placed?.place ?? (failed?.location ? null : stepInProgress(testInfo));
8891
+ const location = (place ? stopCallSite(place) : null) ?? failed?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
8892
+ if (!location) return;
8893
+ const said = stripAnsi(error?.message ?? "").split("\n").find((line) => line.trim()) ?? status;
8894
+ if (page === sink.lastActivePage)
8895
+ await internalCall(page, () => readPageBeforeClose(sink, page)).catch(() => void 0);
8896
+ sink.frozenAt = Date.now();
8897
+ await stopSinkCodeReach(sink, { context });
8898
+ const title = hideSecretsInText(`Failed: ${said.trim().slice(0, 300)}`).slice(0, 1e3);
8899
+ const errorText = failureError(error?.message ?? error?.value);
8900
+ const named = errorText === null ? null : hideSecretsInText(errorText).slice(0, MAX_STOP_ERROR);
8901
+ await showStop(sink, context, title, location, "failure", void 0, { page }, { failed, error: named });
8902
+ }
8903
+ var FAILURE_ERROR_LINES = 4;
8904
+ function failureError(message) {
8905
+ const lines = [];
8906
+ for (const line of stripAnsi(message ?? "").trimStart().replace(/^Error: /, "").split("\n")) {
8907
+ if (!line.trim() || /^\s*Call log:/.test(line)) break;
8908
+ lines.push(line.trimEnd());
8909
+ if (lines.length === FAILURE_ERROR_LINES) break;
8910
+ }
8911
+ const text = lines.join("\n").slice(0, MAX_STOP_ERROR);
8912
+ return text.trim() ? text : null;
8913
+ }
8914
+ function errorPlace(error) {
8915
+ const given = error?.location;
8916
+ if (typeof given?.file === "string" && typeof given.line === "number") {
8917
+ return { file: given.file, line: given.line, column: given.column ?? 0 };
8918
+ }
8919
+ const stack = error?.stack;
8920
+ for (const line of typeof stack === "string" ? stack.split("\n") : []) {
8921
+ const frame = /^\s*at\s+(?:.*?\s+\()?(?:file:\/\/)?(\/[^():]+|[A-Za-z]:[\\/][^():]+):(\d+):(\d+)\)?$/.exec(line);
8922
+ if (!frame) continue;
8923
+ const file = frame[1];
8924
+ if (/[\\/]node_modules[\\/]/.test(file) || path14.dirname(path14.resolve(file)) === __dirname) continue;
8925
+ return { file, line: Number(frame[2]), column: Number(frame[3]) };
8926
+ }
8927
+ return null;
8928
+ }
8929
+ function stepInProgress(testInfo) {
8930
+ let found = null;
8931
+ const visit = (steps) => {
8932
+ if (!Array.isArray(steps)) return;
8933
+ for (const step of steps) {
8934
+ if (step.endWallTime) continue;
8935
+ const at = step.stack?.[0];
8936
+ const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
8937
+ if (called && typeof at?.file === "string" && typeof at.line === "number") {
8938
+ found = { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 };
8939
+ }
8940
+ visit(step.steps);
8941
+ }
8942
+ };
8943
+ visit(testInfo._steps);
8944
+ return found;
8945
+ }
8946
+ function abortion(signal) {
8947
+ return new Promise((resolve5) => {
8948
+ if (signal.aborted) resolve5();
8949
+ else signal.addEventListener("abort", () => resolve5(), { once: true });
8950
+ });
7265
8951
  }
7266
8952
  var END_STOP_MS = 2e3;
7267
8953
  var RELEASED_CALL_MS = 2e3;
7268
8954
  var RELEASED_QUIET_MS = 200;
7269
8955
  async function endShownStops(sink, why, closing) {
7270
8956
  const ending = [];
8957
+ if (why === "ended") sink.stopEnds++;
7271
8958
  let heldAt = Infinity;
7272
- for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
7273
- if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
8959
+ for (const [end, stop] of sink.shownStops) {
8960
+ if (closing && !closesStop(closing, stop)) continue;
7274
8961
  if (!end.signal.aborted) {
7275
- record.duration = Date.now() - record.startedAt;
8962
+ stop.record.duration = Date.now() - stop.record.startedAt;
8963
+ if (closing?.page) stop.after = CLOSING_PAGES.get(closing.page) ?? null;
7276
8964
  end.abort(why);
7277
- if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
8965
+ if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
7278
8966
  }
7279
- ending.push(done);
8967
+ ending.push(stop.done);
7280
8968
  }
7281
8969
  if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
7282
8970
  await settledWithin(ending, END_STOP_MS);
7283
8971
  }
8972
+ function closesStop(closing, stop) {
8973
+ if (closing.context && stop.context === closing.context) return true;
8974
+ if (!closing.page) return false;
8975
+ return stop.page === closing.page || stop.guessed && pageContext(closing.page) === stop.context;
8976
+ }
7284
8977
  function releaseCall(sink, settled) {
7285
8978
  sink.releasedQuietSince = Date.now();
8979
+ sink.releasedUntil = null;
7286
8980
  sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
7287
8981
  }
7288
8982
  function releasedRequestsEnded(sink) {
@@ -7291,8 +8985,8 @@ function releasedRequestsEnded(sink) {
7291
8985
  }
7292
8986
  async function awaitReleasedCalls(sink) {
7293
8987
  if (sink.released.length === 0) return;
7294
- const deadline = Date.now() + RELEASED_CALL_MS;
7295
- await settledWithin(sink.released, RELEASED_CALL_MS);
8988
+ const deadline = sink.releasedUntil ??= Date.now() + RELEASED_CALL_MS;
8989
+ await settledWithin(sink.released, Math.max(0, deadline - Date.now()));
7296
8990
  for (let now = Date.now(); now < deadline; now = Date.now()) {
7297
8991
  if (sink.releasedRequests.size > 0) {
7298
8992
  await settledWithin([releasedRequestsEnded(sink)], deadline - now);
@@ -7322,7 +9016,7 @@ function recorderAt(page, testInfo, location, reason) {
7322
9016
  return async (ended) => {
7323
9017
  console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
7324
9018
  const why = await recordFromHere({ page, testInfo, location, reason, ended });
7325
- if (ended?.aborted) console.log("[piwi] The recording ended with the test.");
9019
+ if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
7326
9020
  else
7327
9021
  console.log(
7328
9022
  why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
@@ -7339,10 +9033,10 @@ async function pauseInPlace(page, playwrightPause, location) {
7339
9033
  if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
7340
9034
  return leaveToPlaywright(context, playwrightPause);
7341
9035
  }
7342
- sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
9036
+ sink.pause ??= pauseState(NO_BREAKPOINTS);
7343
9037
  watchStops(context, onDebuggerStop);
7344
9038
  await steerDebugger(context, "pause");
7345
- PAGE_PAUSES.set(context, location);
9039
+ PAGE_PAUSES.set(context, { location, page });
7346
9040
  try {
7347
9041
  await playwrightPause({ __testHookKeepTestTimeout: true });
7348
9042
  } finally {
@@ -7525,15 +9219,15 @@ function instrumentPage(page) {
7525
9219
  if (typeof page.close === "function") {
7526
9220
  const originalClose = page.close.bind(page);
7527
9221
  page.close = async (...args) => {
7528
- await drainPendingProbes(1e3);
7529
- const sink = currentSink;
7530
- if (sink) {
7531
- await endShownStops(sink, "closed", { page });
7532
- await stashPageState(sink, { page });
7533
- await maybeOpenPicker(sink, { page });
7534
- await stopSinkCodeReach(sink, { page });
9222
+ let closed = () => {
9223
+ };
9224
+ CLOSING_PAGES.set(page, new Promise((resolve5) => closed = resolve5));
9225
+ try {
9226
+ await settleBeforeClose({ page });
9227
+ return await originalClose(...args);
9228
+ } finally {
9229
+ closed();
7535
9230
  }
7536
- return originalClose(...args);
7537
9231
  };
7538
9232
  }
7539
9233
  const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
@@ -7578,7 +9272,7 @@ function instrumentPage(page) {
7578
9272
  });
7579
9273
  page.on("console", (msg) => {
7580
9274
  const sink = currentSink;
7581
- if (!sink) return;
9275
+ if (!sink || sink.frozenAt !== null) return;
7582
9276
  const type = msg.type();
7583
9277
  if (["warning", "error", "assert"].includes(type)) {
7584
9278
  const location = msg.location();
@@ -7594,7 +9288,7 @@ function instrumentPage(page) {
7594
9288
  try {
7595
9289
  page.on("dialogclosed", (dialog) => {
7596
9290
  const sink = currentSink;
7597
- if (!sink) return;
9291
+ if (!sink || sink.frozenAt !== null) return;
7598
9292
  try {
7599
9293
  sink.dialogs.push({
7600
9294
  type: typeof dialog.type === "function" ? dialog.type() : null,
@@ -7613,12 +9307,14 @@ function instrumentPage(page) {
7613
9307
  if (!sink) return;
7614
9308
  try {
7615
9309
  const request = response.request();
9310
+ if (HELD_REQUESTS.has(request)) return;
7616
9311
  if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
7617
9312
  } catch {
7618
9313
  }
7619
9314
  });
7620
9315
  page.on("request", (request) => {
7621
9316
  const sink = currentSink;
9317
+ if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request);
7622
9318
  if (!sink || sink.released.length === 0) return;
7623
9319
  sink.releasedRequests.add(request);
7624
9320
  sink.releasedQuietSince = Date.now();
@@ -7629,16 +9325,18 @@ function instrumentPage(page) {
7629
9325
  sink.releasedQuietSince = Date.now();
7630
9326
  if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
7631
9327
  }
7632
- if (!sink || RECORDED_OPEN.has(request)) return;
9328
+ if (!sink || RECORDED_OPEN.has(request) || HELD_REQUESTS.has(request)) return;
7633
9329
  sink.lastActivePage = page;
7634
9330
  const response = sink.openResponses.get(request) ?? null;
7635
9331
  sink.openResponses.delete(request);
7636
9332
  recordRequest(sink, request, failed ? "failed" : "finished", response);
9333
+ if (!failed) void keepResponse(request, response);
7637
9334
  };
7638
9335
  page.on("requestfinished", (request) => onRequestEnd(request, false));
7639
9336
  page.on("requestfailed", (request) => onRequestEnd(request, true));
7640
9337
  }
7641
9338
  var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
9339
+ var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
7642
9340
  function isTrackedRequest(url, resourceType) {
7643
9341
  if (url.startsWith("data:") || url.startsWith("blob:")) return false;
7644
9342
  return TRACKED_REQUEST_TYPES.includes(resourceType);
@@ -7700,6 +9398,7 @@ function readResponseHeaders(sink, entry, url, headers) {
7700
9398
  }
7701
9399
  }
7702
9400
  function recordApiCall(sink, call) {
9401
+ if (sink.frozenAt !== null) return;
7703
9402
  try {
7704
9403
  const entry = apiCallEntry(call, sink.baseURL);
7705
9404
  if (!entry) return;
@@ -7736,7 +9435,10 @@ function instrumentContext(context) {
7736
9435
  INSTRUMENTED_CONTEXTS.add(context);
7737
9436
  if (typeof context.once === "function") {
7738
9437
  OPEN_CONTEXTS.add(context);
7739
- context.once("close", () => OPEN_CONTEXTS.delete(context));
9438
+ context.once("close", () => {
9439
+ OPEN_CONTEXTS.delete(context);
9440
+ CLOSING_CONTEXTS.add(context);
9441
+ });
7740
9442
  }
7741
9443
  observeOwnApiRequests(context);
7742
9444
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
@@ -7754,14 +9456,7 @@ function instrumentContext(context) {
7754
9456
  if (typeof context.close === "function") {
7755
9457
  const originalClose = context.close.bind(context);
7756
9458
  context.close = async (...args) => {
7757
- await drainPendingProbes(1e3);
7758
- const sink = currentSink;
7759
- if (sink) {
7760
- await endShownStops(sink, "closed", { context });
7761
- await stashPageState(sink, { context });
7762
- await maybeOpenPicker(sink, { context });
7763
- await stopSinkCodeReach(sink, { context });
7764
- }
9459
+ await settleContextClose(context);
7765
9460
  return originalClose(...args);
7766
9461
  };
7767
9462
  }
@@ -7774,7 +9469,7 @@ function patchBrowser(browser) {
7774
9469
  browser.newPage = async (...args) => {
7775
9470
  const page = await originalNewPage(...args);
7776
9471
  instrumentPage(page);
7777
- await armBreakpoints([pageContext(page)]);
9472
+ await armStops([pageContext(page)]);
7778
9473
  await awaitFlakeConditions();
7779
9474
  await maybeStartCodeReach(page);
7780
9475
  return page;
@@ -7783,13 +9478,16 @@ function patchBrowser(browser) {
7783
9478
  browser.newContext = async (...args) => {
7784
9479
  const context = await originalNewContext(...args);
7785
9480
  instrumentContext(context);
7786
- await armBreakpoints([context]);
9481
+ await armStops([context]);
7787
9482
  return context;
7788
9483
  };
7789
9484
  if (typeof browser.close === "function") {
7790
9485
  const originalClose = browser.close.bind(browser);
7791
9486
  browser.close = async (...args) => {
7792
9487
  await drainPendingProbes(1e3);
9488
+ for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
9489
+ await settleContextClose(context);
9490
+ }
7793
9491
  return originalClose(...args);
7794
9492
  };
7795
9493
  }
@@ -7842,7 +9540,11 @@ async function flushSink(sink, testInfo) {
7842
9540
  clearTimeout(drainDeadline);
7843
9541
  const page = sink.lastActivePage;
7844
9542
  const pageReadable = page !== null && !isPageClosed(page);
7845
- if (pageReadable) await maybeOpenPicker(sink);
9543
+ if (pageReadable) {
9544
+ await holdFailure(sink);
9545
+ await maybeOpenPicker(sink);
9546
+ }
9547
+ const live = pageReadable && sink.frozenAt === null;
7846
9548
  if (sink.stops.length > 0) {
7847
9549
  await testInfo.attach(ATTACHMENT_NAMES.pauses, {
7848
9550
  contentType: "application/json",
@@ -7879,6 +9581,21 @@ async function flushSink(sink, testInfo) {
7879
9581
  body: Buffer.from(JSON.stringify(part))
7880
9582
  });
7881
9583
  }
9584
+ if (cssCoverageInWorker()) {
9585
+ const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
9586
+ if (rules) {
9587
+ await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
9588
+ contentType: "application/json",
9589
+ body: Buffer.from(JSON.stringify(rules))
9590
+ });
9591
+ }
9592
+ for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
9593
+ await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
9594
+ contentType: "application/json",
9595
+ body: Buffer.from(JSON.stringify(part))
9596
+ });
9597
+ }
9598
+ }
7882
9599
  if (sink.serverCoverage.size > 0) {
7883
9600
  const requests = {};
7884
9601
  for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
@@ -7895,13 +9612,13 @@ async function flushSink(sink, testInfo) {
7895
9612
  }
7896
9613
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
7897
9614
  try {
7898
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
9615
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
7899
9616
  if (snapshot) {
7900
9617
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
7901
- contentType: "text/plain",
9618
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
7902
9619
  body: snapshot
7903
9620
  });
7904
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
9621
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
7905
9622
  if (snapshotJson) {
7906
9623
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
7907
9624
  contentType: "application/json",
@@ -7926,7 +9643,7 @@ async function flushSink(sink, testInfo) {
7926
9643
  }
7927
9644
  if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
7928
9645
  const option = sink.failureScreenshot;
7929
- const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
9646
+ const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
7930
9647
  if (shot) {
7931
9648
  try {
7932
9649
  await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
@@ -7936,13 +9653,13 @@ async function flushSink(sink, testInfo) {
7936
9653
  }
7937
9654
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
7938
9655
  try {
7939
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
9656
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
7940
9657
  if (snapshot) {
7941
9658
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
7942
- contentType: "text/plain",
9659
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
7943
9660
  body: snapshot
7944
9661
  });
7945
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
9662
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
7946
9663
  if (snapshotJson) {
7947
9664
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
7948
9665
  contentType: "application/json",
@@ -7988,7 +9705,7 @@ async function flushSink(sink, testInfo) {
7988
9705
  body: Buffer.from(JSON.stringify(sink.networkRequests))
7989
9706
  });
7990
9707
  }
7991
- const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
9708
+ const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
7992
9709
  if (webVitals) {
7993
9710
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
7994
9711
  contentType: "application/json",
@@ -7996,7 +9713,7 @@ async function flushSink(sink, testInfo) {
7996
9713
  });
7997
9714
  }
7998
9715
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
7999
- const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
9716
+ const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
8000
9717
  if (pageState) {
8001
9718
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
8002
9719
  contentType: "application/json",
@@ -8005,7 +9722,7 @@ async function flushSink(sink, testInfo) {
8005
9722
  }
8006
9723
  }
8007
9724
  if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
8008
- if (pageReadable) await recordPageWindow(sink, page);
9725
+ if (live) await recordPageWindow(sink, page);
8009
9726
  const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
8010
9727
  if (windows.length > 0) {
8011
9728
  await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
@@ -8015,21 +9732,79 @@ async function flushSink(sink, testInfo) {
8015
9732
  }
8016
9733
  }
8017
9734
  }
9735
+ var pauseNextTest = false;
9736
+ var runControl = {
9737
+ pause() {
9738
+ const sink = currentSink;
9739
+ if (!sink) {
9740
+ pauseNextTest = true;
9741
+ return;
9742
+ }
9743
+ if (!sink.pause) {
9744
+ const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo), channel: true }) : null;
9745
+ tellHub({ type: "notice", message: `This test does not stop${why ? `: ${why}` : ""}.` });
9746
+ return;
9747
+ }
9748
+ sink.pause.pause = true;
9749
+ void armStops(OPEN_CONTEXTS);
9750
+ },
9751
+ breakpoints(places) {
9752
+ const set = replacePauseSet(places);
9753
+ const state = currentSink?.pause;
9754
+ if (!state) return;
9755
+ state.set = set;
9756
+ void armStops(OPEN_CONTEXTS);
9757
+ },
9758
+ runTo(place) {
9759
+ const state = currentSink?.pause;
9760
+ if (!state) return;
9761
+ state.runTo = placeLine(place);
9762
+ void armStops(OPEN_CONTEXTS);
9763
+ }
9764
+ };
9765
+ function runFile(file) {
9766
+ return path14.relative(process.cwd(), file).split(path14.sep).join("/") || file;
9767
+ }
9768
+ function testBegin(testInfo, cannotPause) {
9769
+ const title = testInfo.titlePath.slice(1);
9770
+ return {
9771
+ type: "test-begin",
9772
+ testId: testInfo.testId,
9773
+ title: title.length ? title : [testInfo.title],
9774
+ file: runFile(testInfo.file),
9775
+ line: Math.max(1, testInfo.line),
9776
+ project: testInfo.project.name,
9777
+ retry: testInfo.retry,
9778
+ cannotPause
9779
+ };
9780
+ }
9781
+ function testEnd(sink, testInfo) {
9782
+ return {
9783
+ type: "test-end",
9784
+ testId: testInfo.testId,
9785
+ status: testInfo.status ?? "passed",
9786
+ pausedMs: sink.stops.reduce((sum, stop) => sum + stop.duration, 0)
9787
+ };
9788
+ }
8018
9789
  var piwiFixtures = {
8019
9790
  // Auto, worker-scoped: the worker's resource ledger, and the API request
8020
9791
  // contexts its tests create, handed out behind the call proxy. Set up before
8021
9792
  // any hook (worker fixtures come first), so a `beforeAll` context is in the
8022
9793
  // ledger and records the calls the tests make through it, and torn down after
8023
9794
  // the browser closed, so its census sees what only the worker's shutdown
8024
- // closed. Depends on `playwright` alone, which launches nothing.
9795
+ // closed. Depends on `playwright` alone, which launches nothing. The worker's
9796
+ // run connection to the editor (`PIWI_RUN_CHANNEL`) opens here, before the
9797
+ // first test, and closes as the worker ends.
8025
9798
  piwiResources: [
8026
9799
  async ({ playwright }, use, workerInfo) => {
8027
9800
  boxCaptureFrames();
8028
9801
  startResourceLedger(playwright, workerInfo.workerIndex);
8029
9802
  observeApiRequests(playwright, apiCallRecorder);
9803
+ await openRunChannel(workerInfo, runControl);
8030
9804
  try {
8031
9805
  await use();
8032
9806
  } finally {
9807
+ closeRunChannel();
8033
9808
  stopResourceLedger();
8034
9809
  }
8035
9810
  },
@@ -8049,10 +9824,12 @@ var piwiFixtures = {
8049
9824
  // never double-wraps a page the browser patch already instrumented.
8050
9825
  page: async ({ page }, use) => {
8051
9826
  instrumentPage(page);
8052
- await armBreakpoints([pageContext(page)]);
9827
+ await armStops([pageContext(page)]);
8053
9828
  await awaitFlakeConditions();
8054
9829
  await maybeStartCodeReach(page);
8055
9830
  await use(page);
9831
+ const sink = currentSink;
9832
+ if (sink) await holdFailure(sink, page);
8056
9833
  },
8057
9834
  // Auto, test-scoped: open a capture sink for the running test and flush it
8058
9835
  // (attach the collected data) at teardown. Runs for every test without being
@@ -8067,12 +9844,16 @@ var piwiFixtures = {
8067
9844
  sink.baseURL = baseURL ?? null;
8068
9845
  if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
8069
9846
  const pauseSet = currentPauseSet();
8070
- if (pauseSet.size) {
8071
- const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
8072
- if (reason) logPauseSkipOnce(reason);
8073
- else sink.pause = { set: pauseSet, step: false, finished: false };
9847
+ const channel = runChannelOpen();
9848
+ const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo), channel });
9849
+ if (pauseSet.size || channel) {
9850
+ if (!cannotPause) sink.pause = pauseState(pauseSet, pauseNextTest);
9851
+ else if (pauseSet.size) logPauseSkipOnce(cannotPause);
8074
9852
  }
9853
+ pauseNextTest = false;
8075
9854
  if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
9855
+ if (channel) tellHub(testBegin(testInfo, cannotPause));
9856
+ if (channel && sink.pause) startKeeping();
8076
9857
  if (isFlakeMode()) {
8077
9858
  const plan = loadFlakePlan();
8078
9859
  const role = flakeRoleForTest(plan, {
@@ -8096,14 +9877,18 @@ var piwiFixtures = {
8096
9877
  });
8097
9878
  activeResourceLedger()?.testStarted(testInfo);
8098
9879
  currentSink = sink;
9880
+ const showRunChannel = hideRunChannel();
8099
9881
  try {
8100
- await armBreakpoints(OPEN_CONTEXTS);
9882
+ await armStops(OPEN_CONTEXTS);
8101
9883
  await startCodeReachOnOpenPages();
8102
9884
  await use();
8103
9885
  } finally {
9886
+ sink.ending = true;
8104
9887
  await endShownStops(sink, "ended");
8105
9888
  await awaitReleasedCalls(sink);
9889
+ showRunChannel();
8106
9890
  currentSink = null;
9891
+ if (sink.pause?.pause) pauseNextTest = true;
8107
9892
  if (sink.probeItem) {
8108
9893
  const applied = sink.probeInterception?.applied() ?? false;
8109
9894
  const level = sink.probeItem.level ?? "client";
@@ -8126,7 +9911,12 @@ var piwiFixtures = {
8126
9911
  });
8127
9912
  }
8128
9913
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
8129
- await flushSink(sink, testInfo);
9914
+ try {
9915
+ await flushSink(sink, testInfo);
9916
+ } finally {
9917
+ stopKeeping();
9918
+ if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
9919
+ }
8130
9920
  await recordResourceCensus(testInfo);
8131
9921
  }
8132
9922
  },
@@ -8172,6 +9962,8 @@ function extendPiwiFixtures(test) {
8172
9962
  computeCoreVitals,
8173
9963
  configuredCodeReachRoots,
8174
9964
  extendPiwiFixtures,
9965
+ failureError,
9966
+ intentAt,
8175
9967
  internalCall,
8176
9968
  noteLocatorCall,
8177
9969
  noteNavigation,