@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.
package/dist/index.js CHANGED
@@ -65,9 +65,9 @@ function processAlive(pid) {
65
65
  return error.code === "EPERM";
66
66
  }
67
67
  }
68
- function readDesktopConfig(filePath2) {
68
+ function readDesktopConfig(filePath3) {
69
69
  try {
70
- const parsed2 = JSON.parse(fs.readFileSync(filePath2, "utf8"));
70
+ const parsed2 = JSON.parse(fs.readFileSync(filePath3, "utf8"));
71
71
  const url = parsed2?.url;
72
72
  const token = parsed2?.token;
73
73
  if (typeof url !== "string" || !url) return null;
@@ -102,9 +102,9 @@ function parseDotEnv(text) {
102
102
  }
103
103
 
104
104
  // src/internal/config/dotenv.ts
105
- function readDotEnv(filePath2) {
105
+ function readDotEnv(filePath3) {
106
106
  try {
107
- return parseDotEnv(fs2.readFileSync(filePath2, "utf-8"));
107
+ return parseDotEnv(fs2.readFileSync(filePath3, "utf-8"));
108
108
  } catch {
109
109
  return {};
110
110
  }
@@ -1070,14 +1070,14 @@ var os3 = __toESM(require("os"));
1070
1070
  var fs4 = __toESM(require("fs"));
1071
1071
 
1072
1072
  // src/internal/support/instance-id.ts
1073
- var crypto = __toESM(require("crypto"));
1073
+ var crypto2 = __toESM(require("crypto"));
1074
1074
  var os2 = __toESM(require("os"));
1075
1075
  function hashForProject(projectName) {
1076
- return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
1076
+ return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
1077
1077
  }
1078
1078
  function computeInstanceId(projectName, runLabel) {
1079
1079
  const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
1080
- return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16);
1080
+ return crypto2.createHash("sha256").update(seed).digest("hex").slice(0, 16);
1081
1081
  }
1082
1082
 
1083
1083
  // src/internal/streaming/stream-buffer.ts
@@ -1139,7 +1139,7 @@ var StreamBuffer = class {
1139
1139
  };
1140
1140
 
1141
1141
  // src/internal/streaming/crash-recovery.ts
1142
- var crypto2 = __toESM(require("crypto"));
1142
+ var crypto3 = __toESM(require("crypto"));
1143
1143
  var fs5 = __toESM(require("fs"));
1144
1144
  var os4 = __toESM(require("os"));
1145
1145
  var path5 = __toESM(require("path"));
@@ -1155,7 +1155,7 @@ function recoveryServer(serverUrl) {
1155
1155
  return serverUrl.replace(/\/+$/, "");
1156
1156
  }
1157
1157
  function recoveryFilePath(serverUrl, projectName) {
1158
- const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1158
+ const key = crypto3.createHash("sha256").update(`${recoveryServer(serverUrl)}
1159
1159
  ${projectName}`).digest("hex").slice(0, 16);
1160
1160
  return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1161
1161
  }
@@ -1176,7 +1176,7 @@ var CrashRecovery = class {
1176
1176
  /** Write the payload for a later retry, owner-only, replacing the file in one step. */
1177
1177
  save(payload) {
1178
1178
  const file = { v: 1, serverUrl: this.serverUrl, payload };
1179
- const temp = `${this.filePath}.${process.pid}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
1179
+ const temp = `${this.filePath}.${process.pid}.${crypto3.randomBytes(6).toString("hex")}.tmp`;
1180
1180
  try {
1181
1181
  fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1182
1182
  fs5.renameSync(temp, this.filePath);
@@ -1247,7 +1247,7 @@ var CrashRecovery = class {
1247
1247
  var fs7 = __toESM(require("fs"));
1248
1248
  var os5 = __toESM(require("os"));
1249
1249
  var path7 = __toESM(require("path"));
1250
- var crypto3 = __toESM(require("crypto"));
1250
+ var crypto4 = __toESM(require("crypto"));
1251
1251
 
1252
1252
  // src/internal/files/compression.ts
1253
1253
  var fs6 = __toESM(require("fs"));
@@ -1332,6 +1332,19 @@ var ATTACHMENT_NAMES = {
1332
1332
  pauses: "piwi-pauses"
1333
1333
  };
1334
1334
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
1335
+ var ARIA_SNAPSHOT_CONTENT_TYPE = "text/plain; charset=utf-8";
1336
+ function contentTypeOf(name) {
1337
+ return name === ATTACHMENT_NAMES.ariaSnapshot ? ARIA_SNAPSHOT_CONTENT_TYPE : "application/json";
1338
+ }
1339
+ function isPiwiAttachment(attachment, name) {
1340
+ return attachment.name === name && attachment.contentType === contentTypeOf(name);
1341
+ }
1342
+ function findPiwiAttachment(attachments, name) {
1343
+ for (let i = (attachments?.length ?? 0) - 1; i >= 0; i--) {
1344
+ if (isPiwiAttachment(attachments[i], name)) return attachments[i];
1345
+ }
1346
+ return void 0;
1347
+ }
1335
1348
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
1336
1349
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
1337
1350
 
@@ -1450,18 +1463,18 @@ var FileHandler = class {
1450
1463
  }
1451
1464
  const contentType = a.contentType || "application/octet-stream";
1452
1465
  const originalName = `${safeFileName(a.name || "attachment")}${BODY_EXTENSIONS[contentType] ?? ""}`;
1453
- let filePath2 = this.bodyFiles.get(a);
1454
- if (!filePath2) {
1466
+ let filePath3 = this.bodyFiles.get(a);
1467
+ if (!filePath3) {
1455
1468
  try {
1456
- filePath2 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1457
- fs7.writeFileSync(filePath2, body);
1458
- this.bodyFiles.set(a, filePath2);
1469
+ filePath3 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1470
+ fs7.writeFileSync(filePath3, body);
1471
+ this.bodyFiles.set(a, filePath3);
1459
1472
  } catch (error) {
1460
1473
  this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
1461
1474
  return null;
1462
1475
  }
1463
1476
  }
1464
- return { name: a.name || "attachment", path: filePath2, contentType, originalName };
1477
+ return { name: a.name || "attachment", path: filePath3, contentType, originalName };
1465
1478
  }
1466
1479
  bodyAttachmentDir() {
1467
1480
  if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
@@ -1486,11 +1499,11 @@ var FileHandler = class {
1486
1499
  }
1487
1500
  /** Parse Piwi-internal attachment bodies (`piwi-network`, `piwi-web-vitals`, `piwi-console`, `piwi-aria-snapshot`, `piwi-aria-snapshot-json`) into structured fields on the test case */
1488
1501
  parsePerformanceAttachments(testCase, attachments) {
1489
- const find = (name) => attachments.find((a) => a.name === name);
1490
- const net3 = find(ATTACHMENT_NAMES.network);
1491
- if (net3?.body) {
1502
+ const find = (name) => findPiwiAttachment(attachments, name);
1503
+ const net5 = find(ATTACHMENT_NAMES.network);
1504
+ if (net5?.body) {
1492
1505
  try {
1493
- testCase.networkRequests = JSON.parse(net3.body.toString());
1506
+ testCase.networkRequests = JSON.parse(net5.body.toString());
1494
1507
  } catch {
1495
1508
  }
1496
1509
  }
@@ -1549,7 +1562,7 @@ var FileHandler = class {
1549
1562
  if (fs7.existsSync(tp)) lastPath = tp;
1550
1563
  }
1551
1564
  if (!lastPath) return null;
1552
- const hash = crypto3.createHash("sha256");
1565
+ const hash = crypto4.createHash("sha256");
1553
1566
  await new Promise((resolve11, reject) => {
1554
1567
  fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
1555
1568
  });
@@ -3387,6 +3400,258 @@ function maskUrlFragment(hash) {
3387
3400
  const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
3388
3401
  return hash.startsWith("#") ? `#${out}` : out;
3389
3402
  }
3403
+ var HIDDEN_VALUE = "<hidden>";
3404
+ var SECRET_AUTOCOMPLETE = [
3405
+ "current-password",
3406
+ "new-password",
3407
+ "one-time-code",
3408
+ "cc-number",
3409
+ "cc-csc"
3410
+ ];
3411
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
3412
+ "pass",
3413
+ "password",
3414
+ "passwd",
3415
+ "passphrase",
3416
+ "passcode",
3417
+ "pwd",
3418
+ "secret",
3419
+ "secrets",
3420
+ "token",
3421
+ "tokens",
3422
+ "jwt",
3423
+ "bearer",
3424
+ "sig",
3425
+ "signature",
3426
+ "key",
3427
+ "keys",
3428
+ "apikey",
3429
+ "auth",
3430
+ "oauth",
3431
+ "authorization",
3432
+ "session",
3433
+ "sessionid",
3434
+ "jsessionid",
3435
+ "sid",
3436
+ "cookie",
3437
+ "cookies",
3438
+ "credential",
3439
+ "credentials",
3440
+ "otp",
3441
+ "refresh",
3442
+ "csrf",
3443
+ "xsrf"
3444
+ ]);
3445
+ var SECRET_NAMES = /* @__PURE__ */ new Set(["code", "pin"]);
3446
+ function nameWords(name) {
3447
+ 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);
3448
+ }
3449
+ function isSecretName(name) {
3450
+ let secret = namesRead.get(name);
3451
+ if (secret === void 0) {
3452
+ secret = SECRET_NAMES.has(name.trim().toLowerCase()) || nameWords(name).some((word) => SECRET_WORDS.has(word));
3453
+ if (namesRead.size >= 1024) namesRead.clear();
3454
+ namesRead.set(name, secret);
3455
+ }
3456
+ return secret;
3457
+ }
3458
+ var namesRead = /* @__PURE__ */ new Map();
3459
+ var SECRET_SHAPES = [
3460
+ /\beyJ[\w-]{5,}\.[\w-]{5,}\.[\w-]*/g,
3461
+ /\bBearer(?:\s+|%20)[\w.~+/-]{8,}=*/gi,
3462
+ /\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
3463
+ /\bwhsec_[A-Za-z0-9]{20,}/g,
3464
+ /\bsk-(?=[\w-]{0,200}\d)(?=[\w-]{0,200}[A-Z])[\w-]{32,}/g,
3465
+ /\bglpat-[\w-]{20,}/g,
3466
+ /\bnpm_[A-Za-z0-9]{36}\b/g,
3467
+ /\bSK[0-9a-f]{32}\b/g,
3468
+ /\bAKIA[0-9A-Z]{16}\b/g,
3469
+ /\bAIza[\w-]{30,}/g,
3470
+ /\bgh[opsu]_[A-Za-z0-9]{20,}/g,
3471
+ /\bgithub_pat_\w{20,}/g,
3472
+ /\bxox[abprs]-[\w-]{8,}/g
3473
+ ];
3474
+ var WHOLE_SHAPES = SECRET_SHAPES.map(
3475
+ (re) => new RegExp(`^(?:${re.source})$`, re.flags.replace("g", ""))
3476
+ );
3477
+ var BASIC = /\bBasic\s+([A-Za-z0-9+/]{4,}={0,2})(?![\w+/=])/g;
3478
+ var PEM_BEGIN = /-----BEGIN ([A-Z0-9 ]{0,40}PRIVATE KEY)-----/g;
3479
+ var SET_COOKIE = /(\bSet-Cookie:[ \t]*[^=;\s]{1,256}=)([^;\r\n]{1,4096})/gi;
3480
+ var COOKIE_LINE = /((?<![\w-])Cookie:[ \t]*)([^\r\n]{1,16384})/gi;
3481
+ var JSON_FIELD = /("((?:[^"\\\r\n]|\\.){1,256})"\s*:\s*")((?:[^"\\\r\n]|\\.){1,4096})"/g;
3482
+ var TOKEN_RUN = /[A-Za-z0-9+/_-]{40,}={0,2}/g;
3483
+ var MIN_RANDOM = 40;
3484
+ var BASE64_DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3485
+ function base64OfText(run) {
3486
+ let bits = 0;
3487
+ let acc = 0;
3488
+ let bytes = 0;
3489
+ let printable = 0;
3490
+ for (const c of run.replace(/=+$/, "")) {
3491
+ const digit = BASE64_DIGITS.indexOf(c === "-" ? "+" : c === "_" ? "/" : c);
3492
+ if (digit < 0) return false;
3493
+ acc = (acc << 6 | digit) & 65535;
3494
+ bits += 6;
3495
+ if (bits < 8) continue;
3496
+ bits -= 8;
3497
+ const byte = acc >> bits & 255;
3498
+ bytes++;
3499
+ if (byte >= 32 && byte < 127 || byte === 9 || byte === 10 || byte === 13) printable++;
3500
+ }
3501
+ return bytes > 0 && printable * 10 >= bytes * 9;
3502
+ }
3503
+ function bitsPerChar(text) {
3504
+ const counts = /* @__PURE__ */ new Map();
3505
+ for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
3506
+ let bits = 0;
3507
+ for (const n of counts.values()) bits -= n / text.length * Math.log2(n / text.length);
3508
+ return bits;
3509
+ }
3510
+ function looksRandom(text) {
3511
+ if (text.length < MIN_RANDOM || !/[A-Z]/.test(text) || !/[a-z]/.test(text) || !/\d/.test(text)) return false;
3512
+ let letters = 0;
3513
+ for (const c of text) if (c >= "a" && c <= "z" || c >= "A" && c <= "Z") letters++;
3514
+ let wordy = 0;
3515
+ for (const run of text.matchAll(/[a-z]{6,}|[A-Z]{6,}/g)) wordy += run[0].length;
3516
+ return wordy * 5 <= letters && bitsPerChar(text) >= 4 && !base64OfText(text);
3517
+ }
3518
+ function isBasicCredential(encoded) {
3519
+ const text = encoded.replace(/=+$/, "");
3520
+ if (text.length % 4 === 1) return false;
3521
+ let decoded = "";
3522
+ let bits = 0;
3523
+ let acc = 0;
3524
+ for (const c of text) {
3525
+ const digit = BASE64_DIGITS.indexOf(c);
3526
+ if (digit < 0) return false;
3527
+ acc = (acc << 6 | digit) & 65535;
3528
+ bits += 6;
3529
+ if (bits < 8) continue;
3530
+ bits -= 8;
3531
+ decoded += String.fromCharCode(acc >> bits & 255);
3532
+ }
3533
+ return /^[\x20-\x7e]+:[\x20-\x7e]*$/.test(decoded);
3534
+ }
3535
+ function isSecretValue(value) {
3536
+ const text = value.trim();
3537
+ if (text.length < 10 || text.length > 4096) return false;
3538
+ if (WHOLE_SHAPES.some((re) => re.test(text))) return true;
3539
+ const basic = /^Basic\s+([A-Za-z0-9+/]{4,}={0,2})$/.exec(text);
3540
+ if (basic && isBasicCredential(basic[1])) return true;
3541
+ if (/^-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/.test(text)) return true;
3542
+ return /^[A-Za-z0-9+/_-]{40,}={0,2}$/.test(text) && looksRandom(text);
3543
+ }
3544
+ var URL_IN_TEXT = /\b[a-z][a-z0-9+.-]{0,31}:\/\/[^\s"'<>`]+/gi;
3545
+ var URL_PARTS = /^(?:([a-z][a-z0-9+.-]*:)?\/\/([^/?#]*))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?$/i;
3546
+ function decodeSafe(text) {
3547
+ try {
3548
+ return decodeURIComponent(text);
3549
+ } catch {
3550
+ return text;
3551
+ }
3552
+ }
3553
+ function hider(count) {
3554
+ return () => {
3555
+ count.n++;
3556
+ return HIDDEN_VALUE;
3557
+ };
3558
+ }
3559
+ function hideParams(params, count) {
3560
+ const hide = hider(count);
3561
+ return params.split(/([&;])/).map((part, i) => {
3562
+ const eq = part.indexOf("=");
3563
+ if (i % 2 === 1 || eq < 0) return part;
3564
+ const name = part.slice(0, eq);
3565
+ const value = part.slice(eq + 1);
3566
+ const decoded = decodeSafe(value);
3567
+ if (!value || decoded === HIDDEN_VALUE) return part;
3568
+ return isSecretName(decodeSafe(name)) || isSecretValue(decoded) ? `${name}=${hide()}` : part;
3569
+ }).join("");
3570
+ }
3571
+ function hidePath(path34, count) {
3572
+ const hide = hider(count);
3573
+ return path34.split("/").map((segment) => {
3574
+ const [own = "", ...params] = segment.split(";");
3575
+ const decoded = decodeSafe(own);
3576
+ const kept = decoded !== HIDDEN_VALUE && isSecretValue(decoded) ? hide() : own;
3577
+ return params.length ? `${kept};${hideParams(params.join(";"), count)}` : kept;
3578
+ }).join("/");
3579
+ }
3580
+ function hideSecretsInUrl(url, count = { n: 0 }) {
3581
+ const parts = URL_PARTS.exec(url);
3582
+ if (!parts) return url;
3583
+ const [, scheme = "", authority, path34 = "", query = "", fragment = ""] = parts;
3584
+ let out = "";
3585
+ if (authority !== void 0) {
3586
+ const at = authority.lastIndexOf("@");
3587
+ const colon = at >= 0 ? authority.slice(0, at).indexOf(":") : -1;
3588
+ out += colon >= 0 && colon < at - 1 ? `${scheme}//${authority.slice(0, colon + 1)}${hider(count)()}${authority.slice(at)}` : `${scheme}//${authority}`;
3589
+ }
3590
+ out += hidePath(path34, count);
3591
+ if (query) out += `?${hideParams(query.slice(1), count)}`;
3592
+ if (fragment) {
3593
+ const body = fragment.slice(1);
3594
+ const ask = body.indexOf("?");
3595
+ out += `#${ask >= 0 ? `${hidePath(body.slice(0, ask), count)}?${hideParams(body.slice(ask + 1), count)}` : body.includes("=") ? hideParams(body, count) : hidePath(body, count)}`;
3596
+ }
3597
+ return out;
3598
+ }
3599
+ var QUERY_PAIR = /([?&;#])([^=&#?;\s"'<>]{1,256})=([^&;#\s"'<>]*)/g;
3600
+ var URL_PASSWORD = /(\/\/[^/:@\s"'<>]{1,256}:)([^@/\s"'<>]{1,256})(@)/g;
3601
+ function hidePem(text, hide) {
3602
+ let out = "";
3603
+ let last = 0;
3604
+ let nextEnd = -2;
3605
+ PEM_BEGIN.lastIndex = 0;
3606
+ for (let begin = PEM_BEGIN.exec(text); begin; begin = PEM_BEGIN.exec(text)) {
3607
+ if (begin.index < last) continue;
3608
+ if (nextEnd !== -1 && nextEnd < begin.index) nextEnd = text.indexOf("-----END ", begin.index);
3609
+ const footer = nextEnd >= 0 ? /^-----END [A-Z0-9 ]{0,40}-----/.exec(text.slice(nextEnd, nextEnd + 60)) : null;
3610
+ 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);
3611
+ out += `${text.slice(last, begin.index)}${hide()}`;
3612
+ last = until;
3613
+ PEM_BEGIN.lastIndex = until;
3614
+ }
3615
+ return out + text.slice(last);
3616
+ }
3617
+ function hideInText(text, count) {
3618
+ if (text.length < 4 || !/[A-Za-z0-9]/.test(text)) return text;
3619
+ const hide = hider(count);
3620
+ let out = hidePem(text, hide);
3621
+ out = out.replace(QUERY_PAIR, (all, sep13, name, value) => {
3622
+ const decoded = decodeSafe(value);
3623
+ if (!value || decoded === HIDDEN_VALUE) return all;
3624
+ return isSecretName(name) || isSecretValue(decoded) ? `${sep13}${name}=${hide()}` : all;
3625
+ });
3626
+ out = out.replace(URL_PASSWORD, (_all, before, _password, at) => `${before}${hide()}${at}`);
3627
+ out = out.replace(SET_COOKIE, (_all, before) => `${before}${hide()}`);
3628
+ out = out.replace(COOKIE_LINE, (_all, before, pairs) => {
3629
+ const values = pairs.replace(
3630
+ /([^=;\s]{1,256}=)([^;]*)/g,
3631
+ (pair, name, value) => value.trim() && value.trim() !== HIDDEN_VALUE ? `${name}${hide()}` : pair
3632
+ );
3633
+ return `${before}${values}`;
3634
+ });
3635
+ out = out.replace(
3636
+ JSON_FIELD,
3637
+ (all, before, name, value) => isSecretName(name) && value !== HIDDEN_VALUE ? `${before}${hide()}"` : all
3638
+ );
3639
+ out = out.replace(BASIC, (all, encoded) => isBasicCredential(encoded) ? hide() : all);
3640
+ for (const shape of SECRET_SHAPES) out = out.replace(shape, () => hide());
3641
+ return out.replace(
3642
+ TOKEN_RUN,
3643
+ (run) => run.includes("/") && !/[+=]/.test(run) ? run.split("/").map((part) => looksRandom(part) ? hide() : part).join("/") : looksRandom(run) ? hide() : run
3644
+ );
3645
+ }
3646
+ function hideSecretsInText(text, count = { n: 0 }) {
3647
+ let out = "";
3648
+ let last = 0;
3649
+ for (const url of text.matchAll(URL_IN_TEXT)) {
3650
+ out += hideInText(text.slice(last, url.index), count) + hideSecretsInUrl(url[0], count);
3651
+ last = url.index + url[0].length;
3652
+ }
3653
+ return out + hideInText(text.slice(last), count);
3654
+ }
3390
3655
 
3391
3656
  // ../core/src/locator-break.ts
3392
3657
  var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -4060,8 +4325,8 @@ var fs15 = __toESM(require("fs"));
4060
4325
  var path12 = __toESM(require("path"));
4061
4326
  var os7 = __toESM(require("os"));
4062
4327
  var fs12 = __toESM(require("fs"));
4063
- function ariaSampleIdentity(filePath2, title) {
4064
- return `${filePath2}\0${title}`;
4328
+ function ariaSampleIdentity(filePath3, title) {
4329
+ return `${filePath3}\0${title}`;
4065
4330
  }
4066
4331
  function getAriaSampleFilePath(projectName) {
4067
4332
  return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
@@ -4189,9 +4454,9 @@ function reachedRemotely(use, url, env) {
4189
4454
  if (use?.connectOptions === void 0) {
4190
4455
  return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
4191
4456
  }
4192
- const connect2 = use.connectOptions;
4193
- if (!connect2 || typeof connect2 !== "object" || typeof connect2.wsEndpoint !== "string") return false;
4194
- return !exposeNetworkCovers(typeof connect2.exposeNetwork === "string" ? connect2.exposeNetwork : void 0, url);
4457
+ const connect3 = use.connectOptions;
4458
+ if (!connect3 || typeof connect3 !== "object" || typeof connect3.wsEndpoint !== "string") return false;
4459
+ return !exposeNetworkCovers(typeof connect3.exposeNetwork === "string" ? connect3.exposeNetwork : void 0, url);
4195
4460
  }
4196
4461
  function baseUrlTargets(config, argv = process.argv, env = process.env) {
4197
4462
  const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
@@ -4608,7 +4873,7 @@ function reduceCoverage(capture, granularity) {
4608
4873
  }
4609
4874
 
4610
4875
  // src/internal/collect/source-checksums.ts
4611
- var crypto4 = __toESM(require("crypto"));
4876
+ var crypto5 = __toESM(require("crypto"));
4612
4877
  var fs14 = __toESM(require("fs"));
4613
4878
  var path14 = __toESM(require("path"));
4614
4879
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
@@ -4626,7 +4891,7 @@ function contentChecksums(content) {
4626
4891
  const out = /* @__PURE__ */ new Set();
4627
4892
  for (const form of lineEndingForms(content)) {
4628
4893
  for (const algorithm of ["sha1", "sha256"]) {
4629
- out.add(`${algorithm}:${crypto4.createHash(algorithm).update(form).digest("hex")}`);
4894
+ out.add(`${algorithm}:${crypto5.createHash(algorithm).update(form).digest("hex")}`);
4630
4895
  }
4631
4896
  }
4632
4897
  return out;
@@ -4738,10 +5003,10 @@ function fileChecksums(files, into = /* @__PURE__ */ new Map()) {
4738
5003
  if (!Array.isArray(files)) return into;
4739
5004
  for (const item of files) {
4740
5005
  if (!isRecord2(item)) continue;
4741
- const filePath2 = normalizeCoveragePath(item.path);
5006
+ const filePath3 = normalizeCoveragePath(item.path);
4742
5007
  const checksums = checksumsOf(item.checksums);
4743
- if (filePath2 === null || checksums.length === 0) continue;
4744
- into.set(filePath2, [.../* @__PURE__ */ new Set([...into.get(filePath2) ?? [], ...checksums])]);
5008
+ if (filePath3 === null || checksums.length === 0) continue;
5009
+ into.set(filePath3, [.../* @__PURE__ */ new Set([...into.get(filePath3) ?? [], ...checksums])]);
4745
5010
  }
4746
5011
  return into;
4747
5012
  }
@@ -4750,9 +5015,9 @@ function runFilesOf(answer) {
4750
5015
  const files = [];
4751
5016
  for (const item of answer.files) {
4752
5017
  if (!isRecord2(item) || item.runtime !== "server" && item.runtime !== "browser") continue;
4753
- const filePath2 = normalizeCoveragePath(item.path);
4754
- if (filePath2 === null) continue;
4755
- const file = { path: filePath2, runtime: item.runtime };
5018
+ const filePath3 = normalizeCoveragePath(item.path);
5019
+ if (filePath3 === null) continue;
5020
+ const file = { path: filePath3, runtime: item.runtime };
4756
5021
  const functions = normalizeFunctionRanges(item.functions);
4757
5022
  const lines = normalizeLineRanges(item.lines);
4758
5023
  const executable = normalizeLineRanges(item.executable);
@@ -5375,9 +5640,9 @@ function loadPlan() {
5375
5640
  function probeItemForTest(test) {
5376
5641
  const plan = loadPlan();
5377
5642
  if (!plan) return null;
5378
- const filePath2 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
5643
+ const filePath3 = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
5379
5644
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
5380
- return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
5645
+ return matchProbeItem(plan, { title: test.title, filePath: filePath3, suitePath });
5381
5646
  }
5382
5647
  function classifyProbeHandled(signals) {
5383
5648
  const blank = signals.blankPage === true;
@@ -6372,9 +6637,9 @@ function duplicateTestKey(test) {
6372
6637
  }
6373
6638
  function readDuplicateTest(raw) {
6374
6639
  if (!raw || typeof raw !== "object") return null;
6375
- const { filePath: filePath2, suitePath, title, line } = raw;
6376
- if (typeof filePath2 !== "string" || typeof title !== "string") return null;
6377
- const test = { filePath: filePath2, title };
6640
+ const { filePath: filePath3, suitePath, title, line } = raw;
6641
+ if (typeof filePath3 !== "string" || typeof title !== "string") return null;
6642
+ const test = { filePath: filePath3, title };
6378
6643
  if (Array.isArray(suitePath) && suitePath.every((s) => typeof s === "string") && suitePath.length > 0)
6379
6644
  test.suitePath = suitePath;
6380
6645
  if (typeof line === "number" && Number.isFinite(line)) test.line = line;
@@ -6931,7 +7196,9 @@ var RunSubmitter = class {
6931
7196
  };
6932
7197
 
6933
7198
  // src/internal/capture/resource-ledger.ts
7199
+ var childProcess = __toESM(require("child_process"));
6934
7200
  var fs21 = __toESM(require("fs"));
7201
+ var net2 = __toESM(require("net"));
6935
7202
  var path20 = __toESM(require("path"));
6936
7203
  var import_node_module2 = require("module");
6937
7204
 
@@ -7309,6 +7576,8 @@ var WorkerMetrics = class {
7309
7576
  constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
7310
7577
  this.proc = proc;
7311
7578
  this.pid = pid;
7579
+ /** The lowest heap read right after a full garbage collection in the current window. */
7580
+ this.heapAfterGc = null;
7312
7581
  this.loop = null;
7313
7582
  this.usage = null;
7314
7583
  /** Each process's CPU and wait at the start of the current window; null before the first. */
@@ -7323,6 +7592,28 @@ var WorkerMetrics = class {
7323
7592
  histogram = null;
7324
7593
  }
7325
7594
  this.histogram = histogram;
7595
+ this.gcObserver = this.observeFullCollections();
7596
+ }
7597
+ /**
7598
+ * Read the heap after each full (major) garbage collection V8 runs on its
7599
+ * own: nothing is forced, so a test that triggers none reads null.
7600
+ */
7601
+ observeFullCollections() {
7602
+ try {
7603
+ const observer = new import_node_perf_hooks.PerformanceObserver((list) => {
7604
+ for (const entry of list.getEntries()) {
7605
+ const detail = entry.detail;
7606
+ const kind = detail?.kind ?? entry.kind;
7607
+ if (kind !== import_node_perf_hooks.constants.NODE_PERFORMANCE_GC_MAJOR) continue;
7608
+ const heap = process.memoryUsage().heapUsed;
7609
+ this.heapAfterGc = Math.min(this.heapAfterGc ?? Infinity, heap);
7610
+ }
7611
+ });
7612
+ observer.observe({ entryTypes: ["gc"] });
7613
+ return observer;
7614
+ } catch {
7615
+ return null;
7616
+ }
7326
7617
  }
7327
7618
  /** The worker's processes under it, keyed by pid and start time. */
7328
7619
  tree() {
@@ -7356,6 +7647,7 @@ var WorkerMetrics = class {
7356
7647
  start(processes = true) {
7357
7648
  try {
7358
7649
  this.histogram?.reset();
7650
+ this.heapAfterGc = null;
7359
7651
  this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
7360
7652
  this.usage = process.resourceUsage();
7361
7653
  if (this.linux && processes && !this.subtree) this.snapshot();
@@ -7378,6 +7670,7 @@ var WorkerMetrics = class {
7378
7670
  loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
7379
7671
  loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
7380
7672
  heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
7673
+ heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
7381
7674
  fds: this.linux ? this.proc.fdCount(this.pid) : null
7382
7675
  }
7383
7676
  };
@@ -7412,6 +7705,7 @@ var WorkerMetrics = class {
7412
7705
  }
7413
7706
  dispose() {
7414
7707
  this.histogram?.disable();
7708
+ this.gcObserver?.disconnect();
7415
7709
  }
7416
7710
  };
7417
7711
  var PageReader = class {
@@ -7433,12 +7727,36 @@ var PageReader = class {
7433
7727
  try {
7434
7728
  const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
7435
7729
  const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
7436
- return {
7730
+ const main = {
7437
7731
  cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
7438
7732
  heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
7439
7733
  nodes: values.get("Nodes") ?? 0,
7440
7734
  listeners: values.get("JSEventListeners") ?? 0
7441
7735
  };
7736
+ const elements = await this.countElements(page, opened);
7737
+ if (elements !== null) main.elements = elements;
7738
+ return main;
7739
+ } catch {
7740
+ return null;
7741
+ }
7742
+ }
7743
+ /**
7744
+ * The elements attached to the page's main document, through the page's own
7745
+ * DevTools session: no Playwright call, so no step and no trace action. The
7746
+ * count runs in the page's main world and changes nothing in it.
7747
+ */
7748
+ async countElements(page, session) {
7749
+ try {
7750
+ const result = await internalCall(
7751
+ page,
7752
+ () => session.send("Runtime.evaluate", {
7753
+ expression: "document.getElementsByTagName('*').length",
7754
+ returnByValue: true,
7755
+ silent: true
7756
+ })
7757
+ );
7758
+ const value = result.result?.value;
7759
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
7442
7760
  } catch {
7443
7761
  return null;
7444
7762
  }
@@ -7447,6 +7765,9 @@ var PageReader = class {
7447
7765
 
7448
7766
  // src/internal/capture/resource-ledger.ts
7449
7767
  var TRACKED_HANDLE_TYPES = ["TCPServerWrap", "FSEventWrap", "StatWatcher"];
7768
+ var COUNTED_WITH_RUNNING = ["FSEventWrap", "StatWatcher"];
7769
+ var PLAYWRIGHT_ROLES = /* @__PURE__ */ new Set(["worker", "runner", "browser", "renderer", "gpu", "utility", "ffmpeg"]);
7770
+ var HANDLE_STACK_LIMIT = 40;
7450
7771
  var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
7451
7772
  var CLOSE_TIMEOUT_MS = 5e3;
7452
7773
  var PAGE_READ_CAP = 50;
@@ -7522,20 +7843,41 @@ function playwrightTestInfoSource() {
7522
7843
  function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
7523
7844
  return value === "fail" || value === "close" ? value : "report";
7524
7845
  }
7525
- function countHandles() {
7846
+ function countHandles(types = TRACKED_HANDLE_TYPES) {
7526
7847
  const counts = {};
7527
7848
  const read = process.getActiveResourcesInfo;
7528
7849
  if (typeof read !== "function") return counts;
7529
7850
  for (const type of read.call(process)) {
7530
- if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7851
+ if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7531
7852
  }
7532
7853
  return counts;
7533
7854
  }
7534
- async function settledHandles(start) {
7535
- const end = countHandles();
7855
+ async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
7856
+ const end = countHandles(types);
7536
7857
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
7537
7858
  await new Promise((resolve11) => setTimeout(resolve11, 0));
7538
- return countHandles();
7859
+ return countHandles(types);
7860
+ }
7861
+ function deepUserSite() {
7862
+ const limit = Error.stackTraceLimit;
7863
+ Error.stackTraceLimit = HANDLE_STACK_LIMIT;
7864
+ try {
7865
+ return userSite(new Error().stack);
7866
+ } finally {
7867
+ Error.stackTraceLimit = limit;
7868
+ }
7869
+ }
7870
+ var SHELLS = /^(?:sh|bash|dash|zsh|cmd|cmd\.exe|powershell|powershell\.exe|pwsh)$/i;
7871
+ function commandOf(spawnfile, spawnargs) {
7872
+ const name = (file) => path20.basename(file.replace(/\\/g, "/").replace(/^["']|["']$/g, ""));
7873
+ const program = typeof spawnfile === "string" ? name(spawnfile) : "";
7874
+ const args = Array.isArray(spawnargs) ? spawnargs.map(String) : [];
7875
+ const script = args.findIndex((arg) => arg === "-c" || /^\/c$/i.test(arg));
7876
+ if (SHELLS.test(program) && script >= 0 && args[script + 1]) {
7877
+ const first = args[script + 1].trim().split(/\s+/)[0];
7878
+ if (first) return name(first);
7879
+ }
7880
+ return program || "a child process";
7539
7881
  }
7540
7882
  function settleWithin(promise, ms) {
7541
7883
  let timer;
@@ -7547,7 +7889,7 @@ function settleWithin(promise, ms) {
7547
7889
  ]).finally(() => clearTimeout(timer));
7548
7890
  }
7549
7891
  var ResourceLedger = class {
7550
- constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}) {
7892
+ constructor(playwright, workerIndex, leakCheck = readLeakCheck(), resultsFile = process.env[PIWI_RESOURCES_RESULTS_ENV] || null, testInfoSource = playwrightTestInfoSource(), now = Date.now, readers = {}, nodeHandles = false) {
7551
7893
  this.playwright = playwright;
7552
7894
  this.workerIndex = workerIndex;
7553
7895
  this.leakCheck = leakCheck;
@@ -7555,6 +7897,7 @@ var ResourceLedger = class {
7555
7897
  this.testInfoSource = testInfoSource;
7556
7898
  this.now = now;
7557
7899
  this.readers = readers;
7900
+ this.nodeHandles = nodeHandles;
7558
7901
  this.nextId = 1;
7559
7902
  this.entries = /* @__PURE__ */ new Map();
7560
7903
  this.byObject = /* @__PURE__ */ new WeakMap();
@@ -7571,6 +7914,11 @@ var ResourceLedger = class {
7571
7914
  this.listener = null;
7572
7915
  /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
7573
7916
  this.sawBrowser = false;
7917
+ /** The servers and child processes still running, while the ledger tracks them. */
7918
+ this.handles = /* @__PURE__ */ new Set();
7919
+ this.byHandle = /* @__PURE__ */ new WeakMap();
7920
+ this.trackingHandles = false;
7921
+ this.restores = [];
7574
7922
  }
7575
7923
  /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
7576
7924
  install() {
@@ -7603,13 +7951,107 @@ var ResourceLedger = class {
7603
7951
  for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
7604
7952
  if (type) this.wrapBrowserType(type);
7605
7953
  }
7954
+ if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
7606
7955
  return true;
7607
7956
  }
7608
- /** Stop listening; the objects already tracked stay in the ledger. */
7957
+ /** Stop listening, and put Node's `listen` and `spawn` back; the objects already tracked stay in the ledger. */
7609
7958
  uninstall() {
7610
7959
  const remove = this.playwright?._instrumentation?.removeListener;
7611
7960
  if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
7612
7961
  this.listener = null;
7962
+ for (const restore of this.restores.splice(0)) restore();
7963
+ this.trackingHandles = false;
7964
+ }
7965
+ /**
7966
+ * Wrap `net.Server.prototype.listen` (every HTTP, HTTPS and TCP server) and
7967
+ * `ChildProcess.prototype.spawn` (`spawn`, `exec`, `execFile` and `fork`), so
7968
+ * each server and child process the worker starts carries the line and the
7969
+ * runnable that started it, as a context does. A worker-scoped fixture's
7970
+ * server is then the worker's, never the first test's. Each wrapper calls the
7971
+ * original on its receiver and only records; it never throws.
7972
+ */
7973
+ wrapNodeHandles() {
7974
+ const onServer = (server2, site) => this.guard(() => this.onServer(server2, site));
7975
+ const onChild = (child2, site) => this.guard(() => this.onChild(child2, site));
7976
+ const server = net2.Server.prototype;
7977
+ const listen = server.listen;
7978
+ if (typeof listen === "function") {
7979
+ const wrapped = function(...args) {
7980
+ onServer(this, deepUserSite());
7981
+ return listen.apply(this, args);
7982
+ };
7983
+ server.listen = wrapped;
7984
+ this.restores.push(() => {
7985
+ if (server.listen === wrapped) server.listen = listen;
7986
+ });
7987
+ }
7988
+ const child = childProcess.ChildProcess?.prototype;
7989
+ const spawn = child?.spawn;
7990
+ if (child && typeof spawn === "function") {
7991
+ const wrapped = function(...args) {
7992
+ const site = deepUserSite();
7993
+ const result = spawn.apply(this, args);
7994
+ onChild(this, site);
7995
+ return result;
7996
+ };
7997
+ child.spawn = wrapped;
7998
+ this.restores.push(() => {
7999
+ if (child.spawn === wrapped) child.spawn = spawn;
8000
+ });
8001
+ }
8002
+ this.trackingHandles = true;
8003
+ }
8004
+ trackHandle(object, entry) {
8005
+ const info = this.testInfoSource();
8006
+ const { phase, fixture } = info ? runnableOf(info) : fallbackRunnable(this.window);
8007
+ const tracked = { ...entry, phase, fixture, test: testRef2(info) ?? this.window };
8008
+ this.handles.add(tracked);
8009
+ this.byHandle.set(object, tracked);
8010
+ return tracked;
8011
+ }
8012
+ onServer(server, site) {
8013
+ const known = this.byHandle.get(server);
8014
+ if (known && this.handles.has(known)) return;
8015
+ let bound = false;
8016
+ let closed = false;
8017
+ const entry = this.trackHandle(server, {
8018
+ type: "TCPServerWrap",
8019
+ site,
8020
+ // `close()` unbinds at once, while its 'close' event waits for open connections.
8021
+ running: () => server.listening,
8022
+ pending: () => !bound && !closed && !server.listening
8023
+ });
8024
+ server.once("listening", () => {
8025
+ bound = true;
8026
+ });
8027
+ server.once("close", () => {
8028
+ closed = true;
8029
+ this.handles.delete(entry);
8030
+ });
8031
+ }
8032
+ onChild(child, site) {
8033
+ const command = commandOf(child.spawnfile, child.spawnargs);
8034
+ const role = roleOf(command, (child.spawnargs ?? []).join(" "));
8035
+ if (role && PLAYWRIGHT_ROLES.has(role) || child.pid === void 0) return;
8036
+ const entry = this.trackHandle(child, {
8037
+ type: "ChildProcess",
8038
+ site,
8039
+ command,
8040
+ running: () => child.exitCode === null && child.signalCode === null
8041
+ });
8042
+ child.once("exit", () => this.handles.delete(entry));
8043
+ }
8044
+ /**
8045
+ * The servers and child processes a test started (its body, its each-hooks
8046
+ * or a test-scoped fixture) that still run. A server whose `listen` has not
8047
+ * bound yet is read again one tick later.
8048
+ */
8049
+ async runningOf(testId) {
8050
+ const own = () => [...this.handles].filter(
8051
+ (entry) => entry.test?.id === testId && !entry.fixture?.worker && (entry.phase === "test" || entry.phase === "beforeEach" || entry.phase === "afterEach")
8052
+ );
8053
+ if (own().some((entry) => entry.pending?.())) await new Promise((resolve11) => setTimeout(resolve11, 0));
8054
+ return own().filter((entry) => entry.running()).map((entry) => ({ type: entry.type, site: entry.site, ...entry.command ? { command: entry.command } : {} }));
7613
8055
  }
7614
8056
  /** Mark a page used: a locator was built on it. */
7615
8057
  notePageUse(page) {
@@ -7619,7 +8061,7 @@ var ResourceLedger = class {
7619
8061
  /** Open the census window of a test. */
7620
8062
  testStarted(info) {
7621
8063
  this.window = testRef2(info);
7622
- this.handlesAtStart = countHandles();
8064
+ this.handlesAtStart = countHandles(this.countedHandleTypes());
7623
8065
  this.openAtStart = { contexts: 0, pages: 0 };
7624
8066
  for (const entry of this.openEntries()) {
7625
8067
  if (entry.birth.kind === "page") this.openAtStart.pages++;
@@ -7639,9 +8081,14 @@ var ResourceLedger = class {
7639
8081
  if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
7640
8082
  const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
7641
8083
  const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
7642
- const handles = { start: this.handlesAtStart, end: await settledHandles(this.handlesAtStart) };
8084
+ const handles = {
8085
+ start: this.handlesAtStart,
8086
+ end: await settledHandles(this.handlesAtStart, this.countedHandleTypes())
8087
+ };
8088
+ const running = this.trackingHandles && test ? await this.runningOf(test.id) : null;
7643
8089
  const census = this.census(ref, handles, await this.readPages());
7644
8090
  if (metrics) census.metrics = metrics;
8091
+ if (running) census.running = running;
7645
8092
  if (test) {
7646
8093
  census.openAtStart = this.openAtStart;
7647
8094
  if (!failed) census.leftOpen = leaks.length;
@@ -7680,6 +8127,10 @@ var ResourceLedger = class {
7680
8127
  this.uninstall();
7681
8128
  return census;
7682
8129
  }
8130
+ /** The handle types counted at each boundary: servers leave the count once they are tracked one by one. */
8131
+ countedHandleTypes() {
8132
+ return this.trackingHandles ? COUNTED_WITH_RUNNING : TRACKED_HANDLE_TYPES;
8133
+ }
7683
8134
  guard(run) {
7684
8135
  try {
7685
8136
  run();
@@ -7902,6 +8353,7 @@ var ResourceLedger = class {
7902
8353
  open
7903
8354
  };
7904
8355
  if (handles) census.handles = handles;
8356
+ for (const close of this.closed) this.entries.delete(close.id);
7905
8357
  this.born = [];
7906
8358
  this.closed = [];
7907
8359
  return census;
@@ -7940,7 +8392,8 @@ function startResourceLedger(playwright, workerIndex) {
7940
8392
  process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
7941
8393
  playwrightTestInfoSource(),
7942
8394
  Date.now,
7943
- { metrics, pages: new PageReader() }
8395
+ { metrics, pages: new PageReader() },
8396
+ true
7944
8397
  );
7945
8398
  if (!ledger.install()) {
7946
8399
  metrics.dispose();
@@ -8099,7 +8552,15 @@ function buildResourceReport(input) {
8099
8552
  if (!entry) continue;
8100
8553
  entry.lastOpenAt = census.at;
8101
8554
  if (open.used !== void 0) entry.used = open.used;
8102
- if (census.test) entry.series.push({ pages: open.pages, listeners: open.listeners, routes: open.routes });
8555
+ if (census.test) {
8556
+ entry.series.push({
8557
+ pages: open.pages,
8558
+ listeners: open.listeners,
8559
+ routes: open.routes,
8560
+ elements: open.main?.elements,
8561
+ test: census.test
8562
+ });
8563
+ }
8103
8564
  if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
8104
8565
  }
8105
8566
  }
@@ -8114,6 +8575,7 @@ function buildResourceReport(input) {
8114
8575
  }
8115
8576
  findings.push(...groupLeaks(leaked, all));
8116
8577
  findings.push(...pilingFindings(all));
8578
+ findings.push(...workerPilingFindings(byWorker));
8117
8579
  findings.push(...handleFindings(byWorker));
8118
8580
  findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
8119
8581
  findings.push(...probableFindings(input.tallies ?? []));
@@ -8251,16 +8713,39 @@ function groupIdle(idle, fixturesByTest) {
8251
8713
  }
8252
8714
  return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
8253
8715
  }
8716
+ var PILING_RULES = {
8717
+ pages: { min: 2, share: 0, minPoints: 3 },
8718
+ listeners: { min: 2, share: 0, minPoints: 3 },
8719
+ routes: { min: 2, share: 0, minPoints: 3 },
8720
+ elements: { min: 200, share: 0.25, minPoints: 4 },
8721
+ heapMb: { min: 32, share: 0.2, minPoints: 3 },
8722
+ descriptors: { min: 8, share: 0.2, minPoints: 4 }
8723
+ };
8724
+ function pileUp(values, what) {
8725
+ const rule = PILING_RULES[what];
8726
+ if (values.length < rule.minPoints) return null;
8727
+ const enough = (from, to) => to - from >= Math.max(rule.min, from * rule.share);
8728
+ const first = values[0];
8729
+ const last = values[values.length - 1];
8730
+ if (values.every((value, i) => i === 0 || value >= values[i - 1]) && enough(first, last)) {
8731
+ return { from: first, to: last };
8732
+ }
8733
+ if (values.length < 4) return null;
8734
+ const floor = [...values];
8735
+ for (let i = floor.length - 2; i >= 0; i--) floor[i] = Math.min(floor[i], floor[i + 1]);
8736
+ const start = floor[0];
8737
+ const middle = floor[Math.floor((floor.length - 1) / 2)];
8738
+ const late = floor[Math.floor((floor.length - 1) * 3 / 4)];
8739
+ if (!enough(start, late) || middle - start < (late - start) / 4) return null;
8740
+ return { from: start, to: last };
8741
+ }
8254
8742
  function pilingFindings(all) {
8255
8743
  const out = [];
8256
8744
  for (const entry of all) {
8257
- for (const what of ["pages", "listeners", "routes"]) {
8745
+ for (const what of ["pages", "listeners", "routes", "elements"]) {
8258
8746
  const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
8259
- if (values.length < 3) continue;
8260
- const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
8261
- const from = values[0];
8262
- const to = values[values.length - 1];
8263
- if (!rising || to - from < 2) continue;
8747
+ const growth = pileUp(values, what);
8748
+ if (!growth) continue;
8264
8749
  out.push({
8265
8750
  verdict: "piling",
8266
8751
  kind: entry.birth.kind,
@@ -8268,34 +8753,91 @@ function pilingFindings(all) {
8268
8753
  site: siteOf(entry.birth),
8269
8754
  tests: values.length,
8270
8755
  count: 1,
8271
- growth: { what, from, to, tests: values.length }
8756
+ growth: { what, from: Math.round(growth.from), to: Math.round(growth.to), tests: values.length }
8272
8757
  });
8273
8758
  }
8274
8759
  }
8275
8760
  return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
8276
8761
  }
8762
+ function leftSomethingOpen(census) {
8763
+ if ((census.leftOpen ?? 0) > 0 || (census.running?.length ?? 0) > 0) return true;
8764
+ const handles = census.handles;
8765
+ return !!handles && Object.entries(handles.end).some(([type, n]) => n > (handles.start[type] ?? 0));
8766
+ }
8767
+ function workerPilingFindings(byWorker) {
8768
+ const best = /* @__PURE__ */ new Map();
8769
+ for (const censuses of byWorker.values()) {
8770
+ const measured = censuses.filter((census) => census.test && !census.test.failed && census.metrics).sort((a, b) => a.at - b.at);
8771
+ const read = [
8772
+ ["heapMb", (census) => census.metrics.worker.heapAfterGcMb],
8773
+ ["descriptors", (census) => census.metrics.worker.fds]
8774
+ ];
8775
+ for (const [what, value] of read) {
8776
+ const points = measured.map((census) => ({ census, value: value(census) })).filter((point) => typeof point.value === "number");
8777
+ const unexplained = [];
8778
+ let top = null;
8779
+ points.forEach((point, i) => {
8780
+ if (i === 0) return void unexplained.push(point.value);
8781
+ const gained = point.value - points[i - 1].value;
8782
+ const counted = leftSomethingOpen(point.census) && gained > 0 ? 0 : gained;
8783
+ unexplained.push(unexplained[i - 1] + counted);
8784
+ if (counted > 0 && (!top || counted > top.gained)) top = { test: point.census.test, gained: counted };
8785
+ });
8786
+ const growth = pileUp(unexplained, what);
8787
+ if (!growth) continue;
8788
+ const lead = points.length === measured.length ? top : null;
8789
+ const span = measured.indexOf(points[points.length - 1].census) - measured.indexOf(points[0].census) + 1;
8790
+ const finding = {
8791
+ verdict: "piling",
8792
+ kind: "worker",
8793
+ where: "worker process",
8794
+ tests: measured.length,
8795
+ count: 1,
8796
+ growth: {
8797
+ what,
8798
+ from: Math.round(Math.min(...points.map((point) => point.value))),
8799
+ to: Math.round(points[points.length - 1].value),
8800
+ tests: span
8801
+ },
8802
+ ...lead ? { detail: `most of it during "${lead.test.title ?? ""}" (${lead.test.file})` } : {},
8803
+ gained: growth.to - growth.from
8804
+ };
8805
+ const seen = best.get(what);
8806
+ if (!seen || finding.gained > seen.gained) best.set(what, finding);
8807
+ }
8808
+ }
8809
+ return [...best.values()].map(({ gained: _gained, ...finding }) => finding);
8810
+ }
8277
8811
  function handleFindings(byWorker) {
8278
8812
  const groups = /* @__PURE__ */ new Map();
8813
+ const add2 = (test, type, count, site = null) => {
8814
+ const key = `${test.id}|${type}|${site ?? ""}`;
8815
+ const group = groups.get(key) ?? {
8816
+ verdict: "handle",
8817
+ kind: "handle",
8818
+ where: type,
8819
+ ...site ? { site } : {},
8820
+ tests: 1,
8821
+ count: 0,
8822
+ detail: `"${test.title ?? ""}" (${test.file})`
8823
+ };
8824
+ group.count += count;
8825
+ groups.set(key, group);
8826
+ };
8279
8827
  for (const censuses of byWorker.values()) {
8828
+ for (const census of censuses) {
8829
+ if (!census.test || census.test.failed) continue;
8830
+ for (const handle of census.running ?? []) add2(census.test, handle.type, 1, handle.site);
8831
+ }
8280
8832
  const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
8281
8833
  for (let i = 0; i < ordered.length - 1; i++) {
8282
8834
  const census = ordered[i];
8283
8835
  const next = ordered[i + 1];
8284
8836
  for (const [type, end] of Object.entries(census.handles.end)) {
8837
+ if (census.running && type === "TCPServerWrap") continue;
8285
8838
  const grew = end - (census.handles.start[type] ?? 0);
8286
8839
  if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
8287
- const test = census.test;
8288
- const key = `${test.id}|${type}`;
8289
- const group = groups.get(key) ?? {
8290
- verdict: "handle",
8291
- kind: "handle",
8292
- where: type,
8293
- tests: 1,
8294
- count: 0,
8295
- detail: `"${test.title ?? ""}" (${test.file})`
8296
- };
8297
- group.count += grew;
8298
- groups.set(key, group);
8840
+ add2(census.test, type, grew);
8299
8841
  }
8300
8842
  }
8301
8843
  }
@@ -8340,19 +8882,37 @@ var KIND_LABEL = {
8340
8882
  context: "context",
8341
8883
  page: "page",
8342
8884
  request: "API context",
8343
- handle: "handle"
8885
+ handle: "handle",
8886
+ worker: "worker"
8887
+ };
8888
+ var GROWTH_LABEL = {
8889
+ pages: "pages",
8890
+ listeners: "listeners",
8891
+ routes: "routes",
8892
+ elements: "elements",
8893
+ heapMb: "heap",
8894
+ descriptors: "descriptors"
8895
+ };
8896
+ var GROWTH_NOUN = {
8897
+ pages: "open pages",
8898
+ listeners: "listeners",
8899
+ routes: "route handlers",
8900
+ elements: "DOM elements",
8901
+ heapMb: "MB of heap after garbage collection",
8902
+ descriptors: "open file descriptors"
8344
8903
  };
8345
8904
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
8346
8905
  var HANDLE_LABEL = {
8347
8906
  TCPServerWrap: "server",
8348
8907
  FSEventWrap: "file watcher",
8349
- StatWatcher: "file watcher"
8908
+ StatWatcher: "file watcher",
8909
+ ChildProcess: "process"
8350
8910
  };
8351
8911
  function findingLine(finding) {
8352
8912
  const verdict = finding.verdict.padEnd(8);
8353
- const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? finding.growth.what : KIND_LABEL[finding.kind];
8913
+ const label = finding.verdict === "handle" ? HANDLE_LABEL[finding.where] ?? finding.where : finding.growth ? GROWTH_LABEL[finding.growth.what] : KIND_LABEL[finding.kind];
8354
8914
  const kind = label.padEnd(11);
8355
- const parts = finding.verdict === "handle" ? [] : [finding.where];
8915
+ const parts = finding.verdict === "handle" ? finding.site ? [finding.site] : [] : [finding.where];
8356
8916
  switch (finding.verdict) {
8357
8917
  case "leaked": {
8358
8918
  if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
@@ -8375,11 +8935,12 @@ function findingLine(finding) {
8375
8935
  break;
8376
8936
  case "piling": {
8377
8937
  const growth = finding.growth;
8378
- const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
8379
- parts.push(`${growth.from} \u2192 ${growth.to} ${noun} over ${growth.tests} tests`);
8938
+ parts.push(`${growth.from} \u2192 ${growth.to} ${GROWTH_NOUN[growth.what]} over ${growth.tests} tests`);
8939
+ if (finding.detail) parts.push(finding.detail);
8380
8940
  break;
8381
8941
  }
8382
8942
  case "handle":
8943
+ if (finding.count > 1) parts.push(`${finding.count} ${label}${label.endsWith("s") ? "es" : "s"}`);
8383
8944
  parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
8384
8945
  break;
8385
8946
  case "probable":
@@ -9244,7 +9805,7 @@ function resourceTimeline(samples, censuses) {
9244
9805
  let startedAt = hasSamples ? samples.startedAt : Infinity;
9245
9806
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
9246
9807
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
9247
- const relative11 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
9808
+ const relative12 = (points, by) => points.map(([at, value]) => [Math.round(at + by), value]);
9248
9809
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
9249
9810
  const perWorker = Math.max(
9250
9811
  60,
@@ -9252,17 +9813,17 @@ function resourceTimeline(samples, censuses) {
9252
9813
  );
9253
9814
  return {
9254
9815
  startedAt,
9255
- cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
9256
- memoryBytes: hasSamples ? downsamplePoints(relative11(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
9816
+ cpuPct: hasSamples ? downsamplePoints(relative12(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
9817
+ memoryBytes: hasSamples ? downsamplePoints(relative12(samples.memoryBytes, shift), TIMELINE_SERIES_CAP, "max") : [],
9257
9818
  pages: workers.map(({ worker, events }) => {
9258
- const points = relative11(events, -startedAt);
9819
+ const points = relative12(events, -startedAt);
9259
9820
  const last = points.pop();
9260
9821
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
9261
9822
  }),
9262
9823
  workers: trees.map((tree) => ({
9263
9824
  worker: tree.worker,
9264
- cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
9265
- memoryBytes: downsamplePoints(relative11(tree.memoryBytes, shift), perWorker, "max")
9825
+ cpuCores: downsamplePoints(relative12(tree.cpuCores, shift), perWorker, "mean"),
9826
+ memoryBytes: downsamplePoints(relative12(tree.memoryBytes, shift), perWorker, "max")
9266
9827
  }))
9267
9828
  };
9268
9829
  }
@@ -9340,13 +9901,13 @@ var LiveResources = class {
9340
9901
  const startedAt = samples?.startedAt || this.options.runStartedAt;
9341
9902
  const censuses = this.options.censuses();
9342
9903
  const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
9343
- const sent = new Map(cursors);
9904
+ const sent2 = new Map(cursors);
9344
9905
  let fresh = false;
9345
9906
  const take = (key, points, cap, merge) => {
9346
9907
  const out = since(points, cursors.get(key), cap, merge);
9347
9908
  const last = points[points.length - 1];
9348
9909
  if (out.length > 0 && last) {
9349
- sent.set(key, last[0]);
9910
+ sent2.set(key, last[0]);
9350
9911
  fresh = true;
9351
9912
  }
9352
9913
  return out;
@@ -9384,7 +9945,7 @@ var LiveResources = class {
9384
9945
  memoryCapacityBytes: memory.capacityBytes,
9385
9946
  memoryKind: memory.kind
9386
9947
  },
9387
- sent,
9948
+ sent: sent2,
9388
9949
  censusCount: censuses.length
9389
9950
  };
9390
9951
  }
@@ -9816,8 +10377,8 @@ function detailOf(parsed2, headline) {
9816
10377
  case "assertion-timeout": {
9817
10378
  const received = parsed2.received;
9818
10379
  if (received && !/element\(s\) not found/.test(received)) {
9819
- const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
9820
- if (!headline.includes(shown)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
10380
+ const shown2 = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
10381
+ if (!headline.includes(shown2)) return truncateValue(`Received: ${received}`, HEADLINE_MAX_CHARS);
9821
10382
  }
9822
10383
  const state = parsed2.lastStateLine;
9823
10384
  if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
@@ -9970,7 +10531,7 @@ function browserFunctionCoverage(files) {
9970
10531
  files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9971
10532
  });
9972
10533
  }
9973
- function newKnownFunctions(known, sent) {
10534
+ function newKnownFunctions(known, sent2) {
9974
10535
  const parts = [];
9975
10536
  let files = [];
9976
10537
  let budget = MAX_COVERAGE_RANGES;
@@ -9982,10 +10543,10 @@ function newKnownFunctions(known, sent) {
9982
10543
  for (const [raw, ranges] of known) {
9983
10544
  const path34 = normalizeCoveragePath(raw);
9984
10545
  if (path34 === null) continue;
9985
- let named = sent.get(path34);
10546
+ let named = sent2.get(path34);
9986
10547
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9987
10548
  if (fresh.length === 0) continue;
9988
- if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
10549
+ if (!named) sent2.set(path34, named = /* @__PURE__ */ new Set());
9989
10550
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9990
10551
  while (fresh.length > 0) {
9991
10552
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
@@ -10003,7 +10564,7 @@ function serverCoverageInWorker() {
10003
10564
  }
10004
10565
 
10005
10566
  // src/internal/capture/code-reach.ts
10006
- var crypto5 = __toESM(require("crypto"));
10567
+ var crypto6 = __toESM(require("crypto"));
10007
10568
  var fs23 = __toESM(require("fs"));
10008
10569
  var path23 = __toESM(require("path"));
10009
10570
  var import_node_child_process3 = require("child_process");
@@ -10393,9 +10954,9 @@ function resolveSourcePath(source, roots) {
10393
10954
  const normalized = normalizeSourcePath(source);
10394
10955
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
10395
10956
  if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
10396
- const relative11 = normalized.replace(/^\/+/, "");
10957
+ const relative12 = normalized.replace(/^\/+/, "");
10397
10958
  for (const root of roots) {
10398
- const candidate = path23.resolve(root, relative11);
10959
+ const candidate = path23.resolve(root, relative12);
10399
10960
  if (isFile(candidate)) return candidate;
10400
10961
  }
10401
10962
  return null;
@@ -10430,7 +10991,7 @@ function relativeToRepo(file, repoRoot) {
10430
10991
  return path23.relative(repoRoot, file).split(path23.sep).join("/");
10431
10992
  }
10432
10993
  function scriptVersion(entry) {
10433
- return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
10994
+ return entry.hash ?? crypto6.createHash("sha1").update(entry.source ?? "").digest("hex");
10434
10995
  }
10435
10996
  async function loadMap(entry, fetchMap) {
10436
10997
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -10828,11 +11389,11 @@ async function stopPageCoverage(page) {
10828
11389
 
10829
11390
  // src/internal/collect/blob-ids.ts
10830
11391
  var import_node_child_process4 = require("child_process");
10831
- var crypto6 = __toESM(require("crypto"));
11392
+ var crypto7 = __toESM(require("crypto"));
10832
11393
  var fs24 = __toESM(require("fs"));
10833
11394
  var path24 = __toESM(require("path"));
10834
11395
  function gitBlobId(content) {
10835
- return crypto6.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
11396
+ return crypto7.createHash("sha1").update(`blob ${content.length}\0`).update(content).digest("hex");
10836
11397
  }
10837
11398
  function namesLines(file) {
10838
11399
  return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
@@ -10970,10 +11531,10 @@ function composeTestCoverage(server, browserFiles, settings, cssFiles) {
10970
11531
  const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
10971
11532
  return merged ? sanitizeCoverage(merged) : null;
10972
11533
  }
10973
- function addRanges(into, filePath2, ranges) {
11534
+ function addRanges(into, filePath3, ranges) {
10974
11535
  if (ranges.length === 0) return;
10975
- let known = into.get(filePath2);
10976
- if (!known) into.set(filePath2, known = /* @__PURE__ */ new Map());
11536
+ let known = into.get(filePath3);
11537
+ if (!known) into.set(filePath3, known = /* @__PURE__ */ new Map());
10977
11538
  for (const range of ranges) known.set(`${range[0]}:${range[1]}`, range);
10978
11539
  }
10979
11540
  function chunkRunCoverage(files) {
@@ -11052,8 +11613,9 @@ var CoverageCollection = class {
11052
11613
  if (browser === "off" && css !== "rules") return {};
11053
11614
  const paths = browser === "off" ? void 0 : testCase.codeReach;
11054
11615
  for (const known of attachments ?? []) {
11055
- if (browser === "functions" && known.name === ATTACHMENT_NAMES.browserFunctions) this.addKnownFunctions(known);
11056
- if (css === "rules" && known.name === ATTACHMENT_NAMES.cssRules) this.addKnownFunctions(known);
11616
+ if (browser === "functions" && isPiwiAttachment(known, ATTACHMENT_NAMES.browserFunctions))
11617
+ this.addKnownFunctions(known);
11618
+ if (css === "rules" && isPiwiAttachment(known, ATTACHMENT_NAMES.cssRules)) this.addKnownFunctions(known);
11057
11619
  }
11058
11620
  const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
11059
11621
  const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
@@ -11067,7 +11629,7 @@ var CoverageCollection = class {
11067
11629
  }
11068
11630
  /** The browser files of a test's attachment holding line ranges, added to the run's. */
11069
11631
  rangedFiles(attachments, name) {
11070
- const attachment = attachments?.find((a) => a.name === name);
11632
+ const attachment = findPiwiAttachment(attachments, name);
11071
11633
  if (!attachment?.body) return [];
11072
11634
  let files = [];
11073
11635
  try {
@@ -11091,8 +11653,8 @@ var CoverageCollection = class {
11091
11653
  const files = parsed2?.v === 1 ? parsed2.files : null;
11092
11654
  if (!Array.isArray(files)) return;
11093
11655
  for (const item of files) {
11094
- const filePath2 = normalizeCoveragePath(item?.path);
11095
- if (filePath2 !== null) addRanges(this.browserKnown, filePath2, normalizeFunctionRanges(item.allFunctions));
11656
+ const filePath3 = normalizeCoveragePath(item?.path);
11657
+ if (filePath3 !== null) addRanges(this.browserKnown, filePath3, normalizeFunctionRanges(item.allFunctions));
11096
11658
  }
11097
11659
  }
11098
11660
  /**
@@ -11103,13 +11665,13 @@ var CoverageCollection = class {
11103
11665
  async browserRunFiles() {
11104
11666
  if (!this.browserBlobs) return [];
11105
11667
  const paths = [.../* @__PURE__ */ new Set([...this.browserKnown.keys(), ...this.browserRan.keys()])].sort();
11106
- const files = paths.map((filePath2) => {
11107
- const functions = normalizeFunctionRanges([...this.browserRan.get(filePath2)?.values() ?? []]);
11668
+ const files = paths.map((filePath3) => {
11669
+ const functions = normalizeFunctionRanges([...this.browserRan.get(filePath3)?.values() ?? []]);
11108
11670
  const allFunctions = normalizeFunctionRanges([
11109
- ...this.browserKnown.get(filePath2)?.values() ?? [],
11671
+ ...this.browserKnown.get(filePath3)?.values() ?? [],
11110
11672
  ...functions
11111
11673
  ]);
11112
- const file = { path: filePath2, runtime: "browser" };
11674
+ const file = { path: filePath3, runtime: "browser" };
11113
11675
  if (functions.length > 0) file.functions = functions;
11114
11676
  if (allFunctions.length > 0) file.allFunctions = allFunctions;
11115
11677
  return file;
@@ -11128,7 +11690,7 @@ var CoverageCollection = class {
11128
11690
  */
11129
11691
  apply(testCase, attachments) {
11130
11692
  const browser = this.browserCoverage(testCase, attachments);
11131
- const attachment = this.collector ? attachments?.find((a) => a.name === ATTACHMENT_NAMES.serverCoverageRequests) : void 0;
11693
+ const attachment = this.collector ? findPiwiAttachment(attachments, ATTACHMENT_NAMES.serverCoverageRequests) : void 0;
11132
11694
  const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
11133
11695
  if (!this.collector || !requests) {
11134
11696
  if (!(browser instanceof Promise)) {
@@ -11674,7 +12236,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11674
12236
  if (frames.length) testCase.testSourceFrames = frames;
11675
12237
  }
11676
12238
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
11677
- const pauses = readPauses(result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.pauses)?.body);
12239
+ const pauses = readPauses(findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.pauses)?.body);
11678
12240
  testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
11679
12241
  const stepEvents = extractTestStepEvents(result.steps, result.startTime);
11680
12242
  const waitEvents = extractWaitEvents(result.steps);
@@ -11684,14 +12246,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11684
12246
  if (this.options.collectPerformanceMetrics && result.attachments) {
11685
12247
  this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
11686
12248
  this.manifests?.note(testCase.networkRequests);
11687
- const locatorAttachment = this.options.captureLocators !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.locators) : void 0;
12249
+ const locatorAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locators) : void 0;
11688
12250
  if (locatorAttachment?.body) {
11689
12251
  try {
11690
12252
  testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
11691
12253
  } catch {
11692
12254
  }
11693
12255
  }
11694
- const pagesAttachment = this.options.captureLocators !== false ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.locatorPages) : void 0;
12256
+ const pagesAttachment = this.options.captureLocators !== false ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.locatorPages) : void 0;
11695
12257
  if (pagesAttachment?.body) {
11696
12258
  try {
11697
12259
  testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
@@ -11700,7 +12262,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11700
12262
  }
11701
12263
  }
11702
12264
  if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
11703
- const reachAttachment = this.coverageSettings.browser !== "off" ? result.attachments.find((a) => a.name === ATTACHMENT_NAMES.codeReach) : void 0;
12265
+ const reachAttachment = this.coverageSettings.browser !== "off" ? findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.codeReach) : void 0;
11704
12266
  if (reachAttachment?.body) {
11705
12267
  try {
11706
12268
  const files = JSON.parse(reachAttachment.body.toString());
@@ -11772,7 +12334,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11772
12334
  } catch {
11773
12335
  }
11774
12336
  }
11775
- const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
12337
+ const attachment = findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.resources);
11776
12338
  const census = parseResourceCensus(attachment?.body);
11777
12339
  if (census) {
11778
12340
  this.resourceCensuses.push(census);
@@ -12792,14 +13354,14 @@ function showAnchorPicker(arg) {
12792
13354
  } catch {
12793
13355
  shadowRoots.length = 0;
12794
13356
  }
12795
- const within = (node2, scope) => {
13357
+ const within2 = (node2, scope) => {
12796
13358
  if (scope === doc) return true;
12797
13359
  for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
12798
13360
  return false;
12799
13361
  };
12800
13362
  const queryAll = (scope, sel) => {
12801
13363
  const found = Array.from(scope.querySelectorAll(sel));
12802
- for (const sr of shadowRoots) if (within(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
13364
+ for (const sr of shadowRoots) if (within2(sr.host, scope)) found.push(...Array.from(sr.querySelectorAll(sel)));
12803
13365
  return found;
12804
13366
  };
12805
13367
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
@@ -12880,8 +13442,8 @@ function showAnchorPicker(arg) {
12880
13442
  return;
12881
13443
  }
12882
13444
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
12883
- const outline = doc.createElement("div");
12884
- 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);`;
13445
+ const outline2 = doc.createElement("div");
13446
+ 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);`;
12885
13447
  const outlineLabel = doc.createElement("div");
12886
13448
  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);`;
12887
13449
  const pickedOutline = doc.createElement("div");
@@ -12970,11 +13532,11 @@ function showAnchorPicker(arg) {
12970
13532
  line.appendChild(text);
12971
13533
  line.addEventListener("mouseenter", () => {
12972
13534
  const r = row.node.getBoundingClientRect();
12973
- outline.style.display = "block";
12974
- outline.style.left = r.left + "px";
12975
- outline.style.top = r.top + "px";
12976
- outline.style.width = r.width + "px";
12977
- outline.style.height = r.height + "px";
13535
+ outline2.style.display = "block";
13536
+ outline2.style.left = r.left + "px";
13537
+ outline2.style.top = r.top + "px";
13538
+ outline2.style.width = r.width + "px";
13539
+ outline2.style.height = r.height + "px";
12978
13540
  outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
12979
13541
  outlineLabel.style.display = "block";
12980
13542
  const lr = outlineLabel.getBoundingClientRect();
@@ -12984,7 +13546,7 @@ function showAnchorPicker(arg) {
12984
13546
  outlineLabel.style.top = top + "px";
12985
13547
  });
12986
13548
  line.addEventListener("mouseleave", () => {
12987
- outline.style.display = "none";
13549
+ outline2.style.display = "none";
12988
13550
  outlineLabel.style.display = "none";
12989
13551
  });
12990
13552
  box.addEventListener("change", () => {
@@ -12998,7 +13560,7 @@ function showAnchorPicker(arg) {
12998
13560
  const cleanup = () => {
12999
13561
  doc.removeEventListener("keydown", onKey, true);
13000
13562
  panel.remove();
13001
- outline.remove();
13563
+ outline2.remove();
13002
13564
  outlineLabel.remove();
13003
13565
  pickedOutline.remove();
13004
13566
  if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
@@ -13063,7 +13625,7 @@ function showAnchorPicker(arg) {
13063
13625
  );
13064
13626
  }
13065
13627
  };
13066
- mount(pickedOutline, outline, outlineLabel, panel);
13628
+ mount(pickedOutline, outline2, outlineLabel, panel);
13067
13629
  }
13068
13630
 
13069
13631
  // ../picker-dom/src/overlay-confirm.ts
@@ -13167,7 +13729,7 @@ function showPauseBar(arg) {
13167
13729
  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;`;
13168
13730
  const where = doc.createElement("div");
13169
13731
  where.style.cssText = "font-weight:600;white-space:nowrap;";
13170
- where.textContent = `Paused at ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
13732
+ where.textContent = `${arg.failure ? "Failed at" : "Paused at"} ${arg.place}${arg.attempt ? ` \xB7 attempt ${arg.attempt}` : ""}`;
13171
13733
  const what = doc.createElement("code");
13172
13734
  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;";
13173
13735
  what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
@@ -13190,13 +13752,13 @@ function showPauseBar(arg) {
13190
13752
  delete g.__piwiPauseCleanup;
13191
13753
  };
13192
13754
  const buttons = [
13193
- ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
13194
- ["Step", "step", "Pause again before the next call", false],
13755
+ arg.failure ? ["Resume", "resume", "Let the test end as it failed (Esc)", true] : ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
13756
+ ...arg.failure ? [] : [["Step", "step", "Pause again before the next call", false]],
13195
13757
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
13196
13758
  ...arg.record ? [
13197
13759
  ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
13198
13760
  ] : [],
13199
- ["Finish", "finish", "Run on and pause no more in this test", false]
13761
+ ...arg.failure ? [] : [["Finish", "finish", "Run on and pause no more in this test", false]]
13200
13762
  ];
13201
13763
  for (const [label, choice, hint, primary] of buttons) {
13202
13764
  const btn = doc.createElement("button");
@@ -14604,11 +15166,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
14604
15166
  return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
14605
15167
  }
14606
15168
  var parsed = null;
15169
+ var sent = null;
14607
15170
  function currentPauseSet() {
15171
+ if (sent) return sent;
14608
15172
  const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
14609
15173
  if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
14610
15174
  return parsed.set;
14611
15175
  }
15176
+ function replacePauseSet(places, cwd = process.cwd()) {
15177
+ if (!places) {
15178
+ sent = null;
15179
+ return currentPauseSet();
15180
+ }
15181
+ const lines = new Set(places.map((place) => placeLine(place, cwd)));
15182
+ sent = lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
15183
+ return sent;
15184
+ }
15185
+ function callSitePlace(callSite) {
15186
+ const m = /^(.+):(\d+):(\d+)$/.exec(callSite);
15187
+ if (!m || Number(m[2]) < 1) return null;
15188
+ const column = Number(m[3]);
15189
+ return column > 0 ? { file: m[1], line: Number(m[2]), column } : { file: m[1], line: Number(m[2]) };
15190
+ }
15191
+ function placeLine(place, cwd = process.cwd()) {
15192
+ return `${callSiteFile(place.file, cwd)}:${place.line}`;
15193
+ }
14612
15194
  function pauseGateFromTestInfo(testInfo) {
14613
15195
  const use = testInfo.project?.use ?? {};
14614
15196
  const debug = testInfo._configInternal?.configCLIOverrides?.debug;
@@ -14616,7 +15198,7 @@ function pauseGateFromTestInfo(testInfo) {
14616
15198
  }
14617
15199
  function pauseSkipReason(gate) {
14618
15200
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
14619
- if (gate.headless !== false)
15201
+ if (gate.headless !== false && !gate.channel)
14620
15202
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
14621
15203
  if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
14622
15204
  return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
@@ -14624,20 +15206,42 @@ function pauseSkipReason(gate) {
14624
15206
  return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
14625
15207
  return null;
14626
15208
  }
15209
+ function pauseState(set, pause = false) {
15210
+ return { set, step: false, finished: false, pause, runTo: null };
15211
+ }
15212
+ function wantsStops(state) {
15213
+ return state.step || state.pause || state.runTo !== null || !state.finished && state.set.size > 0;
15214
+ }
15215
+ function atBreakpoint(state, callSite) {
15216
+ return !state.finished && state.set.matches(callSite);
15217
+ }
14627
15218
  function atStop(state, callSite) {
14628
- if (callSite && !state.finished) {
14629
- if (state.step) {
14630
- state.step = false;
14631
- return { show: "step" };
15219
+ if (callSite) {
15220
+ const reason = state.step ? "step" : state.pause ? "pause" : state.runTo !== null && callSiteLine(callSite) === state.runTo ? "run-to" : atBreakpoint(state, callSite) ? "breakpoint" : null;
15221
+ if (reason) {
15222
+ stopShown(state);
15223
+ return { show: reason };
14632
15224
  }
14633
- if (state.set.matches(callSite)) return { show: "breakpoint" };
14634
15225
  }
14635
15226
  return { go: goOn(state) };
14636
15227
  }
15228
+ function stopShown(state) {
15229
+ state.step = false;
15230
+ state.pause = false;
15231
+ state.runTo = null;
15232
+ }
15233
+ function askedOnce(state) {
15234
+ return { step: state.step, pause: state.pause, runTo: state.runTo };
15235
+ }
15236
+ function askAgain(state, asked) {
15237
+ state.step ||= asked.step;
15238
+ state.pause ||= asked.pause;
15239
+ state.runTo ??= asked.runTo;
15240
+ }
14637
15241
  function goOn(state, answer) {
14638
15242
  if (answer === "step") state.step = true;
14639
15243
  else if (answer === "finish") state.finished = true;
14640
- return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
15244
+ return wantsStops(state) ? "next" : "resume";
14641
15245
  }
14642
15246
  function contextDebugger(context) {
14643
15247
  const dbg = context.debugger;
@@ -14773,38 +15377,83 @@ function logPauseSkipOnce(reason) {
14773
15377
  var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
14774
15378
  var MAX_NAVIGATED = 10;
14775
15379
  var HIGHLIGHT_MS = 2e3;
14776
- async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14777
- let shown = null;
15380
+ var BarIntents = class {
15381
+ constructor() {
15382
+ this.take = null;
15383
+ this.queued = null;
15384
+ this.busy = false;
15385
+ }
15386
+ /** Hand the bar an intent; false while it runs one, or holds one already. */
15387
+ offer(intent) {
15388
+ if (this.busy || this.queued) return false;
15389
+ const take = this.take;
15390
+ this.take = null;
15391
+ if (take) take(intent);
15392
+ else this.queued = intent;
15393
+ return true;
15394
+ }
15395
+ /** The bar waits for its answer: settles with the next intent handed to it. */
15396
+ wait() {
15397
+ this.busy = false;
15398
+ const queued = this.queued;
15399
+ this.queued = null;
15400
+ if (queued) return Promise.resolve(queued);
15401
+ return new Promise((resolve11) => {
15402
+ this.take = resolve11;
15403
+ });
15404
+ }
15405
+ /** The bar got its answer: it takes no intent until it waits again. */
15406
+ answered() {
15407
+ this.take = null;
15408
+ this.busy = true;
15409
+ }
15410
+ };
15411
+ var TAKE_DOWN_MS = 1e3;
15412
+ async function within(run, ms) {
15413
+ let timer;
15414
+ try {
15415
+ return await Promise.race([
15416
+ run,
15417
+ new Promise((resolve11) => timer = setTimeout(() => resolve11(void 0), ms))
15418
+ ]);
15419
+ } finally {
15420
+ clearTimeout(timer);
15421
+ }
15422
+ }
15423
+ async function pauseHere(page, point, probe, onPick, onRecord, ended, intents, onIntent) {
15424
+ let shown2 = null;
14778
15425
  try {
14779
15426
  const highlight = point.target?.highlight;
14780
15427
  if (typeof highlight === "function") {
14781
- let timer;
14782
- shown = await Promise.race([
14783
- Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
14784
- new Promise((resolve11) => {
14785
- timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
14786
- })
14787
- ]);
14788
- clearTimeout(timer);
15428
+ const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
15429
+ shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
14789
15430
  }
14790
15431
  const arg = {
14791
15432
  place: pausePlace(point.location),
14792
15433
  attempt: point.attempt,
14793
15434
  action: point.call,
14794
15435
  locator: null,
14795
- record: !!onRecord
15436
+ record: !!onRecord,
15437
+ ...point.failure ? { failure: true } : {}
14796
15438
  };
14797
15439
  console.log(
14798
- `[piwi] Paused at ${callSiteLine(point.location)} before ${point.call}: Resume, Step, Pick a locator${onRecord ? ", Record here" : ""} or Finish in the browser (Esc resumes).`
15440
+ 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).`
14799
15441
  );
14800
15442
  let navigated = 0;
14801
15443
  for (; ; ) {
14802
15444
  try {
14803
15445
  if (ended?.aborted) return "resume";
14804
- await page.evaluate(showPauseBar, arg);
14805
- await unlessEnded(page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }), ended);
15446
+ await unlessEnded(page.evaluate(showPauseBar, arg), ended);
15447
+ const answered = page.waitForFunction(pauseAnswered, void 0, { timeout: 0, polling: 250 }).then(() => null);
15448
+ const intent = await unlessEnded(intents ? Promise.race([answered, intents.wait()]) : answered, ended);
15449
+ intents?.answered();
15450
+ if (ended?.aborted) return "resume";
15451
+ const taken = await unlessEnded(page.evaluate(takePauseState), ended);
15452
+ if (ended?.aborted) return "resume";
15453
+ const choice = taken ?? intent ?? null;
15454
+ const raised = intent && choice === intent ? page.bringToFront().catch(() => void 0) : null;
15455
+ if (raised) await unlessEnded(raised, ended);
14806
15456
  if (ended?.aborted) return "resume";
14807
- const choice = await page.evaluate(takePauseState);
14808
15457
  navigated = 0;
14809
15458
  if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14810
15459
  if (choice === "record" && onRecord) {
@@ -14814,31 +15463,290 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14814
15463
  );
14815
15464
  continue;
14816
15465
  }
15466
+ if ((choice === "assert" || choice === "assert-region") && onIntent) {
15467
+ await unlessEnded(
15468
+ onIntent(choice, ended).catch(() => void 0),
15469
+ ended
15470
+ );
15471
+ continue;
15472
+ }
14817
15473
  if (choice !== "pick") continue;
14818
15474
  const picked = await pickLocator(page, probe, null, ended).catch(() => null);
14819
15475
  if (picked && !ended?.aborted) await onPick(picked);
14820
15476
  } catch (error) {
14821
15477
  if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
14822
- await page.waitForLoadState("domcontentloaded").catch(() => void 0);
15478
+ await unlessEnded(
15479
+ page.waitForLoadState("domcontentloaded").catch(() => void 0),
15480
+ ended
15481
+ );
14823
15482
  }
14824
15483
  }
14825
15484
  } catch {
14826
15485
  return "resume";
14827
15486
  } finally {
15487
+ const takeDown = async () => {
15488
+ try {
15489
+ if (ended?.aborted) await page.evaluate(takePauseState);
15490
+ const dispose = shown2?.dispose;
15491
+ const hide = page.hideHighlight;
15492
+ if (typeof dispose === "function") await dispose.call(shown2);
15493
+ else if (typeof hide === "function") await hide.call(page);
15494
+ } catch {
15495
+ }
15496
+ };
15497
+ await within(takeDown(), TAKE_DOWN_MS);
15498
+ }
15499
+ }
15500
+
15501
+ // src/internal/capture/chain-locator.ts
15502
+ function argValue(page, arg) {
15503
+ switch (arg.type) {
15504
+ case "string":
15505
+ case "number":
15506
+ case "boolean":
15507
+ return arg.value;
15508
+ case "regex":
15509
+ return new RegExp(arg.source, arg.flags);
15510
+ case "object":
15511
+ return Object.fromEntries(arg.entries.map(([key, value]) => [key, argValue(page, value)]));
15512
+ case "chain":
15513
+ return chainOn(page, arg.chain);
15514
+ }
15515
+ }
15516
+ function chainOn(page, chain) {
15517
+ let current = page;
15518
+ for (const call of chain.calls) {
15519
+ if (typeof current[call.method] !== "function") {
15520
+ throw new Error(`${call.method}() cannot be called there.`);
15521
+ }
15522
+ current = callMethod(
15523
+ current,
15524
+ call.method,
15525
+ call.args.map((arg) => argValue(page, arg))
15526
+ );
15527
+ }
15528
+ if (!current || typeof current.count !== "function") {
15529
+ throw new Error("The chain does not end on a locator.");
15530
+ }
15531
+ return current;
15532
+ }
15533
+ function locatorOn(page, source) {
15534
+ const chain = tryParseLocatorChain(source);
15535
+ if (!chain?.calls.length) return null;
15536
+ try {
15537
+ return chainOn(page, chain);
15538
+ } catch {
15539
+ return null;
15540
+ }
15541
+ }
15542
+
15543
+ // src/internal/capture/region-snapshot.ts
15544
+ function regionSnapshotWithoutSecrets(snapshot, secrets) {
15545
+ const hidden = new Set(secrets.filter(Boolean));
15546
+ const kept = [];
15547
+ for (const line of snapshot.split("\n")) {
15548
+ const box = /^(\s*- textbox(?: "(?:[^"\\]|\\.)*")?(?: \[[^\]]*\])*): (.*)$/.exec(line);
15549
+ if (box && hidden.size && hidden.has(yamlScalar(box[2]))) {
15550
+ kept.push({ line: box[1], lostChild: false });
15551
+ continue;
15552
+ }
15553
+ const url = /^\s*- \/url: (.*)$/.exec(line);
15554
+ if (url && hideSecretsInUrl(yamlScalar(url[1])) !== yamlScalar(url[1])) {
15555
+ let parent = kept.length - 1;
15556
+ while (parent >= 0 && indentOf(kept[parent].line) >= indentOf(line)) parent--;
15557
+ if (parent >= 0) kept[parent].lostChild = true;
15558
+ continue;
15559
+ }
15560
+ kept.push({ line, lostChild: false });
15561
+ }
15562
+ return kept.map(({ line, lostChild }, i) => {
15563
+ const next = kept[i + 1];
15564
+ const childless = !next || indentOf(next.line) <= indentOf(line);
15565
+ return lostChild && childless && line.endsWith(":") ? line.slice(0, -1) : line;
15566
+ }).join("\n");
15567
+ }
15568
+ function indentOf(line) {
15569
+ return line.length - line.trimStart().length;
15570
+ }
15571
+ function yamlScalar(text) {
15572
+ const value = text.trim();
15573
+ if (value.startsWith('"')) {
14828
15574
  try {
14829
- if (ended?.aborted) await page.evaluate(takePauseState);
14830
- const dispose = shown?.dispose;
14831
- const hide = page.hideHighlight;
14832
- if (typeof dispose === "function") await dispose.call(shown);
14833
- else if (typeof hide === "function") await hide.call(page);
15575
+ return String(JSON.parse(value));
14834
15576
  } catch {
15577
+ return value;
15578
+ }
15579
+ }
15580
+ if (value.startsWith("'") && value.endsWith("'") && value.length > 1) return value.slice(1, -1).replace(/''/g, "'");
15581
+ return value;
15582
+ }
15583
+
15584
+ // ../core/src/run-channel.ts
15585
+ var RUN_CHANNEL_ENV = "PIWI_RUN_CHANNEL";
15586
+ var RUN_HOLDS = ["failure"];
15587
+ var RUN_INTENTS = ["pick", "record", "assert", "assert-region"];
15588
+ var RUN_TAKES = [...RUN_INTENTS, "requests", "mock", "locator"];
15589
+ var RUN_INTENT_NAMES = {
15590
+ pick: "Pick a locator",
15591
+ record: "Record here",
15592
+ assert: "Assert",
15593
+ "assert-region": "Assert a region"
15594
+ };
15595
+ var MAX_KEPT_RESPONSES = 50;
15596
+ var MAX_KEPT_BODY = 256 * 1024;
15597
+ var MAX_REGION_SNAPSHOT = 1024 * 1024;
15598
+ var MAX_STOP_ERROR = 1e3;
15599
+ function isObject2(value) {
15600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15601
+ }
15602
+ function readJson(line) {
15603
+ try {
15604
+ const value = JSON.parse(line);
15605
+ return isObject2(value) ? value : null;
15606
+ } catch {
15607
+ return null;
15608
+ }
15609
+ }
15610
+ function isInt(value, min) {
15611
+ return typeof value === "number" && Number.isInteger(value) && value >= min;
15612
+ }
15613
+ function filePath2(value) {
15614
+ return typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
15615
+ }
15616
+ function readPlace(value) {
15617
+ if (!isObject2(value) || !filePath2(value.file) || !isInt(value.line, 1)) return null;
15618
+ if (value.column === void 0) return { file: value.file, line: value.line };
15619
+ return isInt(value.column, 1) ? { file: value.file, line: value.line, column: value.column } : null;
15620
+ }
15621
+ function isIntent(value) {
15622
+ return RUN_INTENTS.includes(value);
15623
+ }
15624
+ function readHolds(value) {
15625
+ if (!Array.isArray(value)) return void 0;
15626
+ return value.length > 16 ? [] : RUN_HOLDS.filter((hold) => value.includes(hold));
15627
+ }
15628
+ function isLocatorText(value) {
15629
+ return typeof value === "string" && value.length > 0 && value.length <= 1e3 && !/[\r\n\u2028\u2029]/.test(value);
15630
+ }
15631
+ var MAX_PLACES = 1e4;
15632
+ function parseHubToRun(line) {
15633
+ const value = readJson(line);
15634
+ if (!value) return null;
15635
+ const unknown = (what) => ({ type: "unknown", what: what.slice(0, 64) });
15636
+ switch (value.type) {
15637
+ case "attached": {
15638
+ const holds = readHolds(value.holds);
15639
+ return holds ? { type: "attached", holds } : null;
15640
+ }
15641
+ case "breakpoints": {
15642
+ if (!Array.isArray(value.places) || value.places.length > MAX_PLACES) return null;
15643
+ const places = value.places.map(readPlace);
15644
+ return places.every((p) => p !== null) ? { type: "breakpoints", places } : null;
15645
+ }
15646
+ case "control": {
15647
+ const { action, stop } = value;
15648
+ if (action === "pause") return { type: "control", action };
15649
+ if (action === "step" || action === "resume" || action === "finish") {
15650
+ return isInt(stop, 1) ? { type: "control", action, stop } : null;
15651
+ }
15652
+ return typeof action === "string" ? unknown(`control ${action}`) : null;
15653
+ }
15654
+ case "run-to": {
15655
+ const place = readPlace(value.place);
15656
+ return isInt(value.stop, 1) && place ? { type: "run-to", stop: value.stop, place } : null;
15657
+ }
15658
+ case "request": {
15659
+ const { request: request3, what, stop } = value;
15660
+ if (!isInt(request3, 1)) return null;
15661
+ if (what === "image") return { type: "request", request: request3, what: "image" };
15662
+ if (typeof what !== "string") return null;
15663
+ if (!isIntent(what) && what !== "requests" && what !== "mock" && what !== "locator") {
15664
+ return unknown(`request ${what}`);
15665
+ }
15666
+ if (!isInt(stop, 1)) return null;
15667
+ if (isIntent(what) || what === "requests") return { type: "request", request: request3, what, stop };
15668
+ if (what === "mock") return isInt(value.id, 1) ? { type: "request", request: request3, what, stop, id: value.id } : null;
15669
+ const { locator } = value;
15670
+ return locator === null || isLocatorText(locator) ? { type: "request", request: request3, what, stop, locator } : null;
14835
15671
  }
15672
+ default:
15673
+ return typeof value.type === "string" && value.type ? unknown(value.type) : null;
15674
+ }
15675
+ }
15676
+
15677
+ // src/internal/capture/kept-responses.ts
15678
+ var keeping = null;
15679
+ var TEXT_TYPE = /^(?:text\/[\w.+-]+|application\/(?:[\w.+-]+\+)?(?:json|xml)|application\/(?:javascript|x-www-form-urlencoded|graphql))$/i;
15680
+ function mimeOf(contentType) {
15681
+ return (contentType ?? "").split(";")[0].trim().toLowerCase();
15682
+ }
15683
+ function textOf3(body) {
15684
+ try {
15685
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
15686
+ return text.includes("\0") ? null : text;
15687
+ } catch {
15688
+ return null;
15689
+ }
15690
+ }
15691
+ function startKeeping(now = Date.now()) {
15692
+ keeping = { startedAt: now, kept: [], last: 0 };
15693
+ }
15694
+ function stopKeeping() {
15695
+ keeping = null;
15696
+ }
15697
+ async function keepResponse(request3, response) {
15698
+ const into = keeping;
15699
+ if (!into || !response) return;
15700
+ try {
15701
+ const kind = request3.resourceType();
15702
+ if (kind !== "fetch" && kind !== "xhr") return;
15703
+ const url = hideSecretsInUrl(request3.url());
15704
+ const method = request3.method().toUpperCase();
15705
+ if (url.length > 8192 || !/^[A-Z]{1,16}$/.test(method)) return;
15706
+ const headers = response.headers();
15707
+ const type = mimeOf(headers["content-type"]);
15708
+ if (type && !TEXT_TYPE.test(type)) return;
15709
+ const status = response.status();
15710
+ if (status < 100 || status > 599) return;
15711
+ const time = Math.max(0, Math.round(Date.now() - into.startedAt));
15712
+ const size = /^\d+$/.test(headers["content-length"] ?? "") ? Number(headers["content-length"]) : null;
15713
+ if (size !== null && size > MAX_KEPT_BODY) return;
15714
+ into.kept.push({ id: ++into.last, method, url, status, type, size, time, response });
15715
+ if (into.kept.length > MAX_KEPT_RESPONSES) into.kept.splice(0, into.kept.length - MAX_KEPT_RESPONSES);
15716
+ } catch {
15717
+ }
15718
+ }
15719
+ function keptRequests() {
15720
+ return (keeping?.kept ?? []).map(({ id, method, url, status, type, size, time }) => ({
15721
+ id,
15722
+ method,
15723
+ url,
15724
+ status,
15725
+ type,
15726
+ size,
15727
+ time
15728
+ }));
15729
+ }
15730
+ async function keptResponse(id) {
15731
+ const found = keeping?.kept.find((kept) => kept.id === id);
15732
+ if (!found) return { refusal: "The test kept no such response: it keeps the latest 50." };
15733
+ let body;
15734
+ try {
15735
+ body = await internalCall(found.response, () => found.response.body());
15736
+ } catch {
15737
+ return {
15738
+ refusal: "The browser no longer holds this answer's body: mock one the page received since it last loaded."
15739
+ };
14836
15740
  }
15741
+ if (body.length > MAX_KEPT_BODY) return { refusal: "This answer is larger than 256 KB: Piwi mocks a smaller one." };
15742
+ const text = textOf3(body);
15743
+ if (text === null) return { refusal: "This answer is not text: Piwi mocks a text answer." };
15744
+ return { method: found.method, url: found.url, status: found.status, type: found.type, body: text };
14837
15745
  }
14838
15746
 
14839
15747
  // src/internal/capture/record-handoff.ts
14840
15748
  var fs26 = __toESM(require("fs"));
14841
- var net2 = __toESM(require("net"));
15749
+ var net3 = __toESM(require("net"));
14842
15750
  var path29 = __toESM(require("path"));
14843
15751
 
14844
15752
  // ../core/src/ide-recorder.ts
@@ -14854,7 +15762,7 @@ function parseRecordChannel(value) {
14854
15762
  const path34 = text.slice(0, hash);
14855
15763
  return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
14856
15764
  }
14857
- function isObject2(value) {
15765
+ function isObject3(value) {
14858
15766
  return typeof value === "object" && value !== null && !Array.isArray(value);
14859
15767
  }
14860
15768
  function parseHandoffToTest(line) {
@@ -14864,7 +15772,7 @@ function parseHandoffToTest(line) {
14864
15772
  } catch {
14865
15773
  return null;
14866
15774
  }
14867
- if (!isObject2(value)) return null;
15775
+ if (!isObject3(value)) return null;
14868
15776
  switch (value.type) {
14869
15777
  case "refused":
14870
15778
  return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
@@ -14927,8 +15835,8 @@ async function recordFromHere(options) {
14927
15835
  const use = testInfo.project?.use ?? {};
14928
15836
  const context = page.context();
14929
15837
  return new Promise((resolve11) => {
14930
- const socket = net2.connect(channel.path);
14931
- socket.setEncoding("utf8");
15838
+ const socket2 = net3.connect(channel.path);
15839
+ socket2.setEncoding("utf8");
14932
15840
  const answers = /* @__PURE__ */ new Map();
14933
15841
  let calls = 0;
14934
15842
  let attached = false;
@@ -14936,7 +15844,7 @@ async function recordFromHere(options) {
14936
15844
  let settled = false;
14937
15845
  let initScript;
14938
15846
  const send = (message) => {
14939
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
15847
+ if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
14940
15848
  `);
14941
15849
  };
14942
15850
  const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
@@ -14986,7 +15894,7 @@ async function recordFromHere(options) {
14986
15894
  page.off("close", onGone);
14987
15895
  page.off("crash", onGone);
14988
15896
  ended?.removeEventListener("abort", onEnded);
14989
- socket.end();
15897
+ socket2.end();
14990
15898
  resolve11(recording ? null : said);
14991
15899
  };
14992
15900
  const relayed = (source, payload) => new Promise((answer) => {
@@ -15026,7 +15934,7 @@ async function recordFromHere(options) {
15026
15934
  context.on("page", onPage);
15027
15935
  send({ type: "loaded" });
15028
15936
  };
15029
- socket.on("connect", () => {
15937
+ socket2.on("connect", () => {
15030
15938
  attached = true;
15031
15939
  send({
15032
15940
  type: "attach",
@@ -15035,10 +15943,11 @@ async function recordFromHere(options) {
15035
15943
  line: place.line,
15036
15944
  reason: options.reason,
15037
15945
  testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
15038
- baseURL: typeof use.baseURL === "string" ? use.baseURL : null
15946
+ baseURL: typeof use.baseURL === "string" ? use.baseURL : null,
15947
+ ...options.mode === "check" ? { mode: "check", stop: options.stop ?? null } : {}
15039
15948
  });
15040
15949
  });
15041
- socket.on(
15950
+ socket2.on(
15042
15951
  "data",
15043
15952
  lineReader(
15044
15953
  (line) => {
@@ -15071,13 +15980,257 @@ async function recordFromHere(options) {
15071
15980
  () => finish("the editor sent a message too long to read.")
15072
15981
  )
15073
15982
  );
15074
- socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
15075
- socket.on("close", () => finish("the editor closed the recording."));
15983
+ socket2.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
15984
+ socket2.on("close", () => finish("the editor closed the recording."));
15076
15985
  });
15077
15986
  }
15078
15987
 
15988
+ // src/internal/capture/run-channel.ts
15989
+ var net4 = __toESM(require("net"));
15990
+ var CONNECT_MS = 2e3;
15991
+ var BAR_HIDDEN = "#__piwi-pause-bar { display: none !important; }";
15992
+ var OUTLINE_STYLE = "outline: 2px dashed #a78bfa; outline-offset: 2px; background: rgba(167, 139, 250, 0.15);";
15993
+ var WENT_ON = "The test went on.";
15994
+ var socket = null;
15995
+ var control = null;
15996
+ var shown = /* @__PURE__ */ new Set();
15997
+ var stops = 0;
15998
+ var held = null;
15999
+ var outline = null;
16000
+ var counting = Promise.resolve();
16001
+ async function openRunChannel(worker, handlers, env = process.env) {
16002
+ const channel = parseRecordChannel(env[RUN_CHANNEL_ENV]);
16003
+ const ref = env[PIWI_ORIGIN_ENV.ref];
16004
+ if (socket || !channel || !ref || !/^[\w-]{1,64}$/.test(ref)) return false;
16005
+ const opening = net4.connect(channel.path);
16006
+ opening.setEncoding("utf8");
16007
+ const opened = await new Promise((resolve11) => {
16008
+ const timer = setTimeout(() => resolve11(false), CONNECT_MS);
16009
+ opening.once("connect", () => {
16010
+ clearTimeout(timer);
16011
+ resolve11(true);
16012
+ });
16013
+ opening.once("error", () => {
16014
+ clearTimeout(timer);
16015
+ resolve11(false);
16016
+ });
16017
+ });
16018
+ if (!opened || opening.destroyed) {
16019
+ opening.destroy();
16020
+ return false;
16021
+ }
16022
+ socket = opening;
16023
+ control = handlers;
16024
+ shown = /* @__PURE__ */ new Set();
16025
+ opening.unref();
16026
+ opening.on(
16027
+ "data",
16028
+ lineReader(
16029
+ (line) => onMessage(parseHubToRun(line)),
16030
+ () => opening.destroy()
16031
+ )
16032
+ );
16033
+ opening.on("error", () => opening.destroy());
16034
+ opening.on("close", () => {
16035
+ if (socket === opening) socket = null;
16036
+ if (!held || held.bar) return;
16037
+ console.log("[piwi] The connection to the editor closed: the test goes on.");
16038
+ held.answer("resume");
16039
+ });
16040
+ tellHub({
16041
+ type: "run-attach",
16042
+ token: channel.token,
16043
+ ref,
16044
+ cwd: process.cwd(),
16045
+ workerIndex: worker.workerIndex,
16046
+ parallelIndex: worker.parallelIndex,
16047
+ pid: process.pid,
16048
+ playwright: worker.config.version,
16049
+ reporter: getReporterVersion(),
16050
+ takes: [...RUN_TAKES],
16051
+ holds: [...RUN_HOLDS]
16052
+ });
16053
+ return true;
16054
+ }
16055
+ function editorShows(what) {
16056
+ return runChannelOpen() && shown.has(what);
16057
+ }
16058
+ var HIDDEN_ENV = [RUN_CHANNEL_ENV, PIWI_LOCAL_DEBUG_ENV.editorSend];
16059
+ var hiddenEditorSend;
16060
+ function hideRunChannel(env = process.env) {
16061
+ const hidden = HIDDEN_ENV.flatMap((name) => {
16062
+ const value = env[name];
16063
+ return value === void 0 ? [] : [{ name, value }];
16064
+ });
16065
+ for (const { name } of hidden) delete env[name];
16066
+ hiddenEditorSend = hidden.find(({ name }) => name === PIWI_LOCAL_DEBUG_ENV.editorSend)?.value;
16067
+ return () => {
16068
+ for (const { name, value } of hidden) env[name] = value;
16069
+ hiddenEditorSend = void 0;
16070
+ };
16071
+ }
16072
+ function editorSendAddress(env = process.env) {
16073
+ return env[PIWI_LOCAL_DEBUG_ENV.editorSend] ?? hiddenEditorSend;
16074
+ }
16075
+ function closeRunChannel() {
16076
+ socket?.end();
16077
+ socket = null;
16078
+ control = null;
16079
+ }
16080
+ function runChannelOpen() {
16081
+ return !!socket && !socket.destroyed;
16082
+ }
16083
+ function tellHub(message) {
16084
+ if (socket && !socket.destroyed) socket.write(`${JSON.stringify(message)}
16085
+ `);
16086
+ }
16087
+ function holdStop(stop, onAnswer) {
16088
+ const id = ++stops;
16089
+ let answered = false;
16090
+ const holding = {
16091
+ stop: id,
16092
+ page: stop.page,
16093
+ bar: stop.bar,
16094
+ answer: (choice) => {
16095
+ if (answered) return;
16096
+ answered = true;
16097
+ onAnswer(choice);
16098
+ },
16099
+ intent: (what) => answered ? { ok: false, message: WENT_ON } : stop.intent?.(what) ?? {
16100
+ ok: false,
16101
+ message: `${RUN_INTENT_NAMES[what]} needs the pause bar in the test's window, and this stop shows none: run the test headed.`
16102
+ }
16103
+ };
16104
+ held = holding;
16105
+ tellHub({
16106
+ type: "stopped",
16107
+ stop: id,
16108
+ reason: stop.reason,
16109
+ title: stop.title,
16110
+ place: stop.place,
16111
+ locator: stop.locator,
16112
+ bar: stop.bar,
16113
+ ...stop.error ? { error: stop.error } : {}
16114
+ });
16115
+ if (!runChannelOpen() && !stop.bar) holding.answer("resume");
16116
+ return {
16117
+ stop: id,
16118
+ release: async () => {
16119
+ answered = true;
16120
+ if (held === holding) held = null;
16121
+ await counting;
16122
+ if (outline?.stop === id) await removeOutline();
16123
+ tellHub({ type: "running", stop: id });
16124
+ }
16125
+ };
16126
+ }
16127
+ async function removeOutline() {
16128
+ const shown2 = outline;
16129
+ outline = null;
16130
+ await shown2?.remove().catch(() => void 0);
16131
+ }
16132
+ async function countLocator(holding, source) {
16133
+ await removeOutline();
16134
+ if (source === null) return { ok: true, message: "The outline is gone.", count: null };
16135
+ const page = holding.page;
16136
+ if (!page || page.isClosed()) return { ok: false, message: "This stop has no page to count on.", count: null };
16137
+ const locator = locatorOn(page, source);
16138
+ if (!locator) return { ok: false, message: `${source} is not a locator Piwi can build.`, count: null };
16139
+ try {
16140
+ const count = await internalCall(page, () => locator.count());
16141
+ if (holding.bar && count > 0 && held === holding) {
16142
+ const shown2 = await internalCall(page, () => locator.highlight({ style: OUTLINE_STYLE }));
16143
+ const dispose = shown2?.dispose;
16144
+ outline = {
16145
+ stop: holding.stop,
16146
+ remove: () => internalCall(page, () => typeof dispose === "function" ? dispose.call(shown2) : page.hideHighlight())
16147
+ };
16148
+ }
16149
+ return {
16150
+ ok: true,
16151
+ message: count === 0 ? "No element matches." : count === 1 ? "1 element matches." : `${count} elements match.`,
16152
+ count
16153
+ };
16154
+ } catch (error) {
16155
+ return { ok: false, message: `The locator could not be counted: ${errorMessage(error)}`, count: null };
16156
+ }
16157
+ }
16158
+ function onMessage(message) {
16159
+ if (!message) return;
16160
+ switch (message.type) {
16161
+ case "attached":
16162
+ shown = new Set(message.holds);
16163
+ return;
16164
+ case "breakpoints":
16165
+ control?.breakpoints(message.places);
16166
+ return;
16167
+ case "control":
16168
+ if (message.action === "pause") {
16169
+ if (!held) control?.pause();
16170
+ } else if (held?.stop === message.stop) held.answer(message.action);
16171
+ return;
16172
+ case "run-to":
16173
+ if (held?.stop !== message.stop) return;
16174
+ control?.runTo(message.place);
16175
+ held.answer("resume");
16176
+ return;
16177
+ case "request": {
16178
+ const { request: request3 } = message;
16179
+ const at = message.what !== "image" && held?.stop === message.stop ? held : null;
16180
+ switch (message.what) {
16181
+ case "image":
16182
+ void imageOf(held?.page ?? null).then((value) => tellHub({ type: "answer", request: request3, what: "image", value }));
16183
+ return;
16184
+ case "requests": {
16185
+ const requests = at ? keptRequests() : [];
16186
+ 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.";
16187
+ tellHub({ type: "answer", request: request3, what: "requests", value: { ok: !!at, message: said, requests } });
16188
+ return;
16189
+ }
16190
+ case "mock": {
16191
+ void (at ? keptResponse(message.id) : Promise.resolve({ refusal: WENT_ON })).then((found) => {
16192
+ const value = "refusal" in found ? { ok: false, message: found.refusal, response: null } : { ok: true, message: `${found.method} ${found.url}`, response: found };
16193
+ tellHub({ type: "answer", request: request3, what: "mock", value });
16194
+ });
16195
+ return;
16196
+ }
16197
+ case "locator": {
16198
+ const counted = at ? counting.then(
16199
+ () => held === at ? countLocator(at, message.locator) : { ok: false, message: WENT_ON, count: null }
16200
+ ) : Promise.resolve({ ok: false, message: WENT_ON, count: null });
16201
+ counting = counted.catch(() => void 0);
16202
+ void counted.then((value) => tellHub({ type: "answer", request: request3, what: "locator", value }));
16203
+ return;
16204
+ }
16205
+ default: {
16206
+ const value = at ? at.intent(message.what) : { ok: false, message: WENT_ON };
16207
+ tellHub({ type: "answer", request: request3, what: message.what, value });
16208
+ return;
16209
+ }
16210
+ }
16211
+ }
16212
+ case "unknown":
16213
+ tellHub({
16214
+ type: "notice",
16215
+ message: `@piwitests/reporter ${getReporterVersion()} does not take the editor's ${message.what}: update it.`
16216
+ });
16217
+ }
16218
+ }
16219
+ async function imageOf(page) {
16220
+ if (!page || page.isClosed()) return null;
16221
+ try {
16222
+ const shot = await internalCall(
16223
+ page,
16224
+ () => page.screenshot({ type: "jpeg", quality: 60, style: BAR_HIDDEN, timeout: 5e3 })
16225
+ );
16226
+ return { mime: "image/jpeg", data: shot.toString("base64") };
16227
+ } catch {
16228
+ return null;
16229
+ }
16230
+ }
16231
+
15079
16232
  // src/internal/capture/css-coverage.ts
15080
- var crypto7 = __toESM(require("crypto"));
16233
+ var crypto8 = __toESM(require("crypto"));
15081
16234
  var fs27 = __toESM(require("fs"));
15082
16235
 
15083
16236
  // ../core/src/css-rules.ts
@@ -15292,7 +16445,7 @@ async function resolveSheet(sheet, text, dirs, fetchMap) {
15292
16445
  return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
15293
16446
  }
15294
16447
  function cachedResolution(sheet, text, dirs, fetchMap) {
15295
- const version = crypto7.createHash("sha1").update(text).digest("hex");
16448
+ const version = crypto8.createHash("sha1").update(text).digest("hex");
15296
16449
  const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
15297
16450
  " "
15298
16451
  );
@@ -15635,10 +16788,15 @@ function createSink() {
15635
16788
  locatorCalls: /* @__PURE__ */ new Map(),
15636
16789
  stops: [],
15637
16790
  shownStops: /* @__PURE__ */ new Map(),
16791
+ failureHeld: false,
16792
+ frozenAt: null,
16793
+ stopEnds: 0,
16794
+ ending: false,
15638
16795
  released: [],
15639
16796
  releasedRequests: /* @__PURE__ */ new Set(),
15640
16797
  onReleasedRequestsEnded: null,
15641
16798
  releasedQuietSince: 0,
16799
+ releasedUntil: null,
15642
16800
  locatorPages: new LocatorPageLog(),
15643
16801
  codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
15644
16802
  serverCoverage: /* @__PURE__ */ new Map(),
@@ -15742,6 +16900,25 @@ async function drainPendingProbes(capMs) {
15742
16900
  ]);
15743
16901
  clearTimeout(cap);
15744
16902
  }
16903
+ async function settleBeforeClose(closing) {
16904
+ const drained = drainPendingProbes(1e3);
16905
+ const sink = currentSink;
16906
+ if (sink) {
16907
+ await endShownStops(sink, "closed", closing);
16908
+ await awaitReleasedCalls(sink);
16909
+ await stashPageState(sink, closing);
16910
+ }
16911
+ await drained;
16912
+ if (sink) {
16913
+ await maybeOpenPicker(sink, closing);
16914
+ await stopSinkCodeReach(sink, closing);
16915
+ }
16916
+ }
16917
+ async function settleContextClose(context) {
16918
+ CLOSING_CONTEXTS.add(context);
16919
+ await settleBeforeClose({ context });
16920
+ if (contextDebugger(context)?.pausedDetails()) await steerDebugger(context, "resume");
16921
+ }
15745
16922
  function isPageClosed(page) {
15746
16923
  try {
15747
16924
  return typeof page.isClosed === "function" ? page.isClosed() : false;
@@ -15864,7 +17041,7 @@ function buildPageState(raw, cookies, interaction) {
15864
17041
  }
15865
17042
  async function readPageState(page) {
15866
17043
  try {
15867
- const raw = await page.evaluate(() => {
17044
+ const readRaw = page.evaluate(() => {
15868
17045
  const listStorage = (s) => {
15869
17046
  const out = [];
15870
17047
  try {
@@ -15890,14 +17067,16 @@ async function readPageState(page) {
15890
17067
  sessionStorage: listStorage(globalThis.sessionStorage)
15891
17068
  };
15892
17069
  });
17070
+ const readCookies = (async () => {
17071
+ try {
17072
+ return await pageContext(page)?.cookies() ?? null;
17073
+ } catch {
17074
+ return null;
17075
+ }
17076
+ })();
17077
+ const readInteraction = page.evaluate(readPageInteraction).catch(() => null);
17078
+ const [raw, cookies, interaction] = await Promise.all([readRaw, readCookies, readInteraction]);
15893
17079
  if (!raw) return null;
15894
- let cookies = null;
15895
- try {
15896
- cookies = await pageContext(page)?.cookies() ?? null;
15897
- } catch {
15898
- cookies = null;
15899
- }
15900
- const interaction = await page.evaluate(readPageInteraction).catch(() => null);
15901
17080
  return buildPageState(raw, cookies, interaction);
15902
17081
  } catch {
15903
17082
  return null;
@@ -15961,7 +17140,7 @@ async function recordPageWindow(sink, page) {
15961
17140
  }
15962
17141
  async function stashPageState(sink, closing) {
15963
17142
  const page = sink.lastActivePage;
15964
- if (!page || isPageClosed(page)) return;
17143
+ if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
15965
17144
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
15966
17145
  if (!belongsToClosing) return;
15967
17146
  await internalCall(page, () => readPageBeforeClose(sink, page));
@@ -15973,13 +17152,12 @@ async function readBlankPage(page) {
15973
17152
  return aria != null && aria.trim() === "";
15974
17153
  }
15975
17154
  async function readPageBeforeClose(sink, page) {
15976
- const vitals = await readWebVitals(page);
17155
+ const readBlank = sink.probeItem?.level === "server" ? readBlankPage(page).catch(() => null) : null;
17156
+ const readState = process.env.PIWI_CAPTURE_PAGE_STATE !== "false" ? Promise.resolve(readBlank).then(() => readPageState(page)) : null;
17157
+ const [vitals, blankPage, pageState] = await Promise.all([readWebVitals(page), readBlank, readState]);
15977
17158
  if (vitals) sink.stashedWebVitals = vitals;
15978
- if (sink.probeItem?.level === "server") sink.probeBlankPage = await readBlankPage(page);
15979
- if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
15980
- const pageState = await readPageState(page);
15981
- if (pageState) sink.stashedPageState = pageState;
15982
- }
17159
+ if (blankPage !== null) sink.probeBlankPage = blankPage;
17160
+ if (pageState) sink.stashedPageState = pageState;
15983
17161
  const status = sink.testInfo?.status;
15984
17162
  const sampleAria = async () => {
15985
17163
  const root = page.locator(":root");
@@ -16173,16 +17351,35 @@ function locatorSource(target, method, args) {
16173
17351
  return renderFailing({ method, args });
16174
17352
  }
16175
17353
  var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
17354
+ var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
17355
+ var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
16176
17356
  var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
16177
- async function armBreakpoints(contexts) {
17357
+ function stopPageOf(context, call, paused) {
17358
+ const named = [call?.page, paused].find((page) => !!page && pageContext(page) === context);
17359
+ if (named) return { page: named };
17360
+ const open = openPages(context);
17361
+ return open.length === 1 ? { page: open[0] } : { among: open };
17362
+ }
17363
+ function openPages(context) {
17364
+ return context.pages().filter((page) => !isPageClosed(page));
17365
+ }
17366
+ function stopPages(of) {
17367
+ return "page" in of ? [of.page] : of.among;
17368
+ }
17369
+ function stopCanShow(context, of) {
17370
+ if (CLOSING_CONTEXTS.has(context)) return false;
17371
+ return !stopPages(of).some((page) => isPageClosed(page) || CLOSING_PAGES.has(page));
17372
+ }
17373
+ async function armStops(contexts) {
16178
17374
  const state = currentSink?.pause;
16179
- if (!state || state.finished || state.set.size === 0) return;
17375
+ if (!state || !wantsStops(state)) return;
16180
17376
  await Promise.all(
16181
17377
  [...contexts].map(
16182
17378
  (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
16183
17379
  )
16184
17380
  );
16185
17381
  }
17382
+ var stopTurn = Promise.resolve();
16186
17383
  function onDebuggerStop(context) {
16187
17384
  const details = contextDebugger(context)?.pausedDetails();
16188
17385
  if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
@@ -16193,13 +17390,38 @@ function onDebuggerStop(context) {
16193
17390
  try {
16194
17391
  if (!sink || !state) return;
16195
17392
  const callSite = stopSite(sink, details.location);
17393
+ const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
16196
17394
  const pagePause = PAGE_PAUSES.get(context);
17395
+ const asked = askedOnce(state);
16197
17396
  const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
16198
17397
  if ("go" in decided) {
16199
17398
  how = decided.go;
16200
17399
  return;
16201
17400
  }
16202
- how = goOn(state, await showStop(sink, context, details.title, callSite ?? pagePause, decided.show));
17401
+ if (pagePause) stopShown(state);
17402
+ if (sink.ending || CLOSING_CONTEXTS.has(context)) {
17403
+ askAgain(state, asked);
17404
+ how = goOn(state, sink.ending ? endedWaiting(sink, call) : void 0);
17405
+ return;
17406
+ }
17407
+ const of = stopPageOf(context, call, pagePause?.page);
17408
+ const ends = sink.stopEnds;
17409
+ const shown2 = stopTurn.then(async () => {
17410
+ const ended = currentSink !== sink || sink.ending || sink.stopEnds !== ends;
17411
+ const gone = decided.show === "breakpoint" && !atBreakpoint(state, callSite);
17412
+ if (!ended && !gone && stopCanShow(context, of)) {
17413
+ const location = callSite ?? pagePause.location;
17414
+ const answer = await showStop(sink, context, details.title, location, decided.show, call, of);
17415
+ if (answer !== void 0) return answer;
17416
+ }
17417
+ askAgain(state, asked);
17418
+ if (ended) return endedWaiting(sink, call);
17419
+ const closing = stopPages(of).flatMap((page) => CLOSING_PAGES.get(page) ?? []);
17420
+ await settledWithin(closing, END_STOP_MS);
17421
+ return void 0;
17422
+ });
17423
+ stopTurn = shown2.catch(() => void 0);
17424
+ how = goOn(state, await shown2);
16203
17425
  } catch {
16204
17426
  if (state) how = goOn(state);
16205
17427
  } finally {
@@ -16207,22 +17429,30 @@ function onDebuggerStop(context) {
16207
17429
  }
16208
17430
  })();
16209
17431
  }
17432
+ function endedWaiting(sink, call) {
17433
+ releaseCall(sink, call?.settled ?? Promise.resolve());
17434
+ return "finish";
17435
+ }
16210
17436
  function stopSite(sink, location) {
16211
17437
  if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
16212
17438
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
16213
17439
  }
16214
- function stopPage(sink, context, call) {
16215
- for (const page of [call?.page, sink.lastActivePage]) {
16216
- if (page && !isPageClosed(page) && pageContext(page) === context) return page;
16217
- }
16218
- const open = context.pages().filter((page) => !isPageClosed(page));
17440
+ function stopBarPage(sink, context, of) {
17441
+ if ("page" in of) return of.page;
17442
+ const last = sink.lastActivePage;
17443
+ if (last && !isPageClosed(last) && pageContext(last) === context) return last;
17444
+ const open = openPages(context);
16219
17445
  return open[open.length - 1] ?? null;
16220
17446
  }
16221
- async function showStop(sink, context, title, location, reason) {
17447
+ async function showStop(sink, context, title, location, reason, call, of, failure = null) {
16222
17448
  const testInfo = sink.testInfo;
16223
- const call = sink.locatorCalls.get(callSiteLine(location));
16224
- const page = stopPage(sink, context, call);
16225
- if (!testInfo || !page) return void 0;
17449
+ const page = stopBarPage(sink, context, of);
17450
+ const guessed = !("page" in of);
17451
+ if (!testInfo) return void 0;
17452
+ const bar = !!page && testInfo.project?.use?.headless === false;
17453
+ const place = runChannelOpen() ? callSitePlace(location) : null;
17454
+ if (!bar && !place) return void 0;
17455
+ const failing = failure?.failed ?? null;
16226
17456
  const onPick = async (picked) => {
16227
17457
  if (call) {
16228
17458
  call.picked = true;
@@ -16233,16 +17463,57 @@ async function showStop(sink, context, title, location, reason) {
16233
17463
  ...picked
16234
17464
  });
16235
17465
  sink.probedLocations.add(call.location);
17466
+ } else if (failing) {
17467
+ const pick = {
17468
+ failing: { method: failing.method, args: failing.args, rendered: renderFailing(failing), location },
17469
+ ...picked
17470
+ };
17471
+ sink.userPick = pick;
17472
+ applyPickToSnapshots(sink.capturedLocators, pick);
16236
17473
  }
16237
17474
  console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
16238
- await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
17475
+ await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
16239
17476
  };
17477
+ const handoffReason = reason === "page-pause" ? "pause" : "breakpoint";
17478
+ const onRecord = bar && page ? recorderAt(page, testInfo, location, handoffReason) : void 0;
17479
+ const intents = bar && page ? new BarIntents() : null;
16240
17480
  const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
16241
17481
  sink.stops.push(record);
16242
17482
  const end = new AbortController();
16243
- const shown = withTestTimeoutHeld(
16244
- testInfo,
16245
- () => internalCall(
17483
+ const over = new AbortController();
17484
+ end.signal.addEventListener("abort", () => over.abort(), { once: true });
17485
+ let fromEditor;
17486
+ const held2 = place ? holdStop(
17487
+ {
17488
+ reason,
17489
+ title,
17490
+ place,
17491
+ locator: call ? locatorSource(call.target, call.origin.method, call.origin.args) : failing ? renderFailing(failing) : null,
17492
+ bar,
17493
+ page: guessed ? null : page,
17494
+ ...failure?.error ? { error: failure.error } : {},
17495
+ intent: intents ? (what) => intentAt(intents, what, {
17496
+ record: !!onRecord,
17497
+ guessed,
17498
+ // A failure's stop is held in the teardown it ends with.
17499
+ holds: () => !over.signal.aborted && (!!failure || !sink.ending && stopCanShow(context, of))
17500
+ }) : void 0
17501
+ },
17502
+ (answer2) => {
17503
+ fromEditor = answer2;
17504
+ over.abort();
17505
+ }
17506
+ ) : null;
17507
+ 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;
17508
+ const shown2 = withTestTimeoutHeld(testInfo, async () => {
17509
+ if (!bar || !page) {
17510
+ console.log(
17511
+ 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.`
17512
+ );
17513
+ await abortion(over.signal);
17514
+ return "resume";
17515
+ }
17516
+ return internalCall(
16246
17517
  page,
16247
17518
  () => pauseHere(
16248
17519
  page,
@@ -16250,45 +17521,221 @@ async function showStop(sink, context, title, location, reason) {
16250
17521
  location,
16251
17522
  call: title,
16252
17523
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
16253
- target: call?.page === page ? call.target : null
17524
+ target: call?.page === page ? call.target : null,
17525
+ ...failure ? { failure: true } : {}
16254
17526
  },
16255
17527
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
16256
17528
  onPick,
16257
- recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
16258
- end.signal
17529
+ onRecord,
17530
+ over.signal,
17531
+ intents ?? void 0,
17532
+ onIntent
16259
17533
  )
16260
- )
16261
- ).finally(() => {
17534
+ );
17535
+ }).finally(async () => {
16262
17536
  if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
16263
17537
  else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
16264
17538
  sink.shownStops.delete(end);
17539
+ await held2?.release();
16265
17540
  });
16266
17541
  const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
16267
- sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
16268
- const answer = await shown;
16269
- if (!end.signal.aborted) return answer;
16270
- return end.signal.reason === "closed" ? "resume" : "finish";
17542
+ const stop = { page, guessed, context, record, done: shown2.catch(() => void 0), heldAt, after: null };
17543
+ if (!failure) sink.shownStops.set(end, stop);
17544
+ const answer = await shown2;
17545
+ if (!end.signal.aborted) return fromEditor ?? answer;
17546
+ if (end.signal.reason === "ended") return "finish";
17547
+ await settledWithin(stop.after ? [stop.after] : [], END_STOP_MS);
17548
+ return "resume";
17549
+ }
17550
+ function intentAt(intents, what, stop) {
17551
+ if (!stop.holds()) return { ok: false, message: "The test went on." };
17552
+ if (stop.guessed) {
17553
+ return {
17554
+ ok: false,
17555
+ 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.`
17556
+ };
17557
+ }
17558
+ if ((what === "record" || what === "assert") && !stop.record) {
17559
+ return {
17560
+ ok: false,
17561
+ message: `${RUN_INTENT_NAMES[what]} needs the editor that started the run: this run names no record channel.`
17562
+ };
17563
+ }
17564
+ if (!intents.offer(what)) {
17565
+ return {
17566
+ ok: false,
17567
+ message: `${RUN_INTENT_NAMES[what]} waits: a pick, a check or a recording runs in the test's window.`
17568
+ };
17569
+ }
17570
+ const started = {
17571
+ pick: "Pick an element in the test's window: the locator goes to the editor.",
17572
+ record: "Recording in the test's window: the editor writes the steps above the stopped line.",
17573
+ assert: "Check an element in the test's window: the editor writes the check above the stopped line.",
17574
+ "assert-region": "Pick the region in the test's window: the editor writes its ARIA snapshot above the stopped line."
17575
+ };
17576
+ return { ok: true, message: started[what] };
17577
+ }
17578
+ async function checkAt(page, testInfo, location, reason, stop, ended) {
17579
+ const ref = process.env[PIWI_ORIGIN_ENV.ref] ?? "";
17580
+ console.log(`[piwi] Checking an element at ${callSiteLine(location)}: the editor writes the check.`);
17581
+ const why = await recordFromHere({
17582
+ page,
17583
+ testInfo,
17584
+ location,
17585
+ reason,
17586
+ mode: "check",
17587
+ stop: { ref, worker: testInfo.workerIndex, stop },
17588
+ ended
17589
+ });
17590
+ if (ended?.aborted) console.log("[piwi] The check ended with the stop.");
17591
+ else console.log(why ? `[piwi] Nothing was checked: ${why}` : "[piwi] The check ended: the test is paused again.");
17592
+ }
17593
+ async function regionAt(page, location, stop, ended) {
17594
+ const picked = await pickLocator(page, { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG }, null, ended).catch(
17595
+ () => null
17596
+ );
17597
+ if (!picked || ended?.aborted) return;
17598
+ const source = picked.picked.locator;
17599
+ const say = (message) => {
17600
+ console.log(`[piwi] ${message}`);
17601
+ tellHub({ type: "region", stop, refused: message });
17602
+ };
17603
+ const region = locatorOn(page, source);
17604
+ if (!region) return say(`The region's locator, ${source}, is not one Piwi can read.`);
17605
+ const count = await region.count().catch(() => 0);
17606
+ if (count !== 1) return say(`${source} finds ${count} elements: pick a region it finds alone.`);
17607
+ const read = await region.ariaSnapshot({ timeout: 5e3 }).catch(() => null);
17608
+ if (!read?.trim()) return say(`The ARIA snapshot of ${source} could not be read.`);
17609
+ const secrets = await regionSecrets(region).catch(() => null);
17610
+ if (secrets === null) return say(`The fields of ${source} that hold a secret could not be read.`);
17611
+ const snapshot = regionSnapshotWithoutSecrets(read, secrets);
17612
+ if (snapshot.length > MAX_REGION_SNAPSHOT) {
17613
+ return say(
17614
+ `The ARIA snapshot of ${source} is longer than ${MAX_REGION_SNAPSHOT} characters: pick a smaller region.`
17615
+ );
17616
+ }
17617
+ console.log(`[piwi] Region picked at ${callSiteLine(location)}: ${source}, its ARIA snapshot sent to the editor.`);
17618
+ tellHub({ type: "region", stop, locator: source, snapshot });
17619
+ }
17620
+ function regionSecrets(region) {
17621
+ return region.locator(":scope:is(input, textarea), input, textarea").evaluateAll(
17622
+ (elements, secret) => {
17623
+ return elements.flatMap((field) => {
17624
+ const tokens = (field.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
17625
+ const password = field.localName === "input" && String(field.type).toLowerCase() === "password";
17626
+ return password || tokens.some((token) => secret.includes(token)) ? [String(field.value ?? "")] : [];
17627
+ });
17628
+ },
17629
+ [...SECRET_AUTOCOMPLETE]
17630
+ );
17631
+ }
17632
+ async function holdFailure(sink, fallback) {
17633
+ const testInfo = sink.testInfo;
17634
+ if (sink.failureHeld || !sink.pause || !testInfo || !editorShows("failure")) return;
17635
+ const { status, expectedStatus } = testInfo;
17636
+ if (status !== "failed" && status !== "timedOut" || status === expectedStatus) return;
17637
+ const page = [sink.lastActivePage, fallback].find((open) => !!open && !isPageClosed(open));
17638
+ const context = page ? pageContext(page) : null;
17639
+ if (!page || !context) return;
17640
+ sink.failureHeld = true;
17641
+ const failed = sink.failedLocators[sink.failedLocators.length - 1] ?? deriveFailedLocator(testInfo);
17642
+ const errors = testInfo.errors.length ? testInfo.errors : testInfo.error ? [testInfo.error] : [];
17643
+ const declared = (place2) => place2.file === testInfo.file && place2.line === testInfo.line;
17644
+ const placed = errors.flatMap((error2) => {
17645
+ const place2 = errorPlace(error2);
17646
+ return place2 && !declared(place2) ? [{ error: error2, place: place2 }] : [];
17647
+ })[0];
17648
+ const error = placed?.error ?? errors[0];
17649
+ const place = placed?.place ?? (failed?.location ? null : stepInProgress(testInfo));
17650
+ const location = (place ? stopCallSite(place) : null) ?? failed?.location ?? stopCallSite({ file: testInfo.file, line: testInfo.line, column: testInfo.column });
17651
+ if (!location) return;
17652
+ const said = stripAnsi(error?.message ?? "").split("\n").find((line) => line.trim()) ?? status;
17653
+ if (page === sink.lastActivePage)
17654
+ await internalCall(page, () => readPageBeforeClose(sink, page)).catch(() => void 0);
17655
+ sink.frozenAt = Date.now();
17656
+ await stopSinkCodeReach(sink, { context });
17657
+ const title = hideSecretsInText(`Failed: ${said.trim().slice(0, 300)}`).slice(0, 1e3);
17658
+ const errorText = failureError(error?.message ?? error?.value);
17659
+ const named = errorText === null ? null : hideSecretsInText(errorText).slice(0, MAX_STOP_ERROR);
17660
+ await showStop(sink, context, title, location, "failure", void 0, { page }, { failed, error: named });
17661
+ }
17662
+ var FAILURE_ERROR_LINES = 4;
17663
+ function failureError(message) {
17664
+ const lines = [];
17665
+ for (const line of stripAnsi(message ?? "").trimStart().replace(/^Error: /, "").split("\n")) {
17666
+ if (!line.trim() || /^\s*Call log:/.test(line)) break;
17667
+ lines.push(line.trimEnd());
17668
+ if (lines.length === FAILURE_ERROR_LINES) break;
17669
+ }
17670
+ const text = lines.join("\n").slice(0, MAX_STOP_ERROR);
17671
+ return text.trim() ? text : null;
17672
+ }
17673
+ function errorPlace(error) {
17674
+ const given = error?.location;
17675
+ if (typeof given?.file === "string" && typeof given.line === "number") {
17676
+ return { file: given.file, line: given.line, column: given.column ?? 0 };
17677
+ }
17678
+ const stack = error?.stack;
17679
+ for (const line of typeof stack === "string" ? stack.split("\n") : []) {
17680
+ const frame = /^\s*at\s+(?:.*?\s+\()?(?:file:\/\/)?(\/[^():]+|[A-Za-z]:[\\/][^():]+):(\d+):(\d+)\)?$/.exec(line);
17681
+ if (!frame) continue;
17682
+ const file = frame[1];
17683
+ if (/[\\/]node_modules[\\/]/.test(file) || path30.dirname(path30.resolve(file)) === __dirname) continue;
17684
+ return { file, line: Number(frame[2]), column: Number(frame[3]) };
17685
+ }
17686
+ return null;
17687
+ }
17688
+ function stepInProgress(testInfo) {
17689
+ let found = null;
17690
+ const visit = (steps) => {
17691
+ if (!Array.isArray(steps)) return;
17692
+ for (const step of steps) {
17693
+ if (step.endWallTime) continue;
17694
+ const at = step.stack?.[0];
17695
+ const called = step.category === "pw:api" || step.category === "expect" || step.category === "test.step";
17696
+ if (called && typeof at?.file === "string" && typeof at.line === "number") {
17697
+ found = { file: at.file, line: at.line, column: typeof at.column === "number" ? at.column : 0 };
17698
+ }
17699
+ visit(step.steps);
17700
+ }
17701
+ };
17702
+ visit(testInfo._steps);
17703
+ return found;
17704
+ }
17705
+ function abortion(signal) {
17706
+ return new Promise((resolve11) => {
17707
+ if (signal.aborted) resolve11();
17708
+ else signal.addEventListener("abort", () => resolve11(), { once: true });
17709
+ });
16271
17710
  }
16272
17711
  var END_STOP_MS = 2e3;
16273
17712
  var RELEASED_CALL_MS = 2e3;
16274
17713
  var RELEASED_QUIET_MS = 200;
16275
17714
  async function endShownStops(sink, why, closing) {
16276
17715
  const ending = [];
17716
+ if (why === "ended") sink.stopEnds++;
16277
17717
  let heldAt = Infinity;
16278
- for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
16279
- if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
17718
+ for (const [end, stop] of sink.shownStops) {
17719
+ if (closing && !closesStop(closing, stop)) continue;
16280
17720
  if (!end.signal.aborted) {
16281
- record.duration = Date.now() - record.startedAt;
17721
+ stop.record.duration = Date.now() - stop.record.startedAt;
17722
+ if (closing?.page) stop.after = CLOSING_PAGES.get(closing.page) ?? null;
16282
17723
  end.abort(why);
16283
- if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
17724
+ if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
16284
17725
  }
16285
- ending.push(done);
17726
+ ending.push(stop.done);
16286
17727
  }
16287
17728
  if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
16288
17729
  await settledWithin(ending, END_STOP_MS);
16289
17730
  }
17731
+ function closesStop(closing, stop) {
17732
+ if (closing.context && stop.context === closing.context) return true;
17733
+ if (!closing.page) return false;
17734
+ return stop.page === closing.page || stop.guessed && pageContext(closing.page) === stop.context;
17735
+ }
16290
17736
  function releaseCall(sink, settled) {
16291
17737
  sink.releasedQuietSince = Date.now();
17738
+ sink.releasedUntil = null;
16292
17739
  sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
16293
17740
  }
16294
17741
  function releasedRequestsEnded(sink) {
@@ -16297,8 +17744,8 @@ function releasedRequestsEnded(sink) {
16297
17744
  }
16298
17745
  async function awaitReleasedCalls(sink) {
16299
17746
  if (sink.released.length === 0) return;
16300
- const deadline = Date.now() + RELEASED_CALL_MS;
16301
- await settledWithin(sink.released, RELEASED_CALL_MS);
17747
+ const deadline = sink.releasedUntil ??= Date.now() + RELEASED_CALL_MS;
17748
+ await settledWithin(sink.released, Math.max(0, deadline - Date.now()));
16302
17749
  for (let now = Date.now(); now < deadline; now = Date.now()) {
16303
17750
  if (sink.releasedRequests.size > 0) {
16304
17751
  await settledWithin([releasedRequestsEnded(sink)], deadline - now);
@@ -16328,7 +17775,7 @@ function recorderAt(page, testInfo, location, reason) {
16328
17775
  return async (ended) => {
16329
17776
  console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
16330
17777
  const why = await recordFromHere({ page, testInfo, location, reason, ended });
16331
- if (ended?.aborted) console.log("[piwi] The recording ended with the test.");
17778
+ if (ended?.aborted) console.log("[piwi] The recording ended with the stop.");
16332
17779
  else
16333
17780
  console.log(
16334
17781
  why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
@@ -16345,10 +17792,10 @@ async function pauseInPlace(page, playwrightPause, location) {
16345
17792
  if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
16346
17793
  return leaveToPlaywright(context, playwrightPause);
16347
17794
  }
16348
- sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
17795
+ sink.pause ??= pauseState(NO_BREAKPOINTS);
16349
17796
  watchStops(context, onDebuggerStop);
16350
17797
  await steerDebugger(context, "pause");
16351
- PAGE_PAUSES.set(context, location);
17798
+ PAGE_PAUSES.set(context, { location, page });
16352
17799
  try {
16353
17800
  await playwrightPause({ __testHookKeepTestTimeout: true });
16354
17801
  } finally {
@@ -16531,15 +17978,15 @@ function instrumentPage(page) {
16531
17978
  if (typeof page.close === "function") {
16532
17979
  const originalClose = page.close.bind(page);
16533
17980
  page.close = async (...args) => {
16534
- await drainPendingProbes(1e3);
16535
- const sink = currentSink;
16536
- if (sink) {
16537
- await endShownStops(sink, "closed", { page });
16538
- await stashPageState(sink, { page });
16539
- await maybeOpenPicker(sink, { page });
16540
- await stopSinkCodeReach(sink, { page });
17981
+ let closed = () => {
17982
+ };
17983
+ CLOSING_PAGES.set(page, new Promise((resolve11) => closed = resolve11));
17984
+ try {
17985
+ await settleBeforeClose({ page });
17986
+ return await originalClose(...args);
17987
+ } finally {
17988
+ closed();
16541
17989
  }
16542
- return originalClose(...args);
16543
17990
  };
16544
17991
  }
16545
17992
  const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
@@ -16584,7 +18031,7 @@ function instrumentPage(page) {
16584
18031
  });
16585
18032
  page.on("console", (msg) => {
16586
18033
  const sink = currentSink;
16587
- if (!sink) return;
18034
+ if (!sink || sink.frozenAt !== null) return;
16588
18035
  const type = msg.type();
16589
18036
  if (["warning", "error", "assert"].includes(type)) {
16590
18037
  const location = msg.location();
@@ -16600,7 +18047,7 @@ function instrumentPage(page) {
16600
18047
  try {
16601
18048
  page.on("dialogclosed", (dialog) => {
16602
18049
  const sink = currentSink;
16603
- if (!sink) return;
18050
+ if (!sink || sink.frozenAt !== null) return;
16604
18051
  try {
16605
18052
  sink.dialogs.push({
16606
18053
  type: typeof dialog.type === "function" ? dialog.type() : null,
@@ -16619,12 +18066,14 @@ function instrumentPage(page) {
16619
18066
  if (!sink) return;
16620
18067
  try {
16621
18068
  const request3 = response.request();
18069
+ if (HELD_REQUESTS.has(request3)) return;
16622
18070
  if (isTrackedRequest(request3.url(), request3.resourceType())) sink.openResponses.set(request3, response);
16623
18071
  } catch {
16624
18072
  }
16625
18073
  });
16626
18074
  page.on("request", (request3) => {
16627
18075
  const sink = currentSink;
18076
+ if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request3);
16628
18077
  if (!sink || sink.released.length === 0) return;
16629
18078
  sink.releasedRequests.add(request3);
16630
18079
  sink.releasedQuietSince = Date.now();
@@ -16635,16 +18084,18 @@ function instrumentPage(page) {
16635
18084
  sink.releasedQuietSince = Date.now();
16636
18085
  if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
16637
18086
  }
16638
- if (!sink || RECORDED_OPEN.has(request3)) return;
18087
+ if (!sink || RECORDED_OPEN.has(request3) || HELD_REQUESTS.has(request3)) return;
16639
18088
  sink.lastActivePage = page;
16640
18089
  const response = sink.openResponses.get(request3) ?? null;
16641
18090
  sink.openResponses.delete(request3);
16642
18091
  recordRequest(sink, request3, failed ? "failed" : "finished", response);
18092
+ if (!failed) void keepResponse(request3, response);
16643
18093
  };
16644
18094
  page.on("requestfinished", (request3) => onRequestEnd(request3, false));
16645
18095
  page.on("requestfailed", (request3) => onRequestEnd(request3, true));
16646
18096
  }
16647
18097
  var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
18098
+ var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
16648
18099
  function isTrackedRequest(url, resourceType) {
16649
18100
  if (url.startsWith("data:") || url.startsWith("blob:")) return false;
16650
18101
  return TRACKED_REQUEST_TYPES.includes(resourceType);
@@ -16706,6 +18157,7 @@ function readResponseHeaders(sink, entry, url, headers) {
16706
18157
  }
16707
18158
  }
16708
18159
  function recordApiCall(sink, call) {
18160
+ if (sink.frozenAt !== null) return;
16709
18161
  try {
16710
18162
  const entry = apiCallEntry(call, sink.baseURL);
16711
18163
  if (!entry) return;
@@ -16742,7 +18194,10 @@ function instrumentContext(context) {
16742
18194
  INSTRUMENTED_CONTEXTS.add(context);
16743
18195
  if (typeof context.once === "function") {
16744
18196
  OPEN_CONTEXTS.add(context);
16745
- context.once("close", () => OPEN_CONTEXTS.delete(context));
18197
+ context.once("close", () => {
18198
+ OPEN_CONTEXTS.delete(context);
18199
+ CLOSING_CONTEXTS.add(context);
18200
+ });
16746
18201
  }
16747
18202
  observeOwnApiRequests(context);
16748
18203
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
@@ -16760,14 +18215,7 @@ function instrumentContext(context) {
16760
18215
  if (typeof context.close === "function") {
16761
18216
  const originalClose = context.close.bind(context);
16762
18217
  context.close = async (...args) => {
16763
- await drainPendingProbes(1e3);
16764
- const sink = currentSink;
16765
- if (sink) {
16766
- await endShownStops(sink, "closed", { context });
16767
- await stashPageState(sink, { context });
16768
- await maybeOpenPicker(sink, { context });
16769
- await stopSinkCodeReach(sink, { context });
16770
- }
18218
+ await settleContextClose(context);
16771
18219
  return originalClose(...args);
16772
18220
  };
16773
18221
  }
@@ -16780,7 +18228,7 @@ function patchBrowser(browser) {
16780
18228
  browser.newPage = async (...args) => {
16781
18229
  const page = await originalNewPage(...args);
16782
18230
  instrumentPage(page);
16783
- await armBreakpoints([pageContext(page)]);
18231
+ await armStops([pageContext(page)]);
16784
18232
  await awaitFlakeConditions();
16785
18233
  await maybeStartCodeReach(page);
16786
18234
  return page;
@@ -16789,13 +18237,16 @@ function patchBrowser(browser) {
16789
18237
  browser.newContext = async (...args) => {
16790
18238
  const context = await originalNewContext(...args);
16791
18239
  instrumentContext(context);
16792
- await armBreakpoints([context]);
18240
+ await armStops([context]);
16793
18241
  return context;
16794
18242
  };
16795
18243
  if (typeof browser.close === "function") {
16796
18244
  const originalClose = browser.close.bind(browser);
16797
18245
  browser.close = async (...args) => {
16798
18246
  await drainPendingProbes(1e3);
18247
+ for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
18248
+ await settleContextClose(context);
18249
+ }
16799
18250
  return originalClose(...args);
16800
18251
  };
16801
18252
  }
@@ -16848,7 +18299,11 @@ async function flushSink(sink, testInfo) {
16848
18299
  clearTimeout(drainDeadline);
16849
18300
  const page = sink.lastActivePage;
16850
18301
  const pageReadable = page !== null && !isPageClosed(page);
16851
- if (pageReadable) await maybeOpenPicker(sink);
18302
+ if (pageReadable) {
18303
+ await holdFailure(sink);
18304
+ await maybeOpenPicker(sink);
18305
+ }
18306
+ const live = pageReadable && sink.frozenAt === null;
16852
18307
  if (sink.stops.length > 0) {
16853
18308
  await testInfo.attach(ATTACHMENT_NAMES.pauses, {
16854
18309
  contentType: "application/json",
@@ -16916,13 +18371,13 @@ async function flushSink(sink, testInfo) {
16916
18371
  }
16917
18372
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
16918
18373
  try {
16919
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
18374
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
16920
18375
  if (snapshot) {
16921
18376
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
16922
- contentType: "text/plain",
18377
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
16923
18378
  body: snapshot
16924
18379
  });
16925
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
18380
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
16926
18381
  if (snapshotJson) {
16927
18382
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
16928
18383
  contentType: "application/json",
@@ -16947,7 +18402,7 @@ async function flushSink(sink, testInfo) {
16947
18402
  }
16948
18403
  if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
16949
18404
  const option = sink.failureScreenshot;
16950
- const shot = (pageReadable ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
18405
+ const shot = (live ? await internalCall(page, () => readFailureScreenshot(page, option)) : null) ?? sink.stashedScreenshot;
16951
18406
  if (shot) {
16952
18407
  try {
16953
18408
  await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
@@ -16957,13 +18412,13 @@ async function flushSink(sink, testInfo) {
16957
18412
  }
16958
18413
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
16959
18414
  try {
16960
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
18415
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
16961
18416
  if (snapshot) {
16962
18417
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
16963
- contentType: "text/plain",
18418
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
16964
18419
  body: snapshot
16965
18420
  });
16966
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
18421
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
16967
18422
  if (snapshotJson) {
16968
18423
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
16969
18424
  contentType: "application/json",
@@ -17009,7 +18464,7 @@ async function flushSink(sink, testInfo) {
17009
18464
  body: Buffer.from(JSON.stringify(sink.networkRequests))
17010
18465
  });
17011
18466
  }
17012
- const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
18467
+ const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
17013
18468
  if (webVitals) {
17014
18469
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
17015
18470
  contentType: "application/json",
@@ -17017,7 +18472,7 @@ async function flushSink(sink, testInfo) {
17017
18472
  });
17018
18473
  }
17019
18474
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
17020
- const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
18475
+ const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
17021
18476
  if (pageState) {
17022
18477
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
17023
18478
  contentType: "application/json",
@@ -17026,7 +18481,7 @@ async function flushSink(sink, testInfo) {
17026
18481
  }
17027
18482
  }
17028
18483
  if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
17029
- if (pageReadable) await recordPageWindow(sink, page);
18484
+ if (live) await recordPageWindow(sink, page);
17030
18485
  const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
17031
18486
  if (windows.length > 0) {
17032
18487
  await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
@@ -17036,21 +18491,79 @@ async function flushSink(sink, testInfo) {
17036
18491
  }
17037
18492
  }
17038
18493
  }
18494
+ var pauseNextTest = false;
18495
+ var runControl = {
18496
+ pause() {
18497
+ const sink = currentSink;
18498
+ if (!sink) {
18499
+ pauseNextTest = true;
18500
+ return;
18501
+ }
18502
+ if (!sink.pause) {
18503
+ const why = sink.testInfo ? pauseSkipReason({ ...pauseGateFromTestInfo(sink.testInfo), channel: true }) : null;
18504
+ tellHub({ type: "notice", message: `This test does not stop${why ? `: ${why}` : ""}.` });
18505
+ return;
18506
+ }
18507
+ sink.pause.pause = true;
18508
+ void armStops(OPEN_CONTEXTS);
18509
+ },
18510
+ breakpoints(places) {
18511
+ const set = replacePauseSet(places);
18512
+ const state = currentSink?.pause;
18513
+ if (!state) return;
18514
+ state.set = set;
18515
+ void armStops(OPEN_CONTEXTS);
18516
+ },
18517
+ runTo(place) {
18518
+ const state = currentSink?.pause;
18519
+ if (!state) return;
18520
+ state.runTo = placeLine(place);
18521
+ void armStops(OPEN_CONTEXTS);
18522
+ }
18523
+ };
18524
+ function runFile(file) {
18525
+ return path30.relative(process.cwd(), file).split(path30.sep).join("/") || file;
18526
+ }
18527
+ function testBegin(testInfo, cannotPause) {
18528
+ const title = testInfo.titlePath.slice(1);
18529
+ return {
18530
+ type: "test-begin",
18531
+ testId: testInfo.testId,
18532
+ title: title.length ? title : [testInfo.title],
18533
+ file: runFile(testInfo.file),
18534
+ line: Math.max(1, testInfo.line),
18535
+ project: testInfo.project.name,
18536
+ retry: testInfo.retry,
18537
+ cannotPause
18538
+ };
18539
+ }
18540
+ function testEnd(sink, testInfo) {
18541
+ return {
18542
+ type: "test-end",
18543
+ testId: testInfo.testId,
18544
+ status: testInfo.status ?? "passed",
18545
+ pausedMs: sink.stops.reduce((sum, stop) => sum + stop.duration, 0)
18546
+ };
18547
+ }
17039
18548
  var piwiFixtures = {
17040
18549
  // Auto, worker-scoped: the worker's resource ledger, and the API request
17041
18550
  // contexts its tests create, handed out behind the call proxy. Set up before
17042
18551
  // any hook (worker fixtures come first), so a `beforeAll` context is in the
17043
18552
  // ledger and records the calls the tests make through it, and torn down after
17044
18553
  // the browser closed, so its census sees what only the worker's shutdown
17045
- // closed. Depends on `playwright` alone, which launches nothing.
18554
+ // closed. Depends on `playwright` alone, which launches nothing. The worker's
18555
+ // run connection to the editor (`PIWI_RUN_CHANNEL`) opens here, before the
18556
+ // first test, and closes as the worker ends.
17046
18557
  piwiResources: [
17047
18558
  async ({ playwright }, use, workerInfo) => {
17048
18559
  boxCaptureFrames();
17049
18560
  startResourceLedger(playwright, workerInfo.workerIndex);
17050
18561
  observeApiRequests(playwright, apiCallRecorder);
18562
+ await openRunChannel(workerInfo, runControl);
17051
18563
  try {
17052
18564
  await use();
17053
18565
  } finally {
18566
+ closeRunChannel();
17054
18567
  stopResourceLedger();
17055
18568
  }
17056
18569
  },
@@ -17070,10 +18583,12 @@ var piwiFixtures = {
17070
18583
  // never double-wraps a page the browser patch already instrumented.
17071
18584
  page: async ({ page }, use) => {
17072
18585
  instrumentPage(page);
17073
- await armBreakpoints([pageContext(page)]);
18586
+ await armStops([pageContext(page)]);
17074
18587
  await awaitFlakeConditions();
17075
18588
  await maybeStartCodeReach(page);
17076
18589
  await use(page);
18590
+ const sink = currentSink;
18591
+ if (sink) await holdFailure(sink, page);
17077
18592
  },
17078
18593
  // Auto, test-scoped: open a capture sink for the running test and flush it
17079
18594
  // (attach the collected data) at teardown. Runs for every test without being
@@ -17088,12 +18603,16 @@ var piwiFixtures = {
17088
18603
  sink.baseURL = baseURL ?? null;
17089
18604
  if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
17090
18605
  const pauseSet = currentPauseSet();
17091
- if (pauseSet.size) {
17092
- const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
17093
- if (reason) logPauseSkipOnce(reason);
17094
- else sink.pause = { set: pauseSet, step: false, finished: false };
18606
+ const channel = runChannelOpen();
18607
+ const cannotPause = pauseSkipReason({ ...pauseGateFromTestInfo(testInfo), channel });
18608
+ if (pauseSet.size || channel) {
18609
+ if (!cannotPause) sink.pause = pauseState(pauseSet, pauseNextTest);
18610
+ else if (pauseSet.size) logPauseSkipOnce(cannotPause);
17095
18611
  }
18612
+ pauseNextTest = false;
17096
18613
  if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
18614
+ if (channel) tellHub(testBegin(testInfo, cannotPause));
18615
+ if (channel && sink.pause) startKeeping();
17097
18616
  if (isFlakeMode()) {
17098
18617
  const plan = loadFlakePlan();
17099
18618
  const role = flakeRoleForTest(plan, {
@@ -17117,14 +18636,18 @@ var piwiFixtures = {
17117
18636
  });
17118
18637
  activeResourceLedger()?.testStarted(testInfo);
17119
18638
  currentSink = sink;
18639
+ const showRunChannel = hideRunChannel();
17120
18640
  try {
17121
- await armBreakpoints(OPEN_CONTEXTS);
18641
+ await armStops(OPEN_CONTEXTS);
17122
18642
  await startCodeReachOnOpenPages();
17123
18643
  await use();
17124
18644
  } finally {
18645
+ sink.ending = true;
17125
18646
  await endShownStops(sink, "ended");
17126
18647
  await awaitReleasedCalls(sink);
18648
+ showRunChannel();
17127
18649
  currentSink = null;
18650
+ if (sink.pause?.pause) pauseNextTest = true;
17128
18651
  if (sink.probeItem) {
17129
18652
  const applied = sink.probeInterception?.applied() ?? false;
17130
18653
  const level = sink.probeItem.level ?? "client";
@@ -17147,7 +18670,12 @@ var piwiFixtures = {
17147
18670
  });
17148
18671
  }
17149
18672
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
17150
- await flushSink(sink, testInfo);
18673
+ try {
18674
+ await flushSink(sink, testInfo);
18675
+ } finally {
18676
+ stopKeeping();
18677
+ if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
18678
+ }
17151
18679
  await recordResourceCensus(testInfo);
17152
18680
  }
17153
18681
  },
@@ -17328,7 +18856,7 @@ function withEntryLock(file, fn) {
17328
18856
  }
17329
18857
 
17330
18858
  // src/internal/ai/keys.ts
17331
- var crypto8 = __toESM(require("crypto"));
18859
+ var crypto9 = __toESM(require("crypto"));
17332
18860
  var path32 = __toESM(require("path"));
17333
18861
  var DEFAULT_AI_DIR = "__piwi__";
17334
18862
  function normalizeTemplate(template) {
@@ -17336,7 +18864,7 @@ function normalizeTemplate(template) {
17336
18864
  }
17337
18865
  function hashTemplate(normalized, ordinal = 0) {
17338
18866
  const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
17339
- return crypto8.createHash("sha256").update(input).digest("hex").slice(0, 8);
18867
+ return crypto9.createHash("sha256").update(input).digest("hex").slice(0, 8);
17340
18868
  }
17341
18869
  function slug(text, maxLength = 40) {
17342
18870
  const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");