@piwitests/reporter 0.61.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");
@@ -2414,6 +2416,7 @@ var ATTACHMENT_NAMES = {
2414
2416
  pauses: "piwi-pauses"
2415
2417
  };
2416
2418
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
2419
+ var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
2417
2420
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
2418
2421
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
2419
2422
 
@@ -2746,6 +2749,10 @@ var PIWI_LOCAL_DEBUG_ENV = {
2746
2749
  pauseAt: "PIWI_PAUSE_AT",
2747
2750
  editorSend: "PIWI_EDITOR_SEND"
2748
2751
  };
2752
+ var PIWI_ORIGIN_ENV = {
2753
+ kind: "PIWI_ORIGIN",
2754
+ ref: "PIWI_ORIGIN_REF"
2755
+ };
2749
2756
  var PIWI_PROBE_ENV = {
2750
2757
  flag: "PIWI_PROBE",
2751
2758
  plan: "PIWI_PROBE_PLAN",
@@ -2875,9 +2882,9 @@ function loadPlan() {
2875
2882
  function probeItemForTest(test) {
2876
2883
  const plan = loadPlan();
2877
2884
  if (!plan) return null;
2878
- 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;
2879
2886
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2880
- return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2887
+ return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
2881
2888
  }
2882
2889
  function classifyProbeHandled(signals) {
2883
2890
  const blank = signals.blankPage === true;
@@ -4066,6 +4073,258 @@ function maskUrlFragment(hash) {
4066
4073
  const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
4067
4074
  return hash.startsWith("#") ? `#${out}` : out;
4068
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
+ }
4069
4328
 
4070
4329
  // src/internal/capture/inspect-on-failure.ts
4071
4330
  function isCi(ci) {
@@ -4321,6 +4580,7 @@ var path7 = __toESM(require("path"));
4321
4580
  // src/internal/support/reporter-version.ts
4322
4581
  var fs5 = __toESM(require("fs"));
4323
4582
  var path6 = __toESM(require("path"));
4583
+ var cachedVersion = null;
4324
4584
  var CANDIDATE_OFFSETS = ["..", "../../.."];
4325
4585
  function findOwnPackageJson(fromDir) {
4326
4586
  for (const offset of CANDIDATE_OFFSETS) {
@@ -4335,6 +4595,11 @@ function findOwnPackageJson(fromDir) {
4335
4595
  }
4336
4596
  return void 0;
4337
4597
  }
4598
+ function getReporterVersion() {
4599
+ if (cachedVersion) return cachedVersion;
4600
+ cachedVersion = findOwnPackageJson(__dirname)?.version ?? "unknown";
4601
+ return cachedVersion;
4602
+ }
4338
4603
 
4339
4604
  // src/internal/capture/quiet-capture.ts
4340
4605
  var BOXED_PREFIX_EXPORTS = [
@@ -4460,11 +4725,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
4460
4725
  return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
4461
4726
  }
4462
4727
  var parsed = null;
4728
+ var sent = null;
4463
4729
  function currentPauseSet() {
4730
+ if (sent) return sent;
4464
4731
  const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
4465
4732
  if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
4466
4733
  return parsed.set;
4467
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
+ }
4468
4753
  function pauseGateFromTestInfo(testInfo) {
4469
4754
  const use = testInfo.project?.use ?? {};
4470
4755
  const debug = testInfo._configInternal?.configCLIOverrides?.debug;
@@ -4472,7 +4757,7 @@ function pauseGateFromTestInfo(testInfo) {
4472
4757
  }
4473
4758
  function pauseSkipReason(gate) {
4474
4759
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
4475
- if (gate.headless !== false)
4760
+ if (gate.headless !== false && !gate.channel)
4476
4761
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
4477
4762
  if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
4478
4763
  return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
@@ -4480,20 +4765,42 @@ function pauseSkipReason(gate) {
4480
4765
  return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
4481
4766
  return null;
4482
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
+ }
4483
4777
  function atStop(state, callSite) {
4484
- if (callSite && !state.finished) {
4485
- if (state.step) {
4486
- state.step = false;
4487
- 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 };
4488
4783
  }
4489
- if (state.set.matches(callSite)) return { show: "breakpoint" };
4490
4784
  }
4491
4785
  return { go: goOn(state) };
4492
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
+ }
4493
4800
  function goOn(state, answer) {
4494
4801
  if (answer === "step") state.step = true;
4495
4802
  else if (answer === "finish") state.finished = true;
4496
- return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
4803
+ return wantsStops(state) ? "next" : "resume";
4497
4804
  }
4498
4805
  function contextDebugger(context) {
4499
4806
  const dbg = context.debugger;
@@ -4629,38 +4936,83 @@ function logPauseSkipOnce(reason) {
4629
4936
  var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
4630
4937
  var MAX_NAVIGATED = 10;
4631
4938
  var HIGHLIGHT_MS = 2e3;
4632
- async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4633
- 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;
4634
4984
  try {
4635
4985
  const highlight = point.target?.highlight;
4636
4986
  if (typeof highlight === "function") {
4637
- let timer;
4638
- shown = await Promise.race([
4639
- Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
4640
- new Promise((resolve5) => {
4641
- timer = setTimeout(() => resolve5(null), HIGHLIGHT_MS);
4642
- })
4643
- ]);
4644
- clearTimeout(timer);
4987
+ const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
4988
+ shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
4645
4989
  }
4646
4990
  const arg = {
4647
4991
  place: pausePlace(point.location),
4648
4992
  attempt: point.attempt,
4649
4993
  action: point.call,
4650
4994
  locator: null,
4651
- record: !!onRecord
4995
+ record: !!onRecord,
4996
+ ...point.failure ? { failure: true } : {}
4652
4997
  };
4653
4998
  console.log(
4654
- `[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).`
4655
5000
  );
4656
5001
  let navigated = 0;
4657
5002
  for (; ; ) {
4658
5003
  try {
4659
5004
  if (ended?.aborted) return "resume";
4660
- await page.evaluate(showPauseBar, arg);
4661
- 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);
4662
5015
  if (ended?.aborted) return "resume";
4663
- const choice = await page.evaluate(takePauseState);
4664
5016
  navigated = 0;
4665
5017
  if (choice === "resume" || choice === "step" || choice === "finish") return choice;
4666
5018
  if (choice === "record" && onRecord) {
@@ -4670,26 +5022,285 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
4670
5022
  );
4671
5023
  continue;
4672
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
+ }
4673
5032
  if (choice !== "pick") continue;
4674
5033
  const picked = await pickLocator(page, probe, null, ended).catch(() => null);
4675
5034
  if (picked && !ended?.aborted) await onPick(picked);
4676
5035
  } catch (error) {
4677
5036
  if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
4678
- await page.waitForLoadState("domcontentloaded").catch(() => void 0);
5037
+ await unlessEnded(
5038
+ page.waitForLoadState("domcontentloaded").catch(() => void 0),
5039
+ ended
5040
+ );
4679
5041
  }
4680
5042
  }
4681
5043
  } catch {
4682
5044
  return "resume";
4683
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('"')) {
4684
5133
  try {
4685
- if (ended?.aborted) await page.evaluate(takePauseState);
4686
- const dispose = shown?.dispose;
4687
- const hide = page.hideHighlight;
4688
- if (typeof dispose === "function") await dispose.call(shown);
4689
- else if (typeof hide === "function") await hide.call(page);
5134
+ return String(JSON.parse(value));
4690
5135
  } catch {
5136
+ return value;
4691
5137
  }
4692
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 };
4693
5304
  }
4694
5305
 
4695
5306
  // src/internal/capture/record-handoff.ts
@@ -4710,7 +5321,7 @@ function parseRecordChannel(value) {
4710
5321
  const path15 = text.slice(0, hash);
4711
5322
  return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path15.includes("\0") ? { path: path15, token } : null;
4712
5323
  }
4713
- function isObject2(value) {
5324
+ function isObject3(value) {
4714
5325
  return typeof value === "object" && value !== null && !Array.isArray(value);
4715
5326
  }
4716
5327
  function parseHandoffToTest(line) {
@@ -4720,7 +5331,7 @@ function parseHandoffToTest(line) {
4720
5331
  } catch {
4721
5332
  return null;
4722
5333
  }
4723
- if (!isObject2(value)) return null;
5334
+ if (!isObject3(value)) return null;
4724
5335
  switch (value.type) {
4725
5336
  case "refused":
4726
5337
  return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
@@ -4783,8 +5394,8 @@ async function recordFromHere(options) {
4783
5394
  const use = testInfo.project?.use ?? {};
4784
5395
  const context = page.context();
4785
5396
  return new Promise((resolve5) => {
4786
- const socket = net.connect(channel.path);
4787
- socket.setEncoding("utf8");
5397
+ const socket2 = net.connect(channel.path);
5398
+ socket2.setEncoding("utf8");
4788
5399
  const answers = /* @__PURE__ */ new Map();
4789
5400
  let calls = 0;
4790
5401
  let attached = false;
@@ -4792,7 +5403,7 @@ async function recordFromHere(options) {
4792
5403
  let settled = false;
4793
5404
  let initScript;
4794
5405
  const send = (message) => {
4795
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
5406
+ if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
4796
5407
  `);
4797
5408
  };
4798
5409
  const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
@@ -4842,7 +5453,7 @@ async function recordFromHere(options) {
4842
5453
  page.off("close", onGone);
4843
5454
  page.off("crash", onGone);
4844
5455
  ended?.removeEventListener("abort", onEnded);
4845
- socket.end();
5456
+ socket2.end();
4846
5457
  resolve5(recording ? null : said);
4847
5458
  };
4848
5459
  const relayed = (source, payload) => new Promise((answer) => {
@@ -4882,7 +5493,7 @@ async function recordFromHere(options) {
4882
5493
  context.on("page", onPage);
4883
5494
  send({ type: "loaded" });
4884
5495
  };
4885
- socket.on("connect", () => {
5496
+ socket2.on("connect", () => {
4886
5497
  attached = true;
4887
5498
  send({
4888
5499
  type: "attach",
@@ -4891,10 +5502,11 @@ async function recordFromHere(options) {
4891
5502
  line: place.line,
4892
5503
  reason: options.reason,
4893
5504
  testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
4894
- 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 } : {}
4895
5507
  });
4896
5508
  });
4897
- socket.on(
5509
+ socket2.on(
4898
5510
  "data",
4899
5511
  lineReader(
4900
5512
  (line) => {
@@ -4927,11 +5539,255 @@ async function recordFromHere(options) {
4927
5539
  () => finish("the editor sent a message too long to read.")
4928
5540
  )
4929
5541
  );
4930
- socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
4931
- 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."));
4932
5544
  });
4933
5545
  }
4934
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
+
4935
5791
  // src/internal/collect/coverage-settings.ts
4936
5792
  var SERVER_VALUES = /* @__PURE__ */ new Set(["files", "functions", "lines"]);
4937
5793
  function isServerGranularity(value) {
@@ -4953,7 +5809,7 @@ function browserFunctionCoverage(files) {
4953
5809
  files: [...files].map(([path15, functions]) => ({ path: path15, runtime: "browser", functions }))
4954
5810
  });
4955
5811
  }
4956
- function newKnownFunctions(known, sent) {
5812
+ function newKnownFunctions(known, sent2) {
4957
5813
  const parts = [];
4958
5814
  let files = [];
4959
5815
  let budget = MAX_COVERAGE_RANGES;
@@ -4965,10 +5821,10 @@ function newKnownFunctions(known, sent) {
4965
5821
  for (const [raw, ranges] of known) {
4966
5822
  const path15 = normalizeCoveragePath(raw);
4967
5823
  if (path15 === null) continue;
4968
- let named = sent.get(path15);
5824
+ let named = sent2.get(path15);
4969
5825
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4970
5826
  if (fresh.length === 0) continue;
4971
- if (!named) sent.set(path15, named = /* @__PURE__ */ new Set());
5827
+ if (!named) sent2.set(path15, named = /* @__PURE__ */ new Set());
4972
5828
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4973
5829
  while (fresh.length > 0) {
4974
5830
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
@@ -4991,15 +5847,15 @@ var os3 = __toESM(require("os"));
4991
5847
  var fs7 = __toESM(require("fs"));
4992
5848
 
4993
5849
  // src/internal/support/instance-id.ts
4994
- var crypto = __toESM(require("crypto"));
5850
+ var crypto2 = __toESM(require("crypto"));
4995
5851
  var os2 = __toESM(require("os"));
4996
5852
  function hashForProject(projectName) {
4997
- return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
5853
+ return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
4998
5854
  }
4999
5855
 
5000
5856
  // src/internal/support/aria-sampling.ts
5001
- function ariaSampleIdentity(filePath, title) {
5002
- return `${filePath}\0${title}`;
5857
+ function ariaSampleIdentity(filePath2, title) {
5858
+ return `${filePath2}\0${title}`;
5003
5859
  }
5004
5860
  function getAriaSampleFilePath(projectName) {
5005
5861
  return path10.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
@@ -5031,7 +5887,7 @@ function isDueForAriaSample(testInfo) {
5031
5887
  }
5032
5888
 
5033
5889
  // src/internal/capture/code-reach.ts
5034
- var crypto2 = __toESM(require("crypto"));
5890
+ var crypto3 = __toESM(require("crypto"));
5035
5891
  var fs8 = __toESM(require("fs"));
5036
5892
  var path11 = __toESM(require("path"));
5037
5893
  var import_node_child_process = require("child_process");
@@ -5119,9 +5975,9 @@ function resolveSourcePath(source, roots) {
5119
5975
  const normalized = normalizeSourcePath(source);
5120
5976
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
5121
5977
  if (path11.isAbsolute(normalized) && isFile(normalized)) return path11.resolve(normalized);
5122
- const relative7 = normalized.replace(/^\/+/, "");
5978
+ const relative8 = normalized.replace(/^\/+/, "");
5123
5979
  for (const root of roots) {
5124
- const candidate = path11.resolve(root, relative7);
5980
+ const candidate = path11.resolve(root, relative8);
5125
5981
  if (isFile(candidate)) return candidate;
5126
5982
  }
5127
5983
  return null;
@@ -5156,7 +6012,7 @@ function relativeToRepo(file, repoRoot) {
5156
6012
  return path11.relative(repoRoot, file).split(path11.sep).join("/");
5157
6013
  }
5158
6014
  function scriptVersion(entry) {
5159
- return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
6015
+ return entry.hash ?? crypto3.createHash("sha1").update(entry.source ?? "").digest("hex");
5160
6016
  }
5161
6017
  async function loadMap(entry, fetchMap) {
5162
6018
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -5199,7 +6055,7 @@ async function resolveScript(entry, roots, bases, fetchMap) {
5199
6055
  if (text) scriptCache.set(entry.url, version, resolution);
5200
6056
  return resolution;
5201
6057
  }
5202
- function textOf2(entry, resolution) {
6058
+ function textOf3(entry, resolution) {
5203
6059
  return resolution.text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
5204
6060
  }
5205
6061
  async function resolveCodeReach(entries, roots, fetchMap, pageUrl) {
@@ -5245,7 +6101,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5245
6101
  };
5246
6102
  if (resolution.kind === "file") {
5247
6103
  if (!resolution.file) continue;
5248
- const text = textOf2(covered, resolution);
6104
+ const text = textOf3(covered, resolution);
5249
6105
  if (resolution.lookup) {
5250
6106
  const reached = reachedFunctionLines(covered, resolution.lookup, text);
5251
6107
  if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
@@ -5253,7 +6109,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5253
6109
  add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
5254
6110
  }
5255
6111
  } else if (resolution.kind === "map") {
5256
- 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))) {
5257
6113
  const file = mapFile(resolution, index);
5258
6114
  if (file) add2(file, functions);
5259
6115
  }
@@ -5263,7 +6119,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
5263
6119
  let listed = knownCache.get(covered.url, version);
5264
6120
  if (!listed || listed.count < covered.functions.length) {
5265
6121
  const byFile = /* @__PURE__ */ new Map();
5266
- const text = textOf2(covered, resolution);
6122
+ const text = textOf3(covered, resolution);
5267
6123
  if (resolution.kind === "file" && resolution.file) {
5268
6124
  const lines = knownFunctionLines(covered, resolution.lookup, text);
5269
6125
  const all = resolution.lookup ? [...lines.values()].flat() : lines.get(-1) ?? [];
@@ -5562,7 +6418,7 @@ async function stopPageCoverage(page) {
5562
6418
  }
5563
6419
 
5564
6420
  // src/internal/capture/css-coverage.ts
5565
- var crypto3 = __toESM(require("crypto"));
6421
+ var crypto4 = __toESM(require("crypto"));
5566
6422
  var fs9 = __toESM(require("fs"));
5567
6423
 
5568
6424
  // ../core/src/css-rules.ts
@@ -5777,7 +6633,7 @@ async function resolveSheet(sheet, text, dirs, fetchMap) {
5777
6633
  return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
5778
6634
  }
5779
6635
  function cachedResolution(sheet, text, dirs, fetchMap) {
5780
- const version = crypto3.createHash("sha1").update(text).digest("hex");
6636
+ const version = crypto4.createHash("sha1").update(text).digest("hex");
5781
6637
  const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
5782
6638
  " "
5783
6639
  );
@@ -5823,7 +6679,9 @@ async function resolveCssCoverage(sheets, roots, fetchMap) {
5823
6679
  }
5824
6680
 
5825
6681
  // src/internal/capture/resource-ledger.ts
6682
+ var childProcess = __toESM(require("child_process"));
5826
6683
  var fs11 = __toESM(require("fs"));
6684
+ var net3 = __toESM(require("net"));
5827
6685
  var path13 = __toESM(require("path"));
5828
6686
  var import_node_module2 = require("module");
5829
6687
 
@@ -6003,6 +6861,8 @@ var WorkerMetrics = class {
6003
6861
  constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
6004
6862
  this.proc = proc;
6005
6863
  this.pid = pid;
6864
+ /** The lowest heap read right after a full garbage collection in the current window. */
6865
+ this.heapAfterGc = null;
6006
6866
  this.loop = null;
6007
6867
  this.usage = null;
6008
6868
  /** Each process's CPU and wait at the start of the current window; null before the first. */
@@ -6017,6 +6877,28 @@ var WorkerMetrics = class {
6017
6877
  histogram = null;
6018
6878
  }
6019
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
+ }
6020
6902
  }
6021
6903
  /** The worker's processes under it, keyed by pid and start time. */
6022
6904
  tree() {
@@ -6050,6 +6932,7 @@ var WorkerMetrics = class {
6050
6932
  start(processes = true) {
6051
6933
  try {
6052
6934
  this.histogram?.reset();
6935
+ this.heapAfterGc = null;
6053
6936
  this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
6054
6937
  this.usage = process.resourceUsage();
6055
6938
  if (this.linux && processes && !this.subtree) this.snapshot();
@@ -6072,6 +6955,7 @@ var WorkerMetrics = class {
6072
6955
  loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
6073
6956
  loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
6074
6957
  heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
6958
+ heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
6075
6959
  fds: this.linux ? this.proc.fdCount(this.pid) : null
6076
6960
  }
6077
6961
  };
@@ -6106,6 +6990,7 @@ var WorkerMetrics = class {
6106
6990
  }
6107
6991
  dispose() {
6108
6992
  this.histogram?.disable();
6993
+ this.gcObserver?.disconnect();
6109
6994
  }
6110
6995
  };
6111
6996
  var PageReader = class {
@@ -6127,12 +7012,36 @@ var PageReader = class {
6127
7012
  try {
6128
7013
  const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
6129
7014
  const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
6130
- return {
7015
+ const main = {
6131
7016
  cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
6132
7017
  heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
6133
7018
  nodes: values.get("Nodes") ?? 0,
6134
7019
  listeners: values.get("JSEventListeners") ?? 0
6135
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;
6136
7045
  } catch {
6137
7046
  return null;
6138
7047
  }
@@ -6141,6 +7050,9 @@ var PageReader = class {
6141
7050
 
6142
7051
  // src/internal/capture/resource-ledger.ts
6143
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;
6144
7056
  var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
6145
7057
  var CLOSE_TIMEOUT_MS = 5e3;
6146
7058
  var PAGE_READ_CAP = 50;
@@ -6216,20 +7128,41 @@ function playwrightTestInfoSource() {
6216
7128
  function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
6217
7129
  return value === "fail" || value === "close" ? value : "report";
6218
7130
  }
6219
- function countHandles() {
7131
+ function countHandles(types = TRACKED_HANDLE_TYPES) {
6220
7132
  const counts = {};
6221
7133
  const read = process.getActiveResourcesInfo;
6222
7134
  if (typeof read !== "function") return counts;
6223
7135
  for (const type of read.call(process)) {
6224
- if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7136
+ if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
6225
7137
  }
6226
7138
  return counts;
6227
7139
  }
6228
- async function settledHandles(start) {
6229
- const end = countHandles();
7140
+ async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
7141
+ const end = countHandles(types);
6230
7142
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
6231
7143
  await new Promise((resolve5) => setTimeout(resolve5, 0));
6232
- 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";
6233
7166
  }
6234
7167
  function settleWithin(promise, ms) {
6235
7168
  let timer;
@@ -6241,7 +7174,7 @@ function settleWithin(promise, ms) {
6241
7174
  ]).finally(() => clearTimeout(timer));
6242
7175
  }
6243
7176
  var ResourceLedger = class {
6244
- 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) {
6245
7178
  this.playwright = playwright;
6246
7179
  this.workerIndex = workerIndex;
6247
7180
  this.leakCheck = leakCheck;
@@ -6249,6 +7182,7 @@ var ResourceLedger = class {
6249
7182
  this.testInfoSource = testInfoSource;
6250
7183
  this.now = now;
6251
7184
  this.readers = readers;
7185
+ this.nodeHandles = nodeHandles;
6252
7186
  this.nextId = 1;
6253
7187
  this.entries = /* @__PURE__ */ new Map();
6254
7188
  this.byObject = /* @__PURE__ */ new WeakMap();
@@ -6265,6 +7199,11 @@ var ResourceLedger = class {
6265
7199
  this.listener = null;
6266
7200
  /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
6267
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 = [];
6268
7207
  }
6269
7208
  /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
6270
7209
  install() {
@@ -6297,13 +7236,107 @@ var ResourceLedger = class {
6297
7236
  for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
6298
7237
  if (type) this.wrapBrowserType(type);
6299
7238
  }
7239
+ if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
6300
7240
  return true;
6301
7241
  }
6302
- /** 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. */
6303
7243
  uninstall() {
6304
7244
  const remove = this.playwright?._instrumentation?.removeListener;
6305
7245
  if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
6306
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 } : {} }));
6307
7340
  }
6308
7341
  /** Mark a page used: a locator was built on it. */
6309
7342
  notePageUse(page) {
@@ -6313,7 +7346,7 @@ var ResourceLedger = class {
6313
7346
  /** Open the census window of a test. */
6314
7347
  testStarted(info) {
6315
7348
  this.window = testRef2(info);
6316
- this.handlesAtStart = countHandles();
7349
+ this.handlesAtStart = countHandles(this.countedHandleTypes());
6317
7350
  this.openAtStart = { contexts: 0, pages: 0 };
6318
7351
  for (const entry of this.openEntries()) {
6319
7352
  if (entry.birth.kind === "page") this.openAtStart.pages++;
@@ -6333,9 +7366,14 @@ var ResourceLedger = class {
6333
7366
  if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
6334
7367
  const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
6335
7368
  const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
6336
- 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;
6337
7374
  const census = this.census(ref, handles, await this.readPages());
6338
7375
  if (metrics) census.metrics = metrics;
7376
+ if (running) census.running = running;
6339
7377
  if (test) {
6340
7378
  census.openAtStart = this.openAtStart;
6341
7379
  if (!failed) census.leftOpen = leaks.length;
@@ -6374,6 +7412,10 @@ var ResourceLedger = class {
6374
7412
  this.uninstall();
6375
7413
  return census;
6376
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
+ }
6377
7419
  guard(run) {
6378
7420
  try {
6379
7421
  run();
@@ -6596,6 +7638,7 @@ var ResourceLedger = class {
6596
7638
  open
6597
7639
  };
6598
7640
  if (handles) census.handles = handles;
7641
+ for (const close of this.closed) this.entries.delete(close.id);
6599
7642
  this.born = [];
6600
7643
  this.closed = [];
6601
7644
  return census;
@@ -6634,7 +7677,8 @@ function startResourceLedger(playwright, workerIndex) {
6634
7677
  process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
6635
7678
  playwrightTestInfoSource(),
6636
7679
  Date.now,
6637
- { metrics, pages: new PageReader() }
7680
+ { metrics, pages: new PageReader() },
7681
+ true
6638
7682
  );
6639
7683
  if (!ledger.install()) {
6640
7684
  metrics.dispose();
@@ -6985,10 +8029,15 @@ function createSink() {
6985
8029
  locatorCalls: /* @__PURE__ */ new Map(),
6986
8030
  stops: [],
6987
8031
  shownStops: /* @__PURE__ */ new Map(),
8032
+ failureHeld: false,
8033
+ frozenAt: null,
8034
+ stopEnds: 0,
8035
+ ending: false,
6988
8036
  released: [],
6989
8037
  releasedRequests: /* @__PURE__ */ new Set(),
6990
8038
  onReleasedRequestsEnded: null,
6991
8039
  releasedQuietSince: 0,
8040
+ releasedUntil: null,
6992
8041
  locatorPages: new LocatorPageLog(),
6993
8042
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
6994
8043
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -7092,6 +8141,25 @@ async function drainPendingProbes(capMs) {
7092
8141
  ]);
7093
8142
  clearTimeout(cap);
7094
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
+ }
7095
8163
  function isPageClosed(page) {
7096
8164
  try {
7097
8165
  return typeof page.isClosed === "function" ? page.isClosed() : false;
@@ -7214,7 +8282,7 @@ function buildPageState(raw, cookies, interaction) {
7214
8282
  }
7215
8283
  async function readPageState(page) {
7216
8284
  try {
7217
- const raw = await page.evaluate(() => {
8285
+ const readRaw = page.evaluate(() => {
7218
8286
  const listStorage = (s) => {
7219
8287
  const out = [];
7220
8288
  try {
@@ -7240,14 +8308,16 @@ async function readPageState(page) {
7240
8308
  sessionStorage: listStorage(globalThis.sessionStorage)
7241
8309
  };
7242
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]);
7243
8320
  if (!raw) return null;
7244
- let cookies = null;
7245
- try {
7246
- cookies = await pageContext(page)?.cookies() ?? null;
7247
- } catch {
7248
- cookies = null;
7249
- }
7250
- const interaction = await page.evaluate(readPageInteraction).catch(() => null);
7251
8321
  return buildPageState(raw, cookies, interaction);
7252
8322
  } catch {
7253
8323
  return null;
@@ -7311,7 +8381,7 @@ async function recordPageWindow(sink, page) {
7311
8381
  }
7312
8382
  async function stashPageState(sink, closing) {
7313
8383
  const page = sink.lastActivePage;
7314
- if (!page || isPageClosed(page)) return;
8384
+ if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
7315
8385
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
7316
8386
  if (!belongsToClosing) return;
7317
8387
  await internalCall(page, () => readPageBeforeClose(sink, page));
@@ -7323,13 +8393,12 @@ async function readBlankPage(page) {
7323
8393
  return aria != null && aria.trim() === "";
7324
8394
  }
7325
8395
  async function readPageBeforeClose(sink, page) {
7326
- 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]);
7327
8399
  if (vitals) sink.stashedWebVitals = vitals;
7328
- if (sink.probeItem?.level === "server") sink.probeBlankPage = await readBlankPage(page);
7329
- if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
7330
- const pageState = await readPageState(page);
7331
- if (pageState) sink.stashedPageState = pageState;
7332
- }
8400
+ if (blankPage !== null) sink.probeBlankPage = blankPage;
8401
+ if (pageState) sink.stashedPageState = pageState;
7333
8402
  const status = sink.testInfo?.status;
7334
8403
  const sampleAria = async () => {
7335
8404
  const root = page.locator(":root");
@@ -7523,16 +8592,35 @@ function locatorSource(target, method, args) {
7523
8592
  return renderFailing({ method, args });
7524
8593
  }
7525
8594
  var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
8595
+ var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
8596
+ var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
7526
8597
  var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
7527
- 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) {
7528
8615
  const state = currentSink?.pause;
7529
- if (!state || state.finished || state.set.size === 0) return;
8616
+ if (!state || !wantsStops(state)) return;
7530
8617
  await Promise.all(
7531
8618
  [...contexts].map(
7532
8619
  (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
7533
8620
  )
7534
8621
  );
7535
8622
  }
8623
+ var stopTurn = Promise.resolve();
7536
8624
  function onDebuggerStop(context) {
7537
8625
  const details = contextDebugger(context)?.pausedDetails();
7538
8626
  if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
@@ -7543,13 +8631,38 @@ function onDebuggerStop(context) {
7543
8631
  try {
7544
8632
  if (!sink || !state) return;
7545
8633
  const callSite = stopSite(sink, details.location);
8634
+ const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
7546
8635
  const pagePause = PAGE_PAUSES.get(context);
8636
+ const asked = askedOnce(state);
7547
8637
  const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
7548
8638
  if ("go" in decided) {
7549
8639
  how = decided.go;
7550
8640
  return;
7551
8641
  }
7552
- 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);
7553
8666
  } catch {
7554
8667
  if (state) how = goOn(state);
7555
8668
  } finally {
@@ -7557,22 +8670,30 @@ function onDebuggerStop(context) {
7557
8670
  }
7558
8671
  })();
7559
8672
  }
8673
+ function endedWaiting(sink, call) {
8674
+ releaseCall(sink, call?.settled ?? Promise.resolve());
8675
+ return "finish";
8676
+ }
7560
8677
  function stopSite(sink, location) {
7561
8678
  if (path14.dirname(path14.resolve(location.file)) !== __dirname) return stopCallSite(location);
7562
8679
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
7563
8680
  }
7564
- function stopPage(sink, context, call) {
7565
- for (const page of [call?.page, sink.lastActivePage]) {
7566
- if (page && !isPageClosed(page) && pageContext(page) === context) return page;
7567
- }
7568
- 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);
7569
8686
  return open[open.length - 1] ?? null;
7570
8687
  }
7571
- async function showStop(sink, context, title, location, reason) {
8688
+ async function showStop(sink, context, title, location, reason, call, of, failure = null) {
7572
8689
  const testInfo = sink.testInfo;
7573
- const call = sink.locatorCalls.get(callSiteLine(location));
7574
- const page = stopPage(sink, context, call);
7575
- 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;
7576
8697
  const onPick = async (picked) => {
7577
8698
  if (call) {
7578
8699
  call.picked = true;
@@ -7583,16 +8704,57 @@ async function showStop(sink, context, title, location, reason) {
7583
8704
  ...picked
7584
8705
  });
7585
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);
7586
8714
  }
7587
8715
  console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
7588
- await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
8716
+ await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
7589
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;
7590
8721
  const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
7591
8722
  sink.stops.push(record);
7592
8723
  const end = new AbortController();
7593
- const shown = withTestTimeoutHeld(
7594
- testInfo,
7595
- () => 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(
7596
8758
  page,
7597
8759
  () => pauseHere(
7598
8760
  page,
@@ -7600,45 +8762,221 @@ async function showStop(sink, context, title, location, reason) {
7600
8762
  location,
7601
8763
  call: title,
7602
8764
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
7603
- target: call?.page === page ? call.target : null
8765
+ target: call?.page === page ? call.target : null,
8766
+ ...failure ? { failure: true } : {}
7604
8767
  },
7605
8768
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
7606
8769
  onPick,
7607
- recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
7608
- end.signal
8770
+ onRecord,
8771
+ over.signal,
8772
+ intents ?? void 0,
8773
+ onIntent
7609
8774
  )
7610
- )
7611
- ).finally(() => {
8775
+ );
8776
+ }).finally(async () => {
7612
8777
  if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
7613
8778
  else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
7614
8779
  sink.shownStops.delete(end);
8780
+ await held2?.release();
7615
8781
  });
7616
8782
  const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
7617
- sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
7618
- const answer = await shown;
7619
- if (!end.signal.aborted) return answer;
7620
- 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
+ });
7621
8951
  }
7622
8952
  var END_STOP_MS = 2e3;
7623
8953
  var RELEASED_CALL_MS = 2e3;
7624
8954
  var RELEASED_QUIET_MS = 200;
7625
8955
  async function endShownStops(sink, why, closing) {
7626
8956
  const ending = [];
8957
+ if (why === "ended") sink.stopEnds++;
7627
8958
  let heldAt = Infinity;
7628
- for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
7629
- 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;
7630
8961
  if (!end.signal.aborted) {
7631
- 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;
7632
8964
  end.abort(why);
7633
- if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
8965
+ if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
7634
8966
  }
7635
- ending.push(done);
8967
+ ending.push(stop.done);
7636
8968
  }
7637
8969
  if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
7638
8970
  await settledWithin(ending, END_STOP_MS);
7639
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
+ }
7640
8977
  function releaseCall(sink, settled) {
7641
8978
  sink.releasedQuietSince = Date.now();
8979
+ sink.releasedUntil = null;
7642
8980
  sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
7643
8981
  }
7644
8982
  function releasedRequestsEnded(sink) {
@@ -7647,8 +8985,8 @@ function releasedRequestsEnded(sink) {
7647
8985
  }
7648
8986
  async function awaitReleasedCalls(sink) {
7649
8987
  if (sink.released.length === 0) return;
7650
- const deadline = Date.now() + RELEASED_CALL_MS;
7651
- 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()));
7652
8990
  for (let now = Date.now(); now < deadline; now = Date.now()) {
7653
8991
  if (sink.releasedRequests.size > 0) {
7654
8992
  await settledWithin([releasedRequestsEnded(sink)], deadline - now);
@@ -7678,7 +9016,7 @@ function recorderAt(page, testInfo, location, reason) {
7678
9016
  return async (ended) => {
7679
9017
  console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
7680
9018
  const why = await recordFromHere({ page, testInfo, location, reason, ended });
7681
- 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.");
7682
9020
  else
7683
9021
  console.log(
7684
9022
  why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
@@ -7695,10 +9033,10 @@ async function pauseInPlace(page, playwrightPause, location) {
7695
9033
  if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
7696
9034
  return leaveToPlaywright(context, playwrightPause);
7697
9035
  }
7698
- sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
9036
+ sink.pause ??= pauseState(NO_BREAKPOINTS);
7699
9037
  watchStops(context, onDebuggerStop);
7700
9038
  await steerDebugger(context, "pause");
7701
- PAGE_PAUSES.set(context, location);
9039
+ PAGE_PAUSES.set(context, { location, page });
7702
9040
  try {
7703
9041
  await playwrightPause({ __testHookKeepTestTimeout: true });
7704
9042
  } finally {
@@ -7881,15 +9219,15 @@ function instrumentPage(page) {
7881
9219
  if (typeof page.close === "function") {
7882
9220
  const originalClose = page.close.bind(page);
7883
9221
  page.close = async (...args) => {
7884
- await drainPendingProbes(1e3);
7885
- const sink = currentSink;
7886
- if (sink) {
7887
- await endShownStops(sink, "closed", { page });
7888
- await stashPageState(sink, { page });
7889
- await maybeOpenPicker(sink, { page });
7890
- 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();
7891
9230
  }
7892
- return originalClose(...args);
7893
9231
  };
7894
9232
  }
7895
9233
  const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
@@ -7934,7 +9272,7 @@ function instrumentPage(page) {
7934
9272
  });
7935
9273
  page.on("console", (msg) => {
7936
9274
  const sink = currentSink;
7937
- if (!sink) return;
9275
+ if (!sink || sink.frozenAt !== null) return;
7938
9276
  const type = msg.type();
7939
9277
  if (["warning", "error", "assert"].includes(type)) {
7940
9278
  const location = msg.location();
@@ -7950,7 +9288,7 @@ function instrumentPage(page) {
7950
9288
  try {
7951
9289
  page.on("dialogclosed", (dialog) => {
7952
9290
  const sink = currentSink;
7953
- if (!sink) return;
9291
+ if (!sink || sink.frozenAt !== null) return;
7954
9292
  try {
7955
9293
  sink.dialogs.push({
7956
9294
  type: typeof dialog.type === "function" ? dialog.type() : null,
@@ -7969,12 +9307,14 @@ function instrumentPage(page) {
7969
9307
  if (!sink) return;
7970
9308
  try {
7971
9309
  const request = response.request();
9310
+ if (HELD_REQUESTS.has(request)) return;
7972
9311
  if (isTrackedRequest(request.url(), request.resourceType())) sink.openResponses.set(request, response);
7973
9312
  } catch {
7974
9313
  }
7975
9314
  });
7976
9315
  page.on("request", (request) => {
7977
9316
  const sink = currentSink;
9317
+ if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request);
7978
9318
  if (!sink || sink.released.length === 0) return;
7979
9319
  sink.releasedRequests.add(request);
7980
9320
  sink.releasedQuietSince = Date.now();
@@ -7985,16 +9325,18 @@ function instrumentPage(page) {
7985
9325
  sink.releasedQuietSince = Date.now();
7986
9326
  if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
7987
9327
  }
7988
- if (!sink || RECORDED_OPEN.has(request)) return;
9328
+ if (!sink || RECORDED_OPEN.has(request) || HELD_REQUESTS.has(request)) return;
7989
9329
  sink.lastActivePage = page;
7990
9330
  const response = sink.openResponses.get(request) ?? null;
7991
9331
  sink.openResponses.delete(request);
7992
9332
  recordRequest(sink, request, failed ? "failed" : "finished", response);
9333
+ if (!failed) void keepResponse(request, response);
7993
9334
  };
7994
9335
  page.on("requestfinished", (request) => onRequestEnd(request, false));
7995
9336
  page.on("requestfailed", (request) => onRequestEnd(request, true));
7996
9337
  }
7997
9338
  var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
9339
+ var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
7998
9340
  function isTrackedRequest(url, resourceType) {
7999
9341
  if (url.startsWith("data:") || url.startsWith("blob:")) return false;
8000
9342
  return TRACKED_REQUEST_TYPES.includes(resourceType);
@@ -8056,6 +9398,7 @@ function readResponseHeaders(sink, entry, url, headers) {
8056
9398
  }
8057
9399
  }
8058
9400
  function recordApiCall(sink, call) {
9401
+ if (sink.frozenAt !== null) return;
8059
9402
  try {
8060
9403
  const entry = apiCallEntry(call, sink.baseURL);
8061
9404
  if (!entry) return;
@@ -8092,7 +9435,10 @@ function instrumentContext(context) {
8092
9435
  INSTRUMENTED_CONTEXTS.add(context);
8093
9436
  if (typeof context.once === "function") {
8094
9437
  OPEN_CONTEXTS.add(context);
8095
- context.once("close", () => OPEN_CONTEXTS.delete(context));
9438
+ context.once("close", () => {
9439
+ OPEN_CONTEXTS.delete(context);
9440
+ CLOSING_CONTEXTS.add(context);
9441
+ });
8096
9442
  }
8097
9443
  observeOwnApiRequests(context);
8098
9444
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
@@ -8110,14 +9456,7 @@ function instrumentContext(context) {
8110
9456
  if (typeof context.close === "function") {
8111
9457
  const originalClose = context.close.bind(context);
8112
9458
  context.close = async (...args) => {
8113
- await drainPendingProbes(1e3);
8114
- const sink = currentSink;
8115
- if (sink) {
8116
- await endShownStops(sink, "closed", { context });
8117
- await stashPageState(sink, { context });
8118
- await maybeOpenPicker(sink, { context });
8119
- await stopSinkCodeReach(sink, { context });
8120
- }
9459
+ await settleContextClose(context);
8121
9460
  return originalClose(...args);
8122
9461
  };
8123
9462
  }
@@ -8130,7 +9469,7 @@ function patchBrowser(browser) {
8130
9469
  browser.newPage = async (...args) => {
8131
9470
  const page = await originalNewPage(...args);
8132
9471
  instrumentPage(page);
8133
- await armBreakpoints([pageContext(page)]);
9472
+ await armStops([pageContext(page)]);
8134
9473
  await awaitFlakeConditions();
8135
9474
  await maybeStartCodeReach(page);
8136
9475
  return page;
@@ -8139,13 +9478,16 @@ function patchBrowser(browser) {
8139
9478
  browser.newContext = async (...args) => {
8140
9479
  const context = await originalNewContext(...args);
8141
9480
  instrumentContext(context);
8142
- await armBreakpoints([context]);
9481
+ await armStops([context]);
8143
9482
  return context;
8144
9483
  };
8145
9484
  if (typeof browser.close === "function") {
8146
9485
  const originalClose = browser.close.bind(browser);
8147
9486
  browser.close = async (...args) => {
8148
9487
  await drainPendingProbes(1e3);
9488
+ for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
9489
+ await settleContextClose(context);
9490
+ }
8149
9491
  return originalClose(...args);
8150
9492
  };
8151
9493
  }
@@ -8198,7 +9540,11 @@ async function flushSink(sink, testInfo) {
8198
9540
  clearTimeout(drainDeadline);
8199
9541
  const page = sink.lastActivePage;
8200
9542
  const pageReadable = page !== null && !isPageClosed(page);
8201
- if (pageReadable) await maybeOpenPicker(sink);
9543
+ if (pageReadable) {
9544
+ await holdFailure(sink);
9545
+ await maybeOpenPicker(sink);
9546
+ }
9547
+ const live = pageReadable && sink.frozenAt === null;
8202
9548
  if (sink.stops.length > 0) {
8203
9549
  await testInfo.attach(ATTACHMENT_NAMES.pauses, {
8204
9550
  contentType: "application/json",
@@ -8266,13 +9612,13 @@ async function flushSink(sink, testInfo) {
8266
9612
  }
8267
9613
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
8268
9614
  try {
8269
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
9615
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
8270
9616
  if (snapshot) {
8271
9617
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8272
- contentType: "text/plain",
9618
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
8273
9619
  body: snapshot
8274
9620
  });
8275
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
9621
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8276
9622
  if (snapshotJson) {
8277
9623
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8278
9624
  contentType: "application/json",
@@ -8297,7 +9643,7 @@ async function flushSink(sink, testInfo) {
8297
9643
  }
8298
9644
  if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
8299
9645
  const option = sink.failureScreenshot;
8300
- 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;
8301
9647
  if (shot) {
8302
9648
  try {
8303
9649
  await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
@@ -8307,13 +9653,13 @@ async function flushSink(sink, testInfo) {
8307
9653
  }
8308
9654
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
8309
9655
  try {
8310
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
9656
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
8311
9657
  if (snapshot) {
8312
9658
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
8313
- contentType: "text/plain",
9659
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
8314
9660
  body: snapshot
8315
9661
  });
8316
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
9662
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
8317
9663
  if (snapshotJson) {
8318
9664
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
8319
9665
  contentType: "application/json",
@@ -8359,7 +9705,7 @@ async function flushSink(sink, testInfo) {
8359
9705
  body: Buffer.from(JSON.stringify(sink.networkRequests))
8360
9706
  });
8361
9707
  }
8362
- const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
9708
+ const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
8363
9709
  if (webVitals) {
8364
9710
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
8365
9711
  contentType: "application/json",
@@ -8367,7 +9713,7 @@ async function flushSink(sink, testInfo) {
8367
9713
  });
8368
9714
  }
8369
9715
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
8370
- const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
9716
+ const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
8371
9717
  if (pageState) {
8372
9718
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
8373
9719
  contentType: "application/json",
@@ -8376,7 +9722,7 @@ async function flushSink(sink, testInfo) {
8376
9722
  }
8377
9723
  }
8378
9724
  if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
8379
- if (pageReadable) await recordPageWindow(sink, page);
9725
+ if (live) await recordPageWindow(sink, page);
8380
9726
  const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
8381
9727
  if (windows.length > 0) {
8382
9728
  await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
@@ -8386,21 +9732,79 @@ async function flushSink(sink, testInfo) {
8386
9732
  }
8387
9733
  }
8388
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
+ }
8389
9789
  var piwiFixtures = {
8390
9790
  // Auto, worker-scoped: the worker's resource ledger, and the API request
8391
9791
  // contexts its tests create, handed out behind the call proxy. Set up before
8392
9792
  // any hook (worker fixtures come first), so a `beforeAll` context is in the
8393
9793
  // ledger and records the calls the tests make through it, and torn down after
8394
9794
  // the browser closed, so its census sees what only the worker's shutdown
8395
- // 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.
8396
9798
  piwiResources: [
8397
9799
  async ({ playwright }, use, workerInfo) => {
8398
9800
  boxCaptureFrames();
8399
9801
  startResourceLedger(playwright, workerInfo.workerIndex);
8400
9802
  observeApiRequests(playwright, apiCallRecorder);
9803
+ await openRunChannel(workerInfo, runControl);
8401
9804
  try {
8402
9805
  await use();
8403
9806
  } finally {
9807
+ closeRunChannel();
8404
9808
  stopResourceLedger();
8405
9809
  }
8406
9810
  },
@@ -8420,10 +9824,12 @@ var piwiFixtures = {
8420
9824
  // never double-wraps a page the browser patch already instrumented.
8421
9825
  page: async ({ page }, use) => {
8422
9826
  instrumentPage(page);
8423
- await armBreakpoints([pageContext(page)]);
9827
+ await armStops([pageContext(page)]);
8424
9828
  await awaitFlakeConditions();
8425
9829
  await maybeStartCodeReach(page);
8426
9830
  await use(page);
9831
+ const sink = currentSink;
9832
+ if (sink) await holdFailure(sink, page);
8427
9833
  },
8428
9834
  // Auto, test-scoped: open a capture sink for the running test and flush it
8429
9835
  // (attach the collected data) at teardown. Runs for every test without being
@@ -8438,12 +9844,16 @@ var piwiFixtures = {
8438
9844
  sink.baseURL = baseURL ?? null;
8439
9845
  if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
8440
9846
  const pauseSet = currentPauseSet();
8441
- if (pauseSet.size) {
8442
- const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
8443
- if (reason) logPauseSkipOnce(reason);
8444
- 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);
8445
9852
  }
9853
+ pauseNextTest = false;
8446
9854
  if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
9855
+ if (channel) tellHub(testBegin(testInfo, cannotPause));
9856
+ if (channel && sink.pause) startKeeping();
8447
9857
  if (isFlakeMode()) {
8448
9858
  const plan = loadFlakePlan();
8449
9859
  const role = flakeRoleForTest(plan, {
@@ -8467,14 +9877,18 @@ var piwiFixtures = {
8467
9877
  });
8468
9878
  activeResourceLedger()?.testStarted(testInfo);
8469
9879
  currentSink = sink;
9880
+ const showRunChannel = hideRunChannel();
8470
9881
  try {
8471
- await armBreakpoints(OPEN_CONTEXTS);
9882
+ await armStops(OPEN_CONTEXTS);
8472
9883
  await startCodeReachOnOpenPages();
8473
9884
  await use();
8474
9885
  } finally {
9886
+ sink.ending = true;
8475
9887
  await endShownStops(sink, "ended");
8476
9888
  await awaitReleasedCalls(sink);
9889
+ showRunChannel();
8477
9890
  currentSink = null;
9891
+ if (sink.pause?.pause) pauseNextTest = true;
8478
9892
  if (sink.probeItem) {
8479
9893
  const applied = sink.probeInterception?.applied() ?? false;
8480
9894
  const level = sink.probeItem.level ?? "client";
@@ -8497,7 +9911,12 @@ var piwiFixtures = {
8497
9911
  });
8498
9912
  }
8499
9913
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
8500
- await flushSink(sink, testInfo);
9914
+ try {
9915
+ await flushSink(sink, testInfo);
9916
+ } finally {
9917
+ stopKeeping();
9918
+ if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
9919
+ }
8501
9920
  await recordResourceCensus(testInfo);
8502
9921
  }
8503
9922
  },
@@ -8543,6 +9962,8 @@ function extendPiwiFixtures(test) {
8543
9962
  computeCoreVitals,
8544
9963
  configuredCodeReachRoots,
8545
9964
  extendPiwiFixtures,
9965
+ failureError,
9966
+ intentAt,
8546
9967
  internalCall,
8547
9968
  noteLocatorCall,
8548
9969
  noteNavigation,