@piwitests/reporter 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  }
@@ -203,6 +203,7 @@ var PIWI_ENV_KEYS = {
203
203
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
204
204
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
205
205
  coverageServer: "PIWI_COVERAGE_SERVER",
206
+ coverageCss: "PIWI_COVERAGE_CSS",
206
207
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
207
208
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
208
209
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -357,10 +358,14 @@ function resolveOptions(raw, configDir) {
357
358
  const coverage = { ...mergedRaw.coverage };
358
359
  const coverageBrowser = env[PIWI_ENV_KEYS.coverageBrowser];
359
360
  const coverageServer = env[PIWI_ENV_KEYS.coverageServer];
361
+ const coverageCss = env[PIWI_ENV_KEYS.coverageCss];
360
362
  if (coverage.browser === void 0 && coverageBrowser)
361
363
  coverage.browser = coverageBrowser;
362
364
  if (coverage.server === void 0 && coverageServer) coverage.server = coverageServer;
363
- if (coverage.browser !== void 0 || coverage.server !== void 0) mergedRaw.coverage = coverage;
365
+ if (coverage.css === void 0 && coverageCss) coverage.css = coverageCss;
366
+ if (coverage.browser !== void 0 || coverage.server !== void 0 || coverage.css !== void 0) {
367
+ mergedRaw.coverage = coverage;
368
+ }
364
369
  const externalId = { ...mergedRaw.externalId };
365
370
  const externalIdPattern = env[PIWI_ENV_KEYS.externalIdPattern];
366
371
  const externalIdOnDuplicate = env[PIWI_ENV_KEYS.externalIdOnDuplicate];
@@ -406,9 +411,11 @@ function applyOptionsToEnv(options) {
406
411
  if (options.collectPerformanceMetrics === false) {
407
412
  env[PIWI_ENV_KEYS.coverageBrowser] = "off";
408
413
  env[PIWI_ENV_KEYS.coverageServer] = "off";
414
+ env[PIWI_ENV_KEYS.coverageCss] = "off";
409
415
  } else {
410
416
  if (options.coverage?.browser !== void 0) env[PIWI_ENV_KEYS.coverageBrowser] = options.coverage.browser;
411
417
  if (options.coverage?.server !== void 0) env[PIWI_ENV_KEYS.coverageServer] = options.coverage.server;
418
+ if (options.coverage?.css !== void 0) env[PIWI_ENV_KEYS.coverageCss] = options.coverage.css;
412
419
  }
413
420
  if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
414
421
  env[PIWI_ENV_KEYS.captureServerTraces] = "false";
@@ -1063,14 +1070,14 @@ var os3 = __toESM(require("os"));
1063
1070
  var fs4 = __toESM(require("fs"));
1064
1071
 
1065
1072
  // src/internal/support/instance-id.ts
1066
- var crypto = __toESM(require("crypto"));
1073
+ var crypto2 = __toESM(require("crypto"));
1067
1074
  var os2 = __toESM(require("os"));
1068
1075
  function hashForProject(projectName) {
1069
- return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
1076
+ return crypto2.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
1070
1077
  }
1071
1078
  function computeInstanceId(projectName, runLabel) {
1072
1079
  const seed = runLabel ? `${projectName}|${runLabel}` : `${os2.hostname()}|${projectName}`;
1073
- return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 16);
1080
+ return crypto2.createHash("sha256").update(seed).digest("hex").slice(0, 16);
1074
1081
  }
1075
1082
 
1076
1083
  // src/internal/streaming/stream-buffer.ts
@@ -1132,7 +1139,7 @@ var StreamBuffer = class {
1132
1139
  };
1133
1140
 
1134
1141
  // src/internal/streaming/crash-recovery.ts
1135
- var crypto2 = __toESM(require("crypto"));
1142
+ var crypto3 = __toESM(require("crypto"));
1136
1143
  var fs5 = __toESM(require("fs"));
1137
1144
  var os4 = __toESM(require("os"));
1138
1145
  var path5 = __toESM(require("path"));
@@ -1148,7 +1155,7 @@ function recoveryServer(serverUrl) {
1148
1155
  return serverUrl.replace(/\/+$/, "");
1149
1156
  }
1150
1157
  function recoveryFilePath(serverUrl, projectName) {
1151
- const key = crypto2.createHash("sha256").update(`${recoveryServer(serverUrl)}
1158
+ const key = crypto3.createHash("sha256").update(`${recoveryServer(serverUrl)}
1152
1159
  ${projectName}`).digest("hex").slice(0, 16);
1153
1160
  return path5.join(os4.tmpdir(), `piwi-dashboard-recovery-${key}.json`);
1154
1161
  }
@@ -1169,7 +1176,7 @@ var CrashRecovery = class {
1169
1176
  /** Write the payload for a later retry, owner-only, replacing the file in one step. */
1170
1177
  save(payload) {
1171
1178
  const file = { v: 1, serverUrl: this.serverUrl, payload };
1172
- 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`;
1173
1180
  try {
1174
1181
  fs5.writeFileSync(temp, JSON.stringify(file), { encoding: "utf8", flag: "wx", mode: 384 });
1175
1182
  fs5.renameSync(temp, this.filePath);
@@ -1240,7 +1247,7 @@ var CrashRecovery = class {
1240
1247
  var fs7 = __toESM(require("fs"));
1241
1248
  var os5 = __toESM(require("os"));
1242
1249
  var path7 = __toESM(require("path"));
1243
- var crypto3 = __toESM(require("crypto"));
1250
+ var crypto4 = __toESM(require("crypto"));
1244
1251
 
1245
1252
  // src/internal/files/compression.ts
1246
1253
  var fs6 = __toESM(require("fs"));
@@ -1312,6 +1319,8 @@ var ATTACHMENT_NAMES = {
1312
1319
  codeReach: "piwi-code-reach",
1313
1320
  browserCoverage: "piwi-browser-coverage",
1314
1321
  browserFunctions: "piwi-browser-functions",
1322
+ cssCoverage: "piwi-css-coverage",
1323
+ cssRules: "piwi-css-rules",
1315
1324
  serverCoverageRequests: "piwi-server-coverage-requests",
1316
1325
  webVitals: "piwi-web-vitals",
1317
1326
  locatorSuggestion: "piwi-locator-suggestion",
@@ -1323,6 +1332,19 @@ var ATTACHMENT_NAMES = {
1323
1332
  pauses: "piwi-pauses"
1324
1333
  };
1325
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
+ }
1326
1348
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
1327
1349
  var USER_PICK_ANNOTATION = ATTACHMENT_NAMES.userPick;
1328
1350
 
@@ -1441,18 +1463,18 @@ var FileHandler = class {
1441
1463
  }
1442
1464
  const contentType = a.contentType || "application/octet-stream";
1443
1465
  const originalName = `${safeFileName(a.name || "attachment")}${BODY_EXTENSIONS[contentType] ?? ""}`;
1444
- let filePath2 = this.bodyFiles.get(a);
1445
- if (!filePath2) {
1466
+ let filePath3 = this.bodyFiles.get(a);
1467
+ if (!filePath3) {
1446
1468
  try {
1447
- filePath2 = path7.join(this.bodyAttachmentDir(), `${++this.bodyFileCount}-${originalName}`);
1448
- fs7.writeFileSync(filePath2, body);
1449
- 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);
1450
1472
  } catch (error) {
1451
1473
  this.logger.warn(`Could not stage attachment "${a.name}" for upload: ${errorMessage(error)}`);
1452
1474
  return null;
1453
1475
  }
1454
1476
  }
1455
- return { name: a.name || "attachment", path: filePath2, contentType, originalName };
1477
+ return { name: a.name || "attachment", path: filePath3, contentType, originalName };
1456
1478
  }
1457
1479
  bodyAttachmentDir() {
1458
1480
  if (!this.bodyDir) this.bodyDir = fs7.mkdtempSync(path7.join(os5.tmpdir(), "piwi-dashboard-attachments-"));
@@ -1477,11 +1499,11 @@ var FileHandler = class {
1477
1499
  }
1478
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 */
1479
1501
  parsePerformanceAttachments(testCase, attachments) {
1480
- const find = (name) => attachments.find((a) => a.name === name);
1481
- const net2 = find(ATTACHMENT_NAMES.network);
1482
- if (net2?.body) {
1502
+ const find = (name) => findPiwiAttachment(attachments, name);
1503
+ const net5 = find(ATTACHMENT_NAMES.network);
1504
+ if (net5?.body) {
1483
1505
  try {
1484
- testCase.networkRequests = JSON.parse(net2.body.toString());
1506
+ testCase.networkRequests = JSON.parse(net5.body.toString());
1485
1507
  } catch {
1486
1508
  }
1487
1509
  }
@@ -1540,7 +1562,7 @@ var FileHandler = class {
1540
1562
  if (fs7.existsSync(tp)) lastPath = tp;
1541
1563
  }
1542
1564
  if (!lastPath) return null;
1543
- const hash = crypto3.createHash("sha256");
1565
+ const hash = crypto4.createHash("sha256");
1544
1566
  await new Promise((resolve11, reject) => {
1545
1567
  fs7.createReadStream(lastPath).on("data", (chunk) => hash.update(chunk)).on("end", resolve11).on("error", reject);
1546
1568
  });
@@ -2119,6 +2141,8 @@ var StreamManager = class {
2119
2141
  this._token = null;
2120
2142
  this._auth = null;
2121
2143
  this._startPromise = null;
2144
+ /** Called when the dashboard says someone opened the run. */
2145
+ this.onWatched = null;
2122
2146
  const maxBytes = this.options.maxStreamBufferBytes ?? 0;
2123
2147
  this.pendingEvents = new BoundedEventQueue(maxBytes);
2124
2148
  this.pendingBeginEvents = new BoundedEventQueue(maxBytes);
@@ -2143,6 +2167,10 @@ var StreamManager = class {
2143
2167
  get startPromise() {
2144
2168
  return this._startPromise;
2145
2169
  }
2170
+ /** Whether anyone has the run open live, as the dashboard last said; `null` until it says. */
2171
+ get isWatched() {
2172
+ return this.watched;
2173
+ }
2146
2174
  /**
2147
2175
  * Whether a test-result (`complete`) event never reached the server through
2148
2176
  * the live stream: shed under buffer pressure, too large for any request the
@@ -2393,8 +2421,11 @@ var StreamManager = class {
2393
2421
  if (this.pendingEvents.length > 0) this.flush();
2394
2422
  }, delay);
2395
2423
  }
2396
- // Take the dashboard's answer on whether anyone watches the run. An answer
2397
- // without the flag (an older dashboard) keeps step events flowing.
2424
+ /**
2425
+ * Take the dashboard's answer on whether anyone watches the run, from any
2426
+ * request of the stream. An answer without the flag (an older dashboard)
2427
+ * keeps step events flowing.
2428
+ */
2398
2429
  noteWatched(response) {
2399
2430
  const watched = response?.watched;
2400
2431
  const next = typeof watched === "boolean" ? watched : null;
@@ -2403,7 +2434,9 @@ var StreamManager = class {
2403
2434
  next ? "The run is open on the dashboard: streaming live steps." : "Nobody is watching the run: live steps are not sent."
2404
2435
  );
2405
2436
  }
2437
+ const opened = next === true && this.watched !== true;
2406
2438
  this.watched = next;
2439
+ if (opened) this.onWatched?.();
2407
2440
  }
2408
2441
  // Idle time after which a heartbeat goes out.
2409
2442
  heartbeatDelay() {
@@ -3367,6 +3400,258 @@ function maskUrlFragment(hash) {
3367
3400
  const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
3368
3401
  return hash.startsWith("#") ? `#${out}` : out;
3369
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
+ }
3370
3655
 
3371
3656
  // ../core/src/locator-break.ts
3372
3657
  var filePath = (path34) => path34.replace(/\\/g, "/").replace(/^\.\//, "");
@@ -3903,7 +4188,7 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3903
4188
  const maxFrames = opts.maxFrames ?? 4;
3904
4189
  const picked = [];
3905
4190
  const seen = /* @__PURE__ */ new Set();
3906
- const add = (absFile, line) => {
4191
+ const add2 = (absFile, line) => {
3907
4192
  const key = `${absFile}:${line}`;
3908
4193
  if (seen.has(key)) return;
3909
4194
  seen.add(key);
@@ -3920,12 +4205,12 @@ function collectSourceFrames(errorText, testFile2, declLine, opts = {}) {
3920
4205
  }
3921
4206
  const rel = path11.relative(projectRoot, abs);
3922
4207
  if (rel.startsWith("..") || path11.isAbsolute(rel)) continue;
3923
- if (!isNaN(frame.line)) add(abs, frame.line);
4208
+ if (!isNaN(frame.line)) add2(abs, frame.line);
3924
4209
  }
3925
4210
  }
3926
4211
  const absTest = path11.resolve(testFile2);
3927
4212
  if (!picked.some((p) => p.absFile === absTest)) {
3928
- add(absTest, extractFailingLine(errorText, testFile2, declLine));
4213
+ add2(absTest, extractFailingLine(errorText, testFile2, declLine));
3929
4214
  }
3930
4215
  const frames = [];
3931
4216
  for (const p of picked.slice(0, maxFrames)) {
@@ -4040,8 +4325,8 @@ var fs15 = __toESM(require("fs"));
4040
4325
  var path12 = __toESM(require("path"));
4041
4326
  var os7 = __toESM(require("os"));
4042
4327
  var fs12 = __toESM(require("fs"));
4043
- function ariaSampleIdentity(filePath2, title) {
4044
- return `${filePath2}\0${title}`;
4328
+ function ariaSampleIdentity(filePath3, title) {
4329
+ return `${filePath3}\0${title}`;
4045
4330
  }
4046
4331
  function getAriaSampleFilePath(projectName) {
4047
4332
  return path12.join(os7.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
@@ -4085,6 +4370,7 @@ function isDueForAriaSample(testInfo) {
4085
4370
  }
4086
4371
 
4087
4372
  // src/internal/support/base-url-check.ts
4373
+ var net = __toESM(require("net"));
4088
4374
  var BASE_URL_CHECK_ATTEMPTS = 3;
4089
4375
  var BASE_URL_CHECK_TIMEOUT_MS = 1e4;
4090
4376
  var BASE_URL_CHECK_DELAY_MS = 2e3;
@@ -4106,9 +4392,9 @@ function cliProjectNames(argv = process.argv) {
4106
4392
  }
4107
4393
  return names;
4108
4394
  }
4109
- function wildcard(pattern) {
4395
+ function wildcard(pattern, flags = "i") {
4110
4396
  const escaped = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
4111
- return new RegExp(`^${escaped.join(".*")}$`, "i");
4397
+ return new RegExp(`^${escaped.join(".*")}$`, flags);
4112
4398
  }
4113
4399
  function selectedProjects(projects, argv = process.argv, filtered) {
4114
4400
  let chosen;
@@ -4118,7 +4404,7 @@ function selectedProjects(projects, argv = process.argv, filtered) {
4118
4404
  } else {
4119
4405
  const names = cliProjectNames(argv);
4120
4406
  if (!names || names.length === 0) return projects;
4121
- const patterns = names.map(wildcard);
4407
+ const patterns = names.map((name) => wildcard(name));
4122
4408
  chosen = new Set(projects.filter((p) => patterns.some((re) => re.test(p.name ?? ""))));
4123
4409
  }
4124
4410
  if (!argv.includes("--no-deps")) {
@@ -4140,23 +4426,58 @@ function asProxy(value) {
4140
4426
  const proxy = value;
4141
4427
  return proxy && typeof proxy === "object" && typeof proxy.server === "string" ? proxy : void 0;
4142
4428
  }
4143
- function baseUrlTargets(config, argv = process.argv) {
4429
+ function exposeNetworkCovers(rule, url) {
4430
+ if (!rule) return false;
4431
+ let parsed2;
4432
+ try {
4433
+ parsed2 = new URL(url);
4434
+ } catch {
4435
+ return false;
4436
+ }
4437
+ const host = parsed2.hostname;
4438
+ const port = parsed2.port ? Number(parsed2.port) : parsed2.protocol === "https:" ? 443 : 80;
4439
+ return rule.split(",").some((token) => {
4440
+ const match = token.match(/^(.*?)(?::(\d+))?$/);
4441
+ if (!match) return false;
4442
+ if (match[2] !== void 0 && Number(match[2]) !== port) return false;
4443
+ const tokenHost = match[1];
4444
+ if (tokenHost === "<loopback>") {
4445
+ return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "[::1]";
4446
+ }
4447
+ if (tokenHost === "*") return true;
4448
+ if (net.isIP(tokenHost)) return host === tokenHost;
4449
+ if (net.isIP(host)) return false;
4450
+ return wildcard(tokenHost.startsWith(".") ? `*${tokenHost}` : tokenHost, "").test(host);
4451
+ });
4452
+ }
4453
+ function reachedRemotely(use, url, env) {
4454
+ if (use?.connectOptions === void 0) {
4455
+ return !!env.PW_TEST_CONNECT_WS_ENDPOINT && !exposeNetworkCovers(env.PW_TEST_CONNECT_EXPOSE_NETWORK, url);
4456
+ }
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);
4460
+ }
4461
+ function baseUrlTargets(config, argv = process.argv, env = process.env) {
4144
4462
  const projects = config.projects?.length ? config.projects : [{ name: "", use: config.use }];
4145
4463
  const filtered = config.projects?.length ? config.filteredProjects : void 0;
4146
4464
  const targets = /* @__PURE__ */ new Map();
4147
4465
  for (const project of selectedProjects(projects, argv, filtered)) {
4148
4466
  const url = project.use?.baseURL;
4149
4467
  if (typeof url !== "string" || !/^https?:\/\//i.test(url)) continue;
4468
+ const remoteBrowser = reachedRemotely(project.use, url, env);
4150
4469
  const target = targets.get(url);
4151
4470
  if (target) {
4152
4471
  if (project.name) target.projects.push(project.name);
4472
+ target.remoteBrowser ||= remoteBrowser;
4153
4473
  continue;
4154
4474
  }
4155
4475
  targets.set(url, {
4156
4476
  url,
4157
4477
  projects: project.name ? [project.name] : [],
4158
4478
  ignoreHTTPSErrors: project.use?.ignoreHTTPSErrors === true,
4159
- proxy: asProxy(project.use?.proxy)
4479
+ proxy: asProxy(project.use?.proxy),
4480
+ remoteBrowser
4160
4481
  });
4161
4482
  }
4162
4483
  return [...targets.values()];
@@ -4197,14 +4518,21 @@ async function checkBaseUrl(target, options = {}) {
4197
4518
  }
4198
4519
  return `${reason} (${attempts} ${attempts === 1 ? "attempt" : "attempts"})`;
4199
4520
  }
4521
+ function remoteBrowserNote(target) {
4522
+ if (!target.remoteBrowser) return null;
4523
+ const { host } = new URL(target.url);
4524
+ return `The check ran from this machine's network, but a project's remote browser reaches ${host} from its own, since its exposeNetwork rule does not cover that address.`;
4525
+ }
4200
4526
  async function checkBaseUrls(config, options = {}) {
4201
- const targets = baseUrlTargets(config, options.argv);
4527
+ const targets = baseUrlTargets(config, options.argv, options.env);
4202
4528
  const reasons = await Promise.all(targets.map((target) => checkBaseUrl(target, options)));
4203
4529
  const down = targets.flatMap((target, i) => reasons[i] ? [{ target, reason: reasons[i] }] : []);
4204
4530
  if (down.length === 0) return targets;
4205
4531
  const lines = down.map(({ target, reason }) => {
4206
4532
  const used = target.projects.length > 0 ? ` (${target.projects.join(", ")})` : "";
4207
- return ` ${target.url}${used}: ${reason}`;
4533
+ const note = remoteBrowserNote(target);
4534
+ return ` ${target.url}${used}: ${reason}${note ? `
4535
+ ${note}` : ""}`;
4208
4536
  });
4209
4537
  const message = [
4210
4538
  `[Piwi Dashboard] The app under test did not answer, so no test ran:`,
@@ -4545,7 +4873,7 @@ function reduceCoverage(capture, granularity) {
4545
4873
  }
4546
4874
 
4547
4875
  // src/internal/collect/source-checksums.ts
4548
- var crypto4 = __toESM(require("crypto"));
4876
+ var crypto5 = __toESM(require("crypto"));
4549
4877
  var fs14 = __toESM(require("fs"));
4550
4878
  var path14 = __toESM(require("path"));
4551
4879
  var CHECKSUM = /^(sha1|sha256):[0-9a-f]{40,64}$/;
@@ -4563,7 +4891,7 @@ function contentChecksums(content) {
4563
4891
  const out = /* @__PURE__ */ new Set();
4564
4892
  for (const form of lineEndingForms(content)) {
4565
4893
  for (const algorithm of ["sha1", "sha256"]) {
4566
- out.add(`${algorithm}:${crypto4.createHash(algorithm).update(form).digest("hex")}`);
4894
+ out.add(`${algorithm}:${crypto5.createHash(algorithm).update(form).digest("hex")}`);
4567
4895
  }
4568
4896
  }
4569
4897
  return out;
@@ -4675,10 +5003,10 @@ function fileChecksums(files, into = /* @__PURE__ */ new Map()) {
4675
5003
  if (!Array.isArray(files)) return into;
4676
5004
  for (const item of files) {
4677
5005
  if (!isRecord2(item)) continue;
4678
- const filePath2 = normalizeCoveragePath(item.path);
5006
+ const filePath3 = normalizeCoveragePath(item.path);
4679
5007
  const checksums = checksumsOf(item.checksums);
4680
- if (filePath2 === null || checksums.length === 0) continue;
4681
- 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])]);
4682
5010
  }
4683
5011
  return into;
4684
5012
  }
@@ -4687,9 +5015,9 @@ function runFilesOf(answer) {
4687
5015
  const files = [];
4688
5016
  for (const item of answer.files) {
4689
5017
  if (!isRecord2(item) || item.runtime !== "server" && item.runtime !== "browser") continue;
4690
- const filePath2 = normalizeCoveragePath(item.path);
4691
- if (filePath2 === null) continue;
4692
- 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 };
4693
5021
  const functions = normalizeFunctionRanges(item.functions);
4694
5022
  const lines = normalizeLineRanges(item.lines);
4695
5023
  const executable = normalizeLineRanges(item.executable);
@@ -4709,7 +5037,7 @@ function instanceOf(answer) {
4709
5037
  return { id: instance.id, startedAt };
4710
5038
  }
4711
5039
  function processKeyOf(id) {
4712
- const dot = id.lastIndexOf(".");
5040
+ const dot = id.indexOf(".");
4713
5041
  return dot > 0 ? id.slice(0, dot) : null;
4714
5042
  }
4715
5043
  function withTestCoverage(files, tests) {
@@ -5127,7 +5455,16 @@ function createGlobalSetup(options, userSetup) {
5127
5455
  }
5128
5456
  if (opts.checkBaseUrl === true) {
5129
5457
  const checked = await checkBaseUrls(config ?? {});
5130
- for (const target of checked) logger.debug(`Base URL answered: ${target.url}`);
5458
+ for (const target of checked) {
5459
+ const note = remoteBrowserNote(target);
5460
+ if (note) {
5461
+ logger.info(
5462
+ `Base URL answered: ${target.url}. ${note} A navigation failure from that browser can mean its machine cannot reach the app, not that the app is down.`
5463
+ );
5464
+ } else {
5465
+ logger.debug(`Base URL answered: ${target.url}`);
5466
+ }
5467
+ }
5131
5468
  }
5132
5469
  if (opts.enabled === false || !opts.serverUrl) {
5133
5470
  logger.info("Not enabled \u2014 set PIWI_DASHBOARD_URL or serverUrl to enable.");
@@ -5303,9 +5640,9 @@ function loadPlan() {
5303
5640
  function probeItemForTest(test) {
5304
5641
  const plan = loadPlan();
5305
5642
  if (!plan) return null;
5306
- 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;
5307
5644
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
5308
- return matchProbeItem(plan, { title: test.title, filePath: filePath2, suitePath });
5645
+ return matchProbeItem(plan, { title: test.title, filePath: filePath3, suitePath });
5309
5646
  }
5310
5647
  function classifyProbeHandled(signals) {
5311
5648
  const blank = signals.blankPage === true;
@@ -6300,9 +6637,9 @@ function duplicateTestKey(test) {
6300
6637
  }
6301
6638
  function readDuplicateTest(raw) {
6302
6639
  if (!raw || typeof raw !== "object") return null;
6303
- const { filePath: filePath2, suitePath, title, line } = raw;
6304
- if (typeof filePath2 !== "string" || typeof title !== "string") return null;
6305
- 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 };
6306
6643
  if (Array.isArray(suitePath) && suitePath.every((s) => typeof s === "string") && suitePath.length > 0)
6307
6644
  test.suitePath = suitePath;
6308
6645
  if (typeof line === "number" && Number.isFinite(line)) test.line = line;
@@ -6859,7 +7196,9 @@ var RunSubmitter = class {
6859
7196
  };
6860
7197
 
6861
7198
  // src/internal/capture/resource-ledger.ts
7199
+ var childProcess = __toESM(require("child_process"));
6862
7200
  var fs21 = __toESM(require("fs"));
7201
+ var net2 = __toESM(require("net"));
6863
7202
  var path20 = __toESM(require("path"));
6864
7203
  var import_node_module2 = require("module");
6865
7204
 
@@ -7237,6 +7576,8 @@ var WorkerMetrics = class {
7237
7576
  constructor(proc = new ProcFs(), pid = process.pid, platform = process.platform) {
7238
7577
  this.proc = proc;
7239
7578
  this.pid = pid;
7579
+ /** The lowest heap read right after a full garbage collection in the current window. */
7580
+ this.heapAfterGc = null;
7240
7581
  this.loop = null;
7241
7582
  this.usage = null;
7242
7583
  /** Each process's CPU and wait at the start of the current window; null before the first. */
@@ -7251,6 +7592,28 @@ var WorkerMetrics = class {
7251
7592
  histogram = null;
7252
7593
  }
7253
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
+ }
7254
7617
  }
7255
7618
  /** The worker's processes under it, keyed by pid and start time. */
7256
7619
  tree() {
@@ -7284,6 +7647,7 @@ var WorkerMetrics = class {
7284
7647
  start(processes = true) {
7285
7648
  try {
7286
7649
  this.histogram?.reset();
7650
+ this.heapAfterGc = null;
7287
7651
  this.loop = import_node_perf_hooks.performance.eventLoopUtilization();
7288
7652
  this.usage = process.resourceUsage();
7289
7653
  if (this.linux && processes && !this.subtree) this.snapshot();
@@ -7306,6 +7670,7 @@ var WorkerMetrics = class {
7306
7670
  loopDelayP99Ms: this.histogram ? round1(this.histogram.percentile(99) / 1e6) : 0,
7307
7671
  loopDelayMaxMs: this.histogram ? round1(this.histogram.max / 1e6) : 0,
7308
7672
  heapUsedMb: round1(process.memoryUsage().heapUsed / MB),
7673
+ heapAfterGcMb: this.heapAfterGc === null ? null : round1(this.heapAfterGc / MB),
7309
7674
  fds: this.linux ? this.proc.fdCount(this.pid) : null
7310
7675
  }
7311
7676
  };
@@ -7340,6 +7705,7 @@ var WorkerMetrics = class {
7340
7705
  }
7341
7706
  dispose() {
7342
7707
  this.histogram?.disable();
7708
+ this.gcObserver?.disconnect();
7343
7709
  }
7344
7710
  };
7345
7711
  var PageReader = class {
@@ -7361,12 +7727,36 @@ var PageReader = class {
7361
7727
  try {
7362
7728
  const result = await internalCall(page, () => opened.send("Performance.getMetrics"));
7363
7729
  const values = new Map((result.metrics ?? []).map((metric) => [metric.name, metric.value]));
7364
- return {
7730
+ const main = {
7365
7731
  cpuMs: Math.round((values.get("TaskDuration") ?? 0) * 1e3),
7366
7732
  heapMb: round1((values.get("JSHeapUsedSize") ?? 0) / MB),
7367
7733
  nodes: values.get("Nodes") ?? 0,
7368
7734
  listeners: values.get("JSEventListeners") ?? 0
7369
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;
7370
7760
  } catch {
7371
7761
  return null;
7372
7762
  }
@@ -7375,6 +7765,9 @@ var PageReader = class {
7375
7765
 
7376
7766
  // src/internal/capture/resource-ledger.ts
7377
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;
7378
7771
  var USE_METHODS = ["evaluate", "evaluateHandle", "$eval", "$$eval", "waitForFunction", "addScriptTag", "addStyleTag"];
7379
7772
  var CLOSE_TIMEOUT_MS = 5e3;
7380
7773
  var PAGE_READ_CAP = 50;
@@ -7450,20 +7843,41 @@ function playwrightTestInfoSource() {
7450
7843
  function readLeakCheck(value = process.env[PIWI_ENV_KEYS.leakCheck]) {
7451
7844
  return value === "fail" || value === "close" ? value : "report";
7452
7845
  }
7453
- function countHandles() {
7846
+ function countHandles(types = TRACKED_HANDLE_TYPES) {
7454
7847
  const counts = {};
7455
7848
  const read = process.getActiveResourcesInfo;
7456
7849
  if (typeof read !== "function") return counts;
7457
7850
  for (const type of read.call(process)) {
7458
- if (TRACKED_HANDLE_TYPES.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7851
+ if (types.includes(type)) counts[type] = (counts[type] ?? 0) + 1;
7459
7852
  }
7460
7853
  return counts;
7461
7854
  }
7462
- async function settledHandles(start) {
7463
- const end = countHandles();
7855
+ async function settledHandles(start, types = TRACKED_HANDLE_TYPES) {
7856
+ const end = countHandles(types);
7464
7857
  if (!Object.entries(end).some(([type, n]) => n > (start[type] ?? 0))) return end;
7465
7858
  await new Promise((resolve11) => setTimeout(resolve11, 0));
7466
- 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";
7467
7881
  }
7468
7882
  function settleWithin(promise, ms) {
7469
7883
  let timer;
@@ -7475,7 +7889,7 @@ function settleWithin(promise, ms) {
7475
7889
  ]).finally(() => clearTimeout(timer));
7476
7890
  }
7477
7891
  var ResourceLedger = class {
7478
- 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) {
7479
7893
  this.playwright = playwright;
7480
7894
  this.workerIndex = workerIndex;
7481
7895
  this.leakCheck = leakCheck;
@@ -7483,6 +7897,7 @@ var ResourceLedger = class {
7483
7897
  this.testInfoSource = testInfoSource;
7484
7898
  this.now = now;
7485
7899
  this.readers = readers;
7900
+ this.nodeHandles = nodeHandles;
7486
7901
  this.nextId = 1;
7487
7902
  this.entries = /* @__PURE__ */ new Map();
7488
7903
  this.byObject = /* @__PURE__ */ new WeakMap();
@@ -7499,6 +7914,11 @@ var ResourceLedger = class {
7499
7914
  this.listener = null;
7500
7915
  /** A browser or a context was opened in this worker: there are browser processes for the metrics to read. */
7501
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 = [];
7502
7922
  }
7503
7923
  /** Listen to the worker's Playwright and wrap its browser types. False when the instrumentation is missing. */
7504
7924
  install() {
@@ -7531,13 +7951,107 @@ var ResourceLedger = class {
7531
7951
  for (const type of [this.playwright.chromium, this.playwright.firefox, this.playwright.webkit]) {
7532
7952
  if (type) this.wrapBrowserType(type);
7533
7953
  }
7954
+ if (this.nodeHandles) this.guard(() => this.wrapNodeHandles());
7534
7955
  return true;
7535
7956
  }
7536
- /** 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. */
7537
7958
  uninstall() {
7538
7959
  const remove = this.playwright?._instrumentation?.removeListener;
7539
7960
  if (this.listener && typeof remove === "function") remove.call(this.playwright._instrumentation, this.listener);
7540
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 } : {} }));
7541
8055
  }
7542
8056
  /** Mark a page used: a locator was built on it. */
7543
8057
  notePageUse(page) {
@@ -7547,7 +8061,7 @@ var ResourceLedger = class {
7547
8061
  /** Open the census window of a test. */
7548
8062
  testStarted(info) {
7549
8063
  this.window = testRef2(info);
7550
- this.handlesAtStart = countHandles();
8064
+ this.handlesAtStart = countHandles(this.countedHandleTypes());
7551
8065
  this.openAtStart = { contexts: 0, pages: 0 };
7552
8066
  for (const entry of this.openEntries()) {
7553
8067
  if (entry.birth.kind === "page") this.openAtStart.pages++;
@@ -7567,9 +8081,14 @@ var ResourceLedger = class {
7567
8081
  if (this.leakCheck === "close" && leaks.length > 0) await this.closeLeaks(leaks);
7568
8082
  const ref = test ? { ...test, title: typeof info.title === "string" ? info.title : "", ...failed ? { failed: true } : {} } : null;
7569
8083
  const metrics = this.readers.metrics?.end(this.sawBrowser) ?? null;
7570
- 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;
7571
8089
  const census = this.census(ref, handles, await this.readPages());
7572
8090
  if (metrics) census.metrics = metrics;
8091
+ if (running) census.running = running;
7573
8092
  if (test) {
7574
8093
  census.openAtStart = this.openAtStart;
7575
8094
  if (!failed) census.leftOpen = leaks.length;
@@ -7608,6 +8127,10 @@ var ResourceLedger = class {
7608
8127
  this.uninstall();
7609
8128
  return census;
7610
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
+ }
7611
8134
  guard(run) {
7612
8135
  try {
7613
8136
  run();
@@ -7830,6 +8353,7 @@ var ResourceLedger = class {
7830
8353
  open
7831
8354
  };
7832
8355
  if (handles) census.handles = handles;
8356
+ for (const close of this.closed) this.entries.delete(close.id);
7833
8357
  this.born = [];
7834
8358
  this.closed = [];
7835
8359
  return census;
@@ -7868,7 +8392,8 @@ function startResourceLedger(playwright, workerIndex) {
7868
8392
  process.env[PIWI_RESOURCES_RESULTS_ENV] || null,
7869
8393
  playwrightTestInfoSource(),
7870
8394
  Date.now,
7871
- { metrics, pages: new PageReader() }
8395
+ { metrics, pages: new PageReader() },
8396
+ true
7872
8397
  );
7873
8398
  if (!ledger.install()) {
7874
8399
  metrics.dispose();
@@ -8027,7 +8552,15 @@ function buildResourceReport(input) {
8027
8552
  if (!entry) continue;
8028
8553
  entry.lastOpenAt = census.at;
8029
8554
  if (open.used !== void 0) entry.used = open.used;
8030
- 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
+ }
8031
8564
  if (open.main) entry.mainCpu.push({ at: census.at, cpuMs: open.main.cpuMs });
8032
8565
  }
8033
8566
  }
@@ -8042,6 +8575,7 @@ function buildResourceReport(input) {
8042
8575
  }
8043
8576
  findings.push(...groupLeaks(leaked, all));
8044
8577
  findings.push(...pilingFindings(all));
8578
+ findings.push(...workerPilingFindings(byWorker));
8045
8579
  findings.push(...handleFindings(byWorker));
8046
8580
  findings.push(...groupIdle(idle, input.fixturesByTest ?? /* @__PURE__ */ new Map()));
8047
8581
  findings.push(...probableFindings(input.tallies ?? []));
@@ -8179,16 +8713,39 @@ function groupIdle(idle, fixturesByTest) {
8179
8713
  }
8180
8714
  return [...groups.values()].map(({ testIds, ...finding }) => ({ ...finding, tests: testIds.size })).sort((a, b) => b.count - a.count);
8181
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
+ }
8182
8742
  function pilingFindings(all) {
8183
8743
  const out = [];
8184
8744
  for (const entry of all) {
8185
- for (const what of ["pages", "listeners", "routes"]) {
8745
+ for (const what of ["pages", "listeners", "routes", "elements"]) {
8186
8746
  const values = entry.series.map((point) => point[what]).filter((v) => typeof v === "number");
8187
- if (values.length < 3) continue;
8188
- const rising = values.every((value, i) => i === 0 || value >= values[i - 1]);
8189
- const from = values[0];
8190
- const to = values[values.length - 1];
8191
- if (!rising || to - from < 2) continue;
8747
+ const growth = pileUp(values, what);
8748
+ if (!growth) continue;
8192
8749
  out.push({
8193
8750
  verdict: "piling",
8194
8751
  kind: entry.birth.kind,
@@ -8196,34 +8753,91 @@ function pilingFindings(all) {
8196
8753
  site: siteOf(entry.birth),
8197
8754
  tests: values.length,
8198
8755
  count: 1,
8199
- growth: { what, from, to, tests: values.length }
8756
+ growth: { what, from: Math.round(growth.from), to: Math.round(growth.to), tests: values.length }
8200
8757
  });
8201
8758
  }
8202
8759
  }
8203
8760
  return out.sort((a, b) => b.growth.to - b.growth.from - (a.growth.to - a.growth.from));
8204
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
+ }
8205
8811
  function handleFindings(byWorker) {
8206
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
+ };
8207
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
+ }
8208
8832
  const ordered = censuses.filter((census) => census.test && census.handles).sort((a, b) => a.at - b.at);
8209
8833
  for (let i = 0; i < ordered.length - 1; i++) {
8210
8834
  const census = ordered[i];
8211
8835
  const next = ordered[i + 1];
8212
8836
  for (const [type, end] of Object.entries(census.handles.end)) {
8837
+ if (census.running && type === "TCPServerWrap") continue;
8213
8838
  const grew = end - (census.handles.start[type] ?? 0);
8214
8839
  if (grew <= 0 || (next.handles.start[type] ?? 0) < end) continue;
8215
- const test = census.test;
8216
- const key = `${test.id}|${type}`;
8217
- const group = groups.get(key) ?? {
8218
- verdict: "handle",
8219
- kind: "handle",
8220
- where: type,
8221
- tests: 1,
8222
- count: 0,
8223
- detail: `"${test.title ?? ""}" (${test.file})`
8224
- };
8225
- group.count += grew;
8226
- groups.set(key, group);
8840
+ add2(census.test, type, grew);
8227
8841
  }
8228
8842
  }
8229
8843
  }
@@ -8268,19 +8882,37 @@ var KIND_LABEL = {
8268
8882
  context: "context",
8269
8883
  page: "page",
8270
8884
  request: "API context",
8271
- 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"
8272
8903
  };
8273
8904
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
8274
8905
  var HANDLE_LABEL = {
8275
8906
  TCPServerWrap: "server",
8276
8907
  FSEventWrap: "file watcher",
8277
- StatWatcher: "file watcher"
8908
+ StatWatcher: "file watcher",
8909
+ ChildProcess: "process"
8278
8910
  };
8279
8911
  function findingLine(finding) {
8280
8912
  const verdict = finding.verdict.padEnd(8);
8281
- 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];
8282
8914
  const kind = label.padEnd(11);
8283
- const parts = finding.verdict === "handle" ? [] : [finding.where];
8915
+ const parts = finding.verdict === "handle" ? finding.site ? [finding.site] : [] : [finding.where];
8284
8916
  switch (finding.verdict) {
8285
8917
  case "leaked": {
8286
8918
  if (finding.count > 1) parts.push(`${finding.count} ${KIND_LABEL[finding.kind]}s`);
@@ -8303,11 +8935,12 @@ function findingLine(finding) {
8303
8935
  break;
8304
8936
  case "piling": {
8305
8937
  const growth = finding.growth;
8306
- const noun = growth.what === "pages" ? "open pages" : growth.what === "routes" ? "route handlers" : "listeners";
8307
- 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);
8308
8940
  break;
8309
8941
  }
8310
8942
  case "handle":
8943
+ if (finding.count > 1) parts.push(`${finding.count} ${label}${label.endsWith("s") ? "es" : "s"}`);
8311
8944
  parts.push(`left running in the worker by ${finding.detail ?? "a test"}`);
8312
8945
  break;
8313
8946
  case "probable":
@@ -8446,6 +9079,12 @@ async function dirBytes(dir, budget) {
8446
9079
  }
8447
9080
  return bytes;
8448
9081
  }
9082
+ function memoryOf(container) {
9083
+ const totalMemory = os8.totalmem();
9084
+ const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
9085
+ const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
9086
+ return { memoryBytes: totalMemory, memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null };
9087
+ }
8449
9088
  var RunSampler = class {
8450
9089
  constructor(options = {}) {
8451
9090
  this.timer = null;
@@ -8514,6 +9153,18 @@ var RunSampler = class {
8514
9153
  }
8515
9154
  return this.profile(await this.leftoverBytes());
8516
9155
  }
9156
+ /**
9157
+ * What a live run's memory is drawn against: the machine's memory, or its
9158
+ * container's limit when lower, read once, and how the run's memory is
9159
+ * measured so far.
9160
+ */
9161
+ liveMemory() {
9162
+ if (this.capacityBytes === void 0) {
9163
+ const machine = memoryOf(this.linux ? this.cgroup.read() : null);
9164
+ this.capacityBytes = machine.memoryLimitBytes ?? machine.memoryBytes;
9165
+ }
9166
+ return { capacityBytes: this.capacityBytes || null, kind: this.memoryKind };
9167
+ }
8517
9168
  /** The series read so far, each point at the time it was read. */
8518
9169
  timedSeries() {
8519
9170
  return {
@@ -8810,9 +9461,6 @@ var RunSampler = class {
8810
9461
  if (cpuPressure === null) notMeasured.push("CPU pressure");
8811
9462
  const container = this.linux ? this.cgroup.read() : null;
8812
9463
  const startContainer = this.containerStart;
8813
- const totalMemory = os8.totalmem();
8814
- const constrained = typeof process.constrainedMemory === "function" ? process.constrainedMemory() : void 0;
8815
- const limit = container?.memoryLimitBytes ?? (constrained && constrained > 0 ? constrained : null);
8816
9464
  const raisedPeak = container?.memoryPeakBytes != null && startContainer?.memoryPeakBytes != null && container.memoryPeakBytes > startContainer.memoryPeakBytes ? container.memoryPeakBytes : null;
8817
9465
  const delta = (to, from) => to != null && from != null ? Math.max(0, to - from) : null;
8818
9466
  return {
@@ -8820,8 +9468,7 @@ var RunSampler = class {
8820
9468
  wallMs,
8821
9469
  machine: {
8822
9470
  cores: typeof os8.availableParallelism === "function" ? os8.availableParallelism() : os8.cpus().length,
8823
- memoryBytes: totalMemory,
8824
- memoryLimitBytes: limit !== null && limit < totalMemory ? limit : null,
9471
+ ...memoryOf(container),
8825
9472
  cpuQuotaCores: container?.cpuQuotaCores ?? null
8826
9473
  },
8827
9474
  cpu: {
@@ -9093,7 +9740,7 @@ function openPagesByWorker(censuses) {
9093
9740
  }
9094
9741
  return out;
9095
9742
  }
9096
- function pagesOverTime(censuses) {
9743
+ function pagesOverTime(censuses, live = false) {
9097
9744
  const byWorker = /* @__PURE__ */ new Map();
9098
9745
  for (const census of censuses) {
9099
9746
  const list = byWorker.get(census.worker) ?? [];
@@ -9126,7 +9773,7 @@ function pagesOverTime(censuses) {
9126
9773
  if (last && last[0] === at) last[1] = open;
9127
9774
  else events.push([at, open]);
9128
9775
  }
9129
- if (open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
9776
+ if (!live && open > 0 && lastAt >= events[events.length - 1][0]) events.push([lastAt, 0]);
9130
9777
  out.push({ worker, events });
9131
9778
  }
9132
9779
  return out;
@@ -9158,7 +9805,7 @@ function resourceTimeline(samples, censuses) {
9158
9805
  let startedAt = hasSamples ? samples.startedAt : Infinity;
9159
9806
  for (const { events } of workers) startedAt = Math.min(startedAt, events[0][0]);
9160
9807
  const shift = hasSamples ? samples.startedAt - startedAt : 0;
9161
- 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]);
9162
9809
  const trees = hasSamples ? workerTreesByIndex(samples, censuses) : [];
9163
9810
  const perWorker = Math.max(
9164
9811
  60,
@@ -9166,17 +9813,17 @@ function resourceTimeline(samples, censuses) {
9166
9813
  );
9167
9814
  return {
9168
9815
  startedAt,
9169
- cpuPct: hasSamples ? downsamplePoints(relative11(samples.cpuPct, shift), TIMELINE_SERIES_CAP, "mean") : [],
9170
- 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") : [],
9171
9818
  pages: workers.map(({ worker, events }) => {
9172
- const points = relative11(events, -startedAt);
9819
+ const points = relative12(events, -startedAt);
9173
9820
  const last = points.pop();
9174
9821
  return { worker, points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last] };
9175
9822
  }),
9176
9823
  workers: trees.map((tree) => ({
9177
9824
  worker: tree.worker,
9178
- cpuCores: downsamplePoints(relative11(tree.cpuCores, shift), perWorker, "mean"),
9179
- 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")
9180
9827
  }))
9181
9828
  };
9182
9829
  }
@@ -9195,6 +9842,144 @@ function resourceReportWire(input) {
9195
9842
  };
9196
9843
  }
9197
9844
 
9845
+ // src/internal/streaming/live-resources.ts
9846
+ var LIVE_INTERVAL_MS = 5e3;
9847
+ function since(points, cursor, cap, merge) {
9848
+ const fresh = cursor === void 0 ? points : points.filter(([at]) => at > cursor);
9849
+ return downsamplePoints(fresh, cap, merge);
9850
+ }
9851
+ var LiveResources = class {
9852
+ constructor(stream, httpClient, options, logger = new Logger()) {
9853
+ this.stream = stream;
9854
+ this.httpClient = httpClient;
9855
+ this.options = options;
9856
+ this.logger = logger;
9857
+ this.timer = null;
9858
+ this.inFlight = null;
9859
+ this.stopped = false;
9860
+ /** Whether the next post must carry every point: the first one, and after the dashboard lost them. */
9861
+ this.reset = true;
9862
+ /** The time of the last point the dashboard took, per series. */
9863
+ this.sent = /* @__PURE__ */ new Map();
9864
+ /** How many censuses the open pages last posted came from. */
9865
+ this.censusCount = 0;
9866
+ }
9867
+ /** Post on a timer that never keeps the process alive. */
9868
+ start() {
9869
+ if (this.timer || this.stopped) return;
9870
+ this.timer = setInterval(() => this.tick(), this.options.intervalMs ?? LIVE_INTERVAL_MS);
9871
+ this.timer.unref?.();
9872
+ }
9873
+ /** Post nothing more, and wait for a post in flight. */
9874
+ async stop() {
9875
+ this.stopped = true;
9876
+ if (this.timer) clearInterval(this.timer);
9877
+ this.timer = null;
9878
+ await this.inFlight;
9879
+ }
9880
+ /** Post now, when the run is watched and no post is in flight: the dashboard says someone just opened it. */
9881
+ tick() {
9882
+ if (this.stopped || this.inFlight) return null;
9883
+ const { enabled, runId, token } = this.stream;
9884
+ if (!enabled || runId === null || !token || this.stream.isWatched !== true) return null;
9885
+ let draft;
9886
+ try {
9887
+ draft = this.draft();
9888
+ } catch (error) {
9889
+ this.logger.debug(`Live resources not read: ${errorMessage(error)}`);
9890
+ return null;
9891
+ }
9892
+ if (!draft) return null;
9893
+ this.inFlight = this.send(runId, token, draft).finally(() => {
9894
+ this.inFlight = null;
9895
+ });
9896
+ return this.inFlight;
9897
+ }
9898
+ /** What is new since the last post the dashboard took; null when nothing is. */
9899
+ draft() {
9900
+ const samples = this.options.sampler?.timedSeries() ?? null;
9901
+ const startedAt = samples?.startedAt || this.options.runStartedAt;
9902
+ const censuses = this.options.censuses();
9903
+ const cursors = this.reset ? /* @__PURE__ */ new Map() : this.sent;
9904
+ const sent2 = new Map(cursors);
9905
+ let fresh = false;
9906
+ const take = (key, points, cap, merge) => {
9907
+ const out = since(points, cursors.get(key), cap, merge);
9908
+ const last = points[points.length - 1];
9909
+ if (out.length > 0 && last) {
9910
+ sent2.set(key, last[0]);
9911
+ fresh = true;
9912
+ }
9913
+ return out;
9914
+ };
9915
+ const timeline = { startedAt, cpuPct: [], memoryBytes: [], pages: [], workers: [] };
9916
+ if (samples) {
9917
+ timeline.cpuPct = take("cpu", samples.cpuPct, TIMELINE_SERIES_CAP, "mean");
9918
+ timeline.memoryBytes = take("memory", samples.memoryBytes, TIMELINE_SERIES_CAP, "max");
9919
+ for (const tree of workerTreesByIndex(samples, censuses)) {
9920
+ const cpuCores = take(`cores:${tree.worker}`, tree.cpuCores, TIMELINE_WORKER_POINTS_CAP, "mean");
9921
+ const memoryBytes = take(`bytes:${tree.worker}`, tree.memoryBytes, TIMELINE_WORKER_POINTS_CAP, "max");
9922
+ if (cpuCores.length > 0 || memoryBytes.length > 0) {
9923
+ timeline.workers.push({ worker: tree.worker, cpuCores, memoryBytes });
9924
+ }
9925
+ }
9926
+ }
9927
+ if (this.reset || censuses.length !== this.censusCount) {
9928
+ for (const { worker, events } of pagesOverTime(censuses, true)) {
9929
+ const points = events.map(([at, open]) => [at - startedAt, open]);
9930
+ const last = points.pop();
9931
+ timeline.pages.push({
9932
+ worker,
9933
+ points: [...downsamplePoints(points, TIMELINE_WORKER_POINTS_CAP - 1, "max"), last]
9934
+ });
9935
+ }
9936
+ fresh ||= timeline.pages.length > 0;
9937
+ }
9938
+ if (!fresh) return null;
9939
+ const memory = this.options.sampler?.liveMemory() ?? { capacityBytes: null, kind: null };
9940
+ return {
9941
+ post: {
9942
+ shardIndex: this.options.shardIndex,
9943
+ reset: this.reset,
9944
+ timeline,
9945
+ memoryCapacityBytes: memory.capacityBytes,
9946
+ memoryKind: memory.kind
9947
+ },
9948
+ sent: sent2,
9949
+ censusCount: censuses.length
9950
+ };
9951
+ }
9952
+ async send(runId, token, draft) {
9953
+ try {
9954
+ const answer = await this.httpClient.postJSON(
9955
+ `/api/test-runs/${runId}/live-resources`,
9956
+ { streamToken: token, ...draft.post },
9957
+ this.stream.auth
9958
+ );
9959
+ if (answer?.declined) {
9960
+ this.logger.debug("The project declined resources: live resources are not sent.");
9961
+ this.stopped = true;
9962
+ return;
9963
+ }
9964
+ this.stream.noteWatched(answer);
9965
+ if (answer?.resync) {
9966
+ this.reset = true;
9967
+ return;
9968
+ }
9969
+ this.sent = draft.sent;
9970
+ this.censusCount = draft.censusCount;
9971
+ this.reset = false;
9972
+ } catch (error) {
9973
+ if (error instanceof HttpError && error.status === 404) {
9974
+ this.logger.debug("The dashboard takes no live resources.");
9975
+ this.stopped = true;
9976
+ return;
9977
+ }
9978
+ this.logger.debug(`Live resources not sent: ${errorMessage(error)}`);
9979
+ }
9980
+ }
9981
+ };
9982
+
9198
9983
  // ../core/src/describe-failure.ts
9199
9984
  var HEADLINE_MAX_CHARS = 120;
9200
9985
  var VALUE_MAX_CHARS = 40;
@@ -9592,8 +10377,8 @@ function detailOf(parsed2, headline) {
9592
10377
  case "assertion-timeout": {
9593
10378
  const received = parsed2.received;
9594
10379
  if (received && !/element\(s\) not found/.test(received)) {
9595
- const shown = truncateValue(receivedDisplay(parsed2), SHORT_VALUE_MAX_CHARS).slice(0, 8);
9596
- 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);
9597
10382
  }
9598
10383
  const state = parsed2.lastStateLine;
9599
10384
  if (!state || /^(?:waiting for |unexpected value )/.test(state)) return null;
@@ -9724,10 +10509,14 @@ function isServerGranularity(value) {
9724
10509
  return typeof value === "string" && SERVER_VALUES.has(value);
9725
10510
  }
9726
10511
  function coverageSettings(options) {
9727
- if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off" };
10512
+ if (options.collectPerformanceMetrics === false) return { browser: "off", server: "off", css: "off" };
9728
10513
  const browser = options.coverage?.browser === "functions" ? "functions" : options.coverage?.browser === "files" || options.captureCodeReach === true ? "files" : "off";
9729
10514
  const server = isServerGranularity(options.coverage?.server) ? options.coverage.server : "off";
9730
- return { browser, server };
10515
+ const css = options.coverage?.css === "rules" ? "rules" : "off";
10516
+ return { browser, server, css };
10517
+ }
10518
+ function cssCoverageInWorker() {
10519
+ return process.env[PIWI_ENV_KEYS.coverageCss] === "rules";
9731
10520
  }
9732
10521
  function browserCoverageInWorker() {
9733
10522
  const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
@@ -9742,7 +10531,7 @@ function browserFunctionCoverage(files) {
9742
10531
  files: [...files].map(([path34, functions]) => ({ path: path34, runtime: "browser", functions }))
9743
10532
  });
9744
10533
  }
9745
- function newKnownFunctions(known, sent) {
10534
+ function newKnownFunctions(known, sent2) {
9746
10535
  const parts = [];
9747
10536
  let files = [];
9748
10537
  let budget = MAX_COVERAGE_RANGES;
@@ -9754,10 +10543,10 @@ function newKnownFunctions(known, sent) {
9754
10543
  for (const [raw, ranges] of known) {
9755
10544
  const path34 = normalizeCoveragePath(raw);
9756
10545
  if (path34 === null) continue;
9757
- let named = sent.get(path34);
10546
+ let named = sent2.get(path34);
9758
10547
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
9759
10548
  if (fresh.length === 0) continue;
9760
- if (!named) sent.set(path34, named = /* @__PURE__ */ new Set());
10549
+ if (!named) sent2.set(path34, named = /* @__PURE__ */ new Set());
9761
10550
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
9762
10551
  while (fresh.length > 0) {
9763
10552
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
@@ -9775,7 +10564,7 @@ function serverCoverageInWorker() {
9775
10564
  }
9776
10565
 
9777
10566
  // src/internal/capture/code-reach.ts
9778
- var crypto5 = __toESM(require("crypto"));
10567
+ var crypto6 = __toESM(require("crypto"));
9779
10568
  var fs23 = __toESM(require("fs"));
9780
10569
  var path23 = __toESM(require("path"));
9781
10570
  var import_node_child_process3 = require("child_process");
@@ -9847,10 +10636,10 @@ function joinRoot(root, source) {
9847
10636
  function decodeLines(raw) {
9848
10637
  const sources = [];
9849
10638
  const decoded = [];
9850
- const add = (map, lineOffset, columnOffset) => {
10639
+ const add2 = (map, lineOffset, columnOffset) => {
9851
10640
  if (map.sections) {
9852
10641
  for (const section of map.sections) {
9853
- if (!add(section.map, section.offset.line, section.offset.column)) return false;
10642
+ if (!add2(section.map, section.offset.line, section.offset.column)) return false;
9854
10643
  }
9855
10644
  return true;
9856
10645
  }
@@ -9871,7 +10660,7 @@ function decodeLines(raw) {
9871
10660
  });
9872
10661
  return true;
9873
10662
  };
9874
- return add(raw, 0, 0) ? { sources, lines: decoded } : null;
10663
+ return add2(raw, 0, 0) ? { sources, lines: decoded } : null;
9875
10664
  }
9876
10665
  function compactSegments(lines) {
9877
10666
  let total = 0;
@@ -10047,7 +10836,7 @@ function functionLines(entry, map, text, all) {
10047
10836
  const read = textOf(entry, text);
10048
10837
  const ranges = executedFunctionRanges(entry, read?.length, all);
10049
10838
  if (!ranges.length || !read) return out;
10050
- const add = (source, range) => {
10839
+ const add2 = (source, range) => {
10051
10840
  const list = out.get(source);
10052
10841
  if (list) list.push(range);
10053
10842
  else out.set(source, [range]);
@@ -10057,7 +10846,7 @@ function functionLines(entry, map, text, all) {
10057
10846
  const start = offsetToPosition(starts, range.start);
10058
10847
  if (!map) {
10059
10848
  const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
10060
- add(-1, [start.line + 1, last.line + 1]);
10849
+ add2(-1, [start.line + 1, last.line + 1]);
10061
10850
  continue;
10062
10851
  }
10063
10852
  const at = map.originalAt(start.line, start.column);
@@ -10067,7 +10856,7 @@ function functionLines(entry, map, text, all) {
10067
10856
  if (first === null) continue;
10068
10857
  const header = first === at.line || first === at.line + 1 ? at.line : first;
10069
10858
  const lines = map.originalLinesBetween(at.source, start, end, header);
10070
- if (lines) add(at.source, [header + 1, lines[1] + 1]);
10859
+ if (lines) add2(at.source, [header + 1, lines[1] + 1]);
10071
10860
  }
10072
10861
  return out;
10073
10862
  }
@@ -10165,9 +10954,9 @@ function resolveSourcePath(source, roots) {
10165
10954
  const normalized = normalizeSourcePath(source);
10166
10955
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
10167
10956
  if (path23.isAbsolute(normalized) && isFile(normalized)) return path23.resolve(normalized);
10168
- const relative11 = normalized.replace(/^\/+/, "");
10957
+ const relative12 = normalized.replace(/^\/+/, "");
10169
10958
  for (const root of roots) {
10170
- const candidate = path23.resolve(root, relative11);
10959
+ const candidate = path23.resolve(root, relative12);
10171
10960
  if (isFile(candidate)) return candidate;
10172
10961
  }
10173
10962
  return null;
@@ -10202,7 +10991,7 @@ function relativeToRepo(file, repoRoot) {
10202
10991
  return path23.relative(repoRoot, file).split(path23.sep).join("/");
10203
10992
  }
10204
10993
  function scriptVersion(entry) {
10205
- return entry.hash ?? crypto5.createHash("sha1").update(entry.source ?? "").digest("hex");
10994
+ return entry.hash ?? crypto6.createHash("sha1").update(entry.source ?? "").digest("hex");
10206
10995
  }
10207
10996
  async function loadMap(entry, fetchMap) {
10208
10997
  const ref = entry.source ? sourceMappingUrl(entry.source) : null;
@@ -10261,7 +11050,7 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10261
11050
  if (listed) listed.push(...functions);
10262
11051
  else into.set(path34, [...functions]);
10263
11052
  };
10264
- const add = (file, functions) => addTo(files, file, functions);
11053
+ const add2 = (file, functions) => addTo(files, file, functions);
10265
11054
  const bases = viteBases(entries);
10266
11055
  const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
10267
11056
  const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
@@ -10288,14 +11077,14 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
10288
11077
  const text = textOf2(covered, resolution);
10289
11078
  if (resolution.lookup) {
10290
11079
  const reached = reachedFunctionLines(covered, resolution.lookup, text);
10291
- if (reached.size > 0) add(resolution.file, [...reached.values()].flat());
11080
+ if (reached.size > 0) add2(resolution.file, [...reached.values()].flat());
10292
11081
  } else if (scriptRan(covered, text)) {
10293
- add(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
11082
+ add2(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
10294
11083
  }
10295
11084
  } else if (resolution.kind === "map") {
10296
11085
  for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
10297
11086
  const file = mapFile(resolution, index);
10298
- if (file) add(file, functions);
11087
+ if (file) add2(file, functions);
10299
11088
  }
10300
11089
  }
10301
11090
  if (!withKnown || resolution.kind === "none") continue;
@@ -10374,10 +11163,13 @@ async function markCurrentDocument(page, cdp) {
10374
11163
  }
10375
11164
  function onPaused(page, capture, event) {
10376
11165
  const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
10377
- const read = leaving ? readCoverage(page, capture.cdp) : null;
11166
+ const read = leaving ? Promise.all([
11167
+ capture.js ? readCoverage(page, capture.cdp) : Promise.resolve([]),
11168
+ capture.css ? readCssUsage(page, capture.cdp, capture.css) : Promise.resolve()
11169
+ ]) : null;
10378
11170
  if (read) {
10379
11171
  const merging = read.then(
10380
- (coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
11172
+ ([coverage]) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
10381
11173
  );
10382
11174
  capture.reading.add(merging);
10383
11175
  void merging.finally(() => capture.reading.delete(merging));
@@ -10391,7 +11183,7 @@ function versionKey(url, hash) {
10391
11183
  return `${hash} ${url}`;
10392
11184
  }
10393
11185
  function onScriptParsed(page, capture, event) {
10394
- if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
11186
+ if (!capture.js || !event.url || REPLAYED_SCRIPT.test(event.url)) return;
10395
11187
  capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
10396
11188
  if (NEVER_REACHED.test(event.url)) return;
10397
11189
  const key = versionKey(event.url, event.hash);
@@ -10420,6 +11212,53 @@ function readCoverage(page, cdp) {
10420
11212
  );
10421
11213
  return Promise.race([taken, late]).finally(() => clearTimeout(timer));
10422
11214
  }
11215
+ function onStyleSheetAdded(page, cdp, css, pageUrl, header) {
11216
+ if (header.isConstructed) return;
11217
+ const sheet = {
11218
+ sourceURL: header.sourceURL,
11219
+ ...header.sourceMapURL ? { sourceMapURL: header.sourceMapURL } : {},
11220
+ isInline: header.isInline,
11221
+ startLine: header.startLine,
11222
+ pageUrl,
11223
+ used: /* @__PURE__ */ new Map()
11224
+ };
11225
+ css.sheets.set(header.styleSheetId, sheet);
11226
+ const reading = (async () => {
11227
+ try {
11228
+ if (header.ownerNode !== void 0 && !/\.css(?:[?#]|$)/i.test(header.sourceURL)) {
11229
+ const { node } = await internalCall(
11230
+ page,
11231
+ () => cdp.send("DOM.describeNode", { backendNodeId: header.ownerNode })
11232
+ );
11233
+ const attributes = node.attributes ?? [];
11234
+ const at = attributes.indexOf("data-vite-dev-id");
11235
+ if (at >= 0 && typeof attributes[at + 1] === "string") sheet.devId = attributes[at + 1];
11236
+ }
11237
+ if (/\/node_modules\//.test(sheet.devId ?? header.sourceURL)) return;
11238
+ const { text } = await internalCall(
11239
+ page,
11240
+ () => cdp.send("CSS.getStyleSheetText", { styleSheetId: header.styleSheetId })
11241
+ );
11242
+ sheet.text = text;
11243
+ } catch {
11244
+ }
11245
+ })();
11246
+ css.reading.add(reading);
11247
+ void reading.finally(() => css.reading.delete(reading));
11248
+ }
11249
+ async function readCssUsage(page, cdp, css) {
11250
+ try {
11251
+ const { coverage } = await internalCall(page, () => cdp.send("CSS.takeCoverageDelta"));
11252
+ for (const rule of coverage) {
11253
+ if (!rule.used) continue;
11254
+ css.sheets.get(rule.styleSheetId)?.used.set(`${rule.startOffset}:${rule.endOffset}`, {
11255
+ startOffset: rule.startOffset,
11256
+ endOffset: rule.endOffset
11257
+ });
11258
+ }
11259
+ } catch {
11260
+ }
11261
+ }
10423
11262
  function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
10424
11263
  for (const covered of read) {
10425
11264
  const script = scripts.get(covered.scriptId);
@@ -10454,11 +11293,15 @@ async function detach(page, cdp) {
10454
11293
  }
10455
11294
  async function startCodeReach(page) {
10456
11295
  if (!isChromiumPage(page) || captures.has(page)) return false;
11296
+ const withCss = cssCoverageInWorker();
11297
+ const js = browserCoverageInWorker() || !withCss;
10457
11298
  let cdp = null;
10458
11299
  try {
10459
11300
  cdp = await internalCall(page, () => page.context().newCDPSession(page));
10460
11301
  const capture = {
10461
11302
  cdp,
11303
+ js,
11304
+ css: withCss ? { sheets: /* @__PURE__ */ new Map(), reading: /* @__PURE__ */ new Set() } : null,
10462
11305
  scripts: /* @__PURE__ */ new Map(),
10463
11306
  sources: /* @__PURE__ */ new Map(),
10464
11307
  ran: /* @__PURE__ */ new Map(),
@@ -10468,6 +11311,9 @@ async function startCodeReach(page) {
10468
11311
  };
10469
11312
  const marks = marksOf(page);
10470
11313
  cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
11314
+ const css = capture.css;
11315
+ if (css)
11316
+ cdp.on("CSS.styleSheetAdded", (event) => onStyleSheetAdded(page, cdp, css, capture.pageUrl, event.header));
10471
11317
  cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
10472
11318
  cdp.on("Page.frameNavigated", (event) => {
10473
11319
  if (event.frame.parentId) return;
@@ -10478,8 +11324,17 @@ async function startCodeReach(page) {
10478
11324
  page,
10479
11325
  () => Promise.all([
10480
11326
  capture.cdp.send("Debugger.enable"),
10481
- capture.cdp.send("Profiler.enable"),
10482
- capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false }),
11327
+ ...js ? [
11328
+ capture.cdp.send("Profiler.enable"),
11329
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
11330
+ ] : [],
11331
+ // The style sheets' rule usage records nothing without the DOM and CSS domains. A failure there leaves the
11332
+ // scripts' coverage running.
11333
+ ...css ? [
11334
+ capture.cdp.send("DOM.enable").then(() => capture.cdp.send("CSS.enable")).then(() => capture.cdp.send("CSS.startRuleUsageTracking")).catch(() => {
11335
+ capture.css = null;
11336
+ })
11337
+ ] : [],
10483
11338
  capture.cdp.send("Page.enable"),
10484
11339
  capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
10485
11340
  capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
@@ -10493,18 +11348,26 @@ async function startCodeReach(page) {
10493
11348
  return false;
10494
11349
  }
10495
11350
  }
10496
- async function stopCodeReach(page) {
11351
+ async function stopPageCoverage(page) {
10497
11352
  const capture = captures.get(page);
10498
- if (!capture) return null;
11353
+ if (!capture) return { scripts: null, sheets: [] };
10499
11354
  captures.delete(page);
10500
11355
  const { cdp } = capture;
10501
- try {
11356
+ const sheets = async () => {
11357
+ if (!capture.css) return [];
11358
+ await readCssUsage(page, cdp, capture.css);
11359
+ await Promise.all(capture.css.reading);
11360
+ await internalCall(page, () => cdp.send("CSS.stopRuleUsageTracking")).catch(() => {
11361
+ });
11362
+ return [...capture.css.sheets.values()].filter((sheet) => sheet.used.size > 0 || sheet.text !== void 0);
11363
+ };
11364
+ const scripts = async () => {
11365
+ if (!capture.js) return null;
10502
11366
  const last = await internalCall(page, async () => {
10503
11367
  const taken = await cdp.send("Profiler.takePreciseCoverage");
10504
11368
  await cdp.send("Profiler.stopPreciseCoverage");
10505
11369
  return taken.result;
10506
11370
  });
10507
- await Promise.all(capture.reading);
10508
11371
  mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
10509
11372
  const entries = [];
10510
11373
  for (const [key, { script, functions }] of capture.ran) {
@@ -10514,8 +11377,11 @@ async function stopCodeReach(page) {
10514
11377
  entries.push(entry);
10515
11378
  }
10516
11379
  return entries;
10517
- } catch {
10518
- return null;
11380
+ };
11381
+ try {
11382
+ await Promise.all(capture.reading);
11383
+ const [read, styles] = await Promise.all([scripts().catch(() => null), sheets().catch(() => [])]);
11384
+ return { scripts: read, sheets: styles };
10519
11385
  } finally {
10520
11386
  await detach(page, cdp);
10521
11387
  }
@@ -10523,11 +11389,11 @@ async function stopCodeReach(page) {
10523
11389
 
10524
11390
  // src/internal/collect/blob-ids.ts
10525
11391
  var import_node_child_process4 = require("child_process");
10526
- var crypto6 = __toESM(require("crypto"));
11392
+ var crypto7 = __toESM(require("crypto"));
10527
11393
  var fs24 = __toESM(require("fs"));
10528
11394
  var path24 = __toESM(require("path"));
10529
11395
  function gitBlobId(content) {
10530
- 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");
10531
11397
  }
10532
11398
  function namesLines(file) {
10533
11399
  return Boolean(file.functions?.length || file.lines?.length || file.executable?.length || file.allFunctions?.length);
@@ -10632,7 +11498,7 @@ var SETTLE_WAIT_MS = 15e3;
10632
11498
  var RUN_COVERAGE_FILES_PER_REQUEST = 5e3;
10633
11499
  var RUN_COVERAGE_BYTES_PER_REQUEST = 8 * 1024 * 1024;
10634
11500
  var INCOMPLETE = { files: [], incomplete: true };
10635
- function composeTestCoverage(server, browserFiles, settings) {
11501
+ function composeTestCoverage(server, browserFiles, settings, cssFiles) {
10636
11502
  const serverCapture = settings.server !== "off" && server ? reduceCoverage(
10637
11503
  {
10638
11504
  v: 1,
@@ -10650,13 +11516,25 @@ function composeTestCoverage(server, browserFiles, settings) {
10650
11516
  },
10651
11517
  settings.browser
10652
11518
  ) : null;
10653
- const merged = mergeCoverage(serverCapture, browserCapture);
11519
+ const wholeFiles = new Set(
11520
+ settings.browser === "functions" ? [] : (browserFiles ?? []).map((file) => typeof file === "string" ? file : file.path)
11521
+ );
11522
+ const cssCapture = settings.css === "rules" && cssFiles?.length ? reduceCoverage(
11523
+ {
11524
+ v: 1,
11525
+ files: cssFiles.map(
11526
+ (file) => wholeFiles.has(file.path) ? { path: file.path, runtime: "browser" } : { ...file, runtime: "browser" }
11527
+ )
11528
+ },
11529
+ "functions"
11530
+ ) : null;
11531
+ const merged = mergeCoverage(serverCapture, mergeCoverage(browserCapture, cssCapture));
10654
11532
  return merged ? sanitizeCoverage(merged) : null;
10655
11533
  }
10656
- function addRanges(into, filePath2, ranges) {
11534
+ function addRanges(into, filePath3, ranges) {
10657
11535
  if (ranges.length === 0) return;
10658
- let known = into.get(filePath2);
10659
- 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());
10660
11538
  for (const range of ranges) known.set(`${range[0]}:${range[1]}`, range);
10661
11539
  }
10662
11540
  function chunkRunCoverage(files) {
@@ -10710,7 +11588,7 @@ var CoverageCollection = class {
10710
11588
  this.browserKnown = /* @__PURE__ */ new Map();
10711
11589
  this.logger = options.logger ?? new Logger();
10712
11590
  this.waitMs = options.waitMs ?? TEST_COVERAGE_WAIT_MS;
10713
- this.browserBlobs = options.settings.browser === "functions" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
11591
+ this.browserBlobs = options.settings.browser === "functions" || options.settings.css === "rules" ? new BlobIds(() => repositoryRoot(options.configDir)) : null;
10714
11592
  const granularity = options.settings.server;
10715
11593
  this.collector = granularity === "off" ? null : new ServerCoverageCollector({
10716
11594
  granularity,
@@ -10727,28 +11605,41 @@ var CoverageCollection = class {
10727
11605
  /**
10728
11606
  * The browser side of a test's coverage: its code reach paths, or, at
10729
11607
  * `functions` granularity, the files of its `piwi-browser-coverage` attachment
10730
- * with their blob ids, once read. Never rejects.
11608
+ * with their blob ids, once read; with `coverage.css: 'rules'`, the style
11609
+ * sheet files of its `piwi-css-coverage` attachment too. Never rejects.
10731
11610
  */
10732
11611
  browserCoverage(testCase, attachments) {
10733
- if (this.options.settings.browser === "off") return void 0;
10734
- const paths = testCase.codeReach;
10735
- const attachment = this.browserBlobs ? attachments?.find((a) => a.name === ATTACHMENT_NAMES.browserCoverage) : void 0;
10736
- if (this.browserBlobs) {
10737
- for (const known of attachments ?? []) {
10738
- if (known.name === ATTACHMENT_NAMES.browserFunctions) this.addKnownFunctions(known);
10739
- }
10740
- }
10741
- if (!this.browserBlobs || !attachment?.body) return paths;
11612
+ const { browser, css } = this.options.settings;
11613
+ if (browser === "off" && css !== "rules") return {};
11614
+ const paths = browser === "off" ? void 0 : testCase.codeReach;
11615
+ for (const known of attachments ?? []) {
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);
11619
+ }
11620
+ const scripts = browser === "functions" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.browserCoverage) : [];
11621
+ const sheets = css === "rules" ? this.rangedFiles(attachments, ATTACHMENT_NAMES.cssCoverage) : [];
11622
+ const blobs = this.browserBlobs;
11623
+ if (!blobs || scripts.length === 0 && sheets.length === 0) return { files: paths };
11624
+ const withBlobs = (files) => blobs.withBlobs(files).catch(() => files);
11625
+ return Promise.all([
11626
+ scripts.length > 0 ? withBlobs(scripts) : Promise.resolve(null),
11627
+ sheets.length > 0 ? withBlobs(sheets) : Promise.resolve([])
11628
+ ]).then(([files, rules]) => ({ files: files ?? paths, css: rules }));
11629
+ }
11630
+ /** The browser files of a test's attachment holding line ranges, added to the run's. */
11631
+ rangedFiles(attachments, name) {
11632
+ const attachment = findPiwiAttachment(attachments, name);
11633
+ if (!attachment?.body) return [];
10742
11634
  let files = [];
10743
11635
  try {
10744
11636
  files = sanitizeCoverage(JSON.parse(attachment.body.toString("utf8")))?.files ?? [];
10745
11637
  } catch {
10746
- return paths;
11638
+ return [];
10747
11639
  }
10748
11640
  const browser = files.filter((file) => file.runtime === "browser");
10749
- if (browser.length === 0) return paths;
10750
11641
  for (const file of browser) addRanges(this.browserRan, file.path, file.functions ?? []);
10751
- return this.browserBlobs.withBlobs(browser).catch(() => browser);
11642
+ return browser;
10752
11643
  }
10753
11644
  /** Add the functions of a `piwi-browser-functions` attachment, one of a test's, to the run's. */
10754
11645
  addKnownFunctions(attachment) {
@@ -10762,8 +11653,8 @@ var CoverageCollection = class {
10762
11653
  const files = parsed2?.v === 1 ? parsed2.files : null;
10763
11654
  if (!Array.isArray(files)) return;
10764
11655
  for (const item of files) {
10765
- const filePath2 = normalizeCoveragePath(item?.path);
10766
- 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));
10767
11658
  }
10768
11659
  }
10769
11660
  /**
@@ -10774,13 +11665,13 @@ var CoverageCollection = class {
10774
11665
  async browserRunFiles() {
10775
11666
  if (!this.browserBlobs) return [];
10776
11667
  const paths = [.../* @__PURE__ */ new Set([...this.browserKnown.keys(), ...this.browserRan.keys()])].sort();
10777
- const files = paths.map((filePath2) => {
10778
- const functions = normalizeFunctionRanges([...this.browserRan.get(filePath2)?.values() ?? []]);
11668
+ const files = paths.map((filePath3) => {
11669
+ const functions = normalizeFunctionRanges([...this.browserRan.get(filePath3)?.values() ?? []]);
10779
11670
  const allFunctions = normalizeFunctionRanges([
10780
- ...this.browserKnown.get(filePath2)?.values() ?? [],
11671
+ ...this.browserKnown.get(filePath3)?.values() ?? [],
10781
11672
  ...functions
10782
11673
  ]);
10783
- const file = { path: filePath2, runtime: "browser" };
11674
+ const file = { path: filePath3, runtime: "browser" };
10784
11675
  if (functions.length > 0) file.functions = functions;
10785
11676
  if (allFunctions.length > 0) file.allFunctions = allFunctions;
10786
11677
  return file;
@@ -10799,15 +11690,15 @@ var CoverageCollection = class {
10799
11690
  */
10800
11691
  apply(testCase, attachments) {
10801
11692
  const browser = this.browserCoverage(testCase, attachments);
10802
- 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;
10803
11694
  const requests = attachment?.body ? parseServerCoverageRequests(attachment.body) : null;
10804
11695
  if (!this.collector || !requests) {
10805
11696
  if (!(browser instanceof Promise)) {
10806
- this.assign(testCase, composeTestCoverage(null, browser, this.options.settings));
11697
+ this.assign(testCase, composeTestCoverage(null, browser.files, this.options.settings, browser.css));
10807
11698
  return null;
10808
11699
  }
10809
11700
  const ready = browser.then(
10810
- (files) => this.assign(testCase, composeTestCoverage(null, files, this.options.settings))
11701
+ (side) => this.assign(testCase, composeTestCoverage(null, side.files, this.options.settings, side.css))
10811
11702
  );
10812
11703
  this.browserPending.add(ready);
10813
11704
  return ready.finally(() => this.browserPending.delete(ready));
@@ -10821,8 +11712,8 @@ var CoverageCollection = class {
10821
11712
  settled = true;
10822
11713
  clearTimeout(timer);
10823
11714
  this.pending.delete(entry);
10824
- void Promise.resolve(browser).then((files) => {
10825
- this.assign(testCase, composeTestCoverage(server, files, this.options.settings));
11715
+ void Promise.resolve(browser).then((side) => {
11716
+ this.assign(testCase, composeTestCoverage(server, side.files, this.options.settings, side.css));
10826
11717
  resolve11();
10827
11718
  });
10828
11719
  };
@@ -10959,6 +11850,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
10959
11850
  this.resourcesFile = null;
10960
11851
  /** Samples the run's processes and the machine from `onBegin` to `onEnd`. */
10961
11852
  this.sampler = null;
11853
+ /** Posts what the sampler and the censuses add while someone has the run open on the dashboard. */
11854
+ this.liveResources = null;
10962
11855
  /** Bytes of the files the tests attached, by kind. */
10963
11856
  this.artifactBytes = emptyArtifacts();
10964
11857
  /** The run's resource report for the dashboard, built in `onEnd`. */
@@ -11117,8 +12010,9 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11117
12010
  );
11118
12011
  }
11119
12012
  if (coverage.server !== "off") this.lastTestWatch = new LastTestWatch(this.plannedTests.length);
11120
- if (coverage.browser !== "off" || coverage.server !== "off") {
12013
+ if (coverage.browser !== "off" || coverage.server !== "off" || coverage.css === "rules") {
11121
12014
  if (coverage.browser !== "off") process.env[PIWI_ENV_KEYS.coverageBrowser] = coverage.browser;
12015
+ if (coverage.css === "rules") process.env[PIWI_ENV_KEYS.coverageCss] = coverage.css;
11122
12016
  if (coverage.server !== "off") {
11123
12017
  process.env[PIWI_ENV_KEYS.coverageServer] = coverage.server;
11124
12018
  }
@@ -11143,6 +12037,22 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11143
12037
  // the dashboard shows the real total from the first render.
11144
12038
  this.plannedTests.length
11145
12039
  );
12040
+ if (this.streamManager && this.options.captureResources !== false) {
12041
+ const live = new LiveResources(
12042
+ this.streamManager,
12043
+ this.httpClient,
12044
+ {
12045
+ sampler: this.sampler,
12046
+ censuses: () => this.resourceCensuses,
12047
+ shardIndex: this.shardInfo?.current ?? null,
12048
+ runStartedAt: Date.now()
12049
+ },
12050
+ this.logger
12051
+ );
12052
+ this.streamManager.onWatched = () => void live.tick();
12053
+ live.start();
12054
+ this.liveResources = live;
12055
+ }
11146
12056
  }
11147
12057
  /**
11148
12058
  * Settle each planned test's external ID before any test runs, name the IDs two tests share, and record them on
@@ -11326,7 +12236,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11326
12236
  if (frames.length) testCase.testSourceFrames = frames;
11327
12237
  }
11328
12238
  if (this.options.collectPerformanceMetrics && result.steps?.length > 0) {
11329
- const pauses = readPauses(result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.pauses)?.body);
12239
+ const pauses = readPauses(findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.pauses)?.body);
11330
12240
  testCase.performanceMetrics = collectStepMetrics(result.steps, result.errors, pauses);
11331
12241
  const stepEvents = extractTestStepEvents(result.steps, result.startTime);
11332
12242
  const waitEvents = extractWaitEvents(result.steps);
@@ -11336,14 +12246,14 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11336
12246
  if (this.options.collectPerformanceMetrics && result.attachments) {
11337
12247
  this.fileHandler.parsePerformanceAttachments(testCase, result.attachments);
11338
12248
  this.manifests?.note(testCase.networkRequests);
11339
- 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;
11340
12250
  if (locatorAttachment?.body) {
11341
12251
  try {
11342
12252
  testCase.locatorSnapshots = JSON.parse(locatorAttachment.body.toString());
11343
12253
  } catch {
11344
12254
  }
11345
12255
  }
11346
- 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;
11347
12257
  if (pagesAttachment?.body) {
11348
12258
  try {
11349
12259
  testCase.locatorPages = JSON.parse(pagesAttachment.body.toString());
@@ -11352,7 +12262,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11352
12262
  }
11353
12263
  }
11354
12264
  if (this.enabled && this.options.captureResources !== false) this.collectResources(test, result, testCase);
11355
- 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;
11356
12266
  if (reachAttachment?.body) {
11357
12267
  try {
11358
12268
  const files = JSON.parse(reachAttachment.body.toString());
@@ -11424,7 +12334,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11424
12334
  } catch {
11425
12335
  }
11426
12336
  }
11427
- const attachment = result.attachments?.find((a) => a.name === ATTACHMENT_NAMES.resources);
12337
+ const attachment = findPiwiAttachment(result.attachments, ATTACHMENT_NAMES.resources);
11428
12338
  const census = parseResourceCensus(attachment?.body);
11429
12339
  if (census) {
11430
12340
  this.resourceCensuses.push(census);
@@ -11445,6 +12355,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
11445
12355
  */
11446
12356
  async reportResources() {
11447
12357
  if (this.options.captureResources === false) return;
12358
+ await this.liveResources?.stop();
12359
+ this.liveResources = null;
11448
12360
  const profile = this.sampler ? await this.sampler.stop() : null;
11449
12361
  const samples = this.sampler?.timedSeries() ?? null;
11450
12362
  this.sampler = null;
@@ -12442,14 +13354,14 @@ function showAnchorPicker(arg) {
12442
13354
  } catch {
12443
13355
  shadowRoots.length = 0;
12444
13356
  }
12445
- const within = (node2, scope) => {
13357
+ const within2 = (node2, scope) => {
12446
13358
  if (scope === doc) return true;
12447
13359
  for (let n = node2; n; n = parentOf(n)) if (n === scope) return true;
12448
13360
  return false;
12449
13361
  };
12450
13362
  const queryAll = (scope, sel) => {
12451
13363
  const found = Array.from(scope.querySelectorAll(sel));
12452
- 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)));
12453
13365
  return found;
12454
13366
  };
12455
13367
  const leafCountable = !!leafTestId || roleOf2(el) === leafRole;
@@ -12530,8 +13442,8 @@ function showAnchorPicker(arg) {
12530
13442
  return;
12531
13443
  }
12532
13444
  const MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace";
12533
- const outline = doc.createElement("div");
12534
- 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);`;
12535
13447
  const outlineLabel = doc.createElement("div");
12536
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);`;
12537
13449
  const pickedOutline = doc.createElement("div");
@@ -12620,11 +13532,11 @@ function showAnchorPicker(arg) {
12620
13532
  line.appendChild(text);
12621
13533
  line.addEventListener("mouseenter", () => {
12622
13534
  const r = row.node.getBoundingClientRect();
12623
- outline.style.display = "block";
12624
- outline.style.left = r.left + "px";
12625
- outline.style.top = r.top + "px";
12626
- outline.style.width = r.width + "px";
12627
- 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";
12628
13540
  outlineLabel.textContent = `<${row.info.tag}> ${row.hookLabel}`;
12629
13541
  outlineLabel.style.display = "block";
12630
13542
  const lr = outlineLabel.getBoundingClientRect();
@@ -12634,7 +13546,7 @@ function showAnchorPicker(arg) {
12634
13546
  outlineLabel.style.top = top + "px";
12635
13547
  });
12636
13548
  line.addEventListener("mouseleave", () => {
12637
- outline.style.display = "none";
13549
+ outline2.style.display = "none";
12638
13550
  outlineLabel.style.display = "none";
12639
13551
  });
12640
13552
  box.addEventListener("change", () => {
@@ -12648,7 +13560,7 @@ function showAnchorPicker(arg) {
12648
13560
  const cleanup = () => {
12649
13561
  doc.removeEventListener("keydown", onKey, true);
12650
13562
  panel.remove();
12651
- outline.remove();
13563
+ outline2.remove();
12652
13564
  outlineLabel.remove();
12653
13565
  pickedOutline.remove();
12654
13566
  if (g.__piwiAnchorCleanup === cleanup) delete g.__piwiAnchorCleanup;
@@ -12713,7 +13625,7 @@ function showAnchorPicker(arg) {
12713
13625
  );
12714
13626
  }
12715
13627
  };
12716
- mount(pickedOutline, outline, outlineLabel, panel);
13628
+ mount(pickedOutline, outline2, outlineLabel, panel);
12717
13629
  }
12718
13630
 
12719
13631
  // ../picker-dom/src/overlay-confirm.ts
@@ -12817,7 +13729,7 @@ function showPauseBar(arg) {
12817
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;`;
12818
13730
  const where = doc.createElement("div");
12819
13731
  where.style.cssText = "font-weight:600;white-space:nowrap;";
12820
- 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}` : ""}`;
12821
13733
  const what = doc.createElement("code");
12822
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;";
12823
13735
  what.textContent = arg.locator ? `${arg.action} \xB7 ${arg.locator}` : arg.action;
@@ -12840,13 +13752,13 @@ function showPauseBar(arg) {
12840
13752
  delete g.__piwiPauseCleanup;
12841
13753
  };
12842
13754
  const buttons = [
12843
- ["Resume", "resume", "Run on to the next breakpoint (Esc)", true],
12844
- ["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]],
12845
13757
  ["Pick a locator", "pick", "Pick an element on the page and confirm a locator for it", false],
12846
13758
  ...arg.record ? [
12847
13759
  ["Record here", "record", "Record steps in this page: the editor writes them before this line", false]
12848
13760
  ] : [],
12849
- ["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]]
12850
13762
  ];
12851
13763
  for (const [label, choice, hint, primary] of buttons) {
12852
13764
  const btn = doc.createElement("button");
@@ -13011,7 +13923,7 @@ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "dat
13011
13923
  function generateAlternatives(attrs, options = {}) {
13012
13924
  const alts = [];
13013
13925
  const seen = /* @__PURE__ */ new Set();
13014
- const add = (loc) => {
13926
+ const add2 = (loc) => {
13015
13927
  if (!seen.has(loc.locator)) {
13016
13928
  seen.add(loc.locator);
13017
13929
  alts.push(loc);
@@ -13028,7 +13940,7 @@ function generateAlternatives(attrs, options = {}) {
13028
13940
  const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
13029
13941
  const testId = attr(attrs, testIdAttribute);
13030
13942
  if (testId && isUnique(counts?.testId)) {
13031
- add({
13943
+ add2({
13032
13944
  locator: `getByTestId(${quoteJsString(testId)})`,
13033
13945
  method: "getByTestId",
13034
13946
  args: { testId },
@@ -13039,7 +13951,7 @@ function generateAlternatives(attrs, options = {}) {
13039
13951
  const levelPart = level != null ? `, level: ${level}` : "";
13040
13952
  const withLevel = (base) => level != null ? { ...base, level } : base;
13041
13953
  if (role && accessibleName) {
13042
- add({
13954
+ add2({
13043
13955
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
13044
13956
  method: "getByRole",
13045
13957
  args: withLevel({ role, name: accessibleName }),
@@ -13048,7 +13960,7 @@ function generateAlternatives(attrs, options = {}) {
13048
13960
  }
13049
13961
  const ariaLabel = attr(attrs, "aria-label");
13050
13962
  if (role && ariaLabel && ariaLabel !== accessibleName) {
13051
- add({
13963
+ add2({
13052
13964
  locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
13053
13965
  method: "getByRole",
13054
13966
  args: withLevel({ role, name: ariaLabel }),
@@ -13059,7 +13971,7 @@ function generateAlternatives(attrs, options = {}) {
13059
13971
  const label = attr(attrs, "aria-label");
13060
13972
  const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
13061
13973
  if (labelBacked) {
13062
- add({
13974
+ add2({
13063
13975
  locator: `getByLabel(${quoteJsString(accessibleName)})`,
13064
13976
  method: "getByLabel",
13065
13977
  args: { label: accessibleName },
@@ -13069,7 +13981,7 @@ function generateAlternatives(attrs, options = {}) {
13069
13981
  }
13070
13982
  const placeholder = attr(attrs, "placeholder");
13071
13983
  if (placeholder) {
13072
- add({
13984
+ add2({
13073
13985
  locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
13074
13986
  method: "getByPlaceholder",
13075
13987
  args: { placeholder },
@@ -13077,7 +13989,7 @@ function generateAlternatives(attrs, options = {}) {
13077
13989
  });
13078
13990
  }
13079
13991
  if (text && text.length < 80) {
13080
- add({
13992
+ add2({
13081
13993
  locator: `getByText(${quoteJsString(text)})`,
13082
13994
  method: "getByText",
13083
13995
  args: { text },
@@ -13087,7 +13999,7 @@ function generateAlternatives(attrs, options = {}) {
13087
13999
  const id = attr(attrs, "id");
13088
14000
  if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
13089
14001
  const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
13090
- add({
14002
+ add2({
13091
14003
  locator: `locator(${quoteJsString(selector)})`,
13092
14004
  method: "locator",
13093
14005
  args: { selector },
@@ -13097,7 +14009,7 @@ function generateAlternatives(attrs, options = {}) {
13097
14009
  const name = attr(attrs, "name");
13098
14010
  if (name && selectable("name") && isUnique(counts?.name)) {
13099
14011
  const selector = `[name="${escCssAttrValue(name)}"]`;
13100
- add({
14012
+ add2({
13101
14013
  locator: `locator(${quoteJsString(selector)})`,
13102
14014
  method: "locator",
13103
14015
  args: { selector },
@@ -13106,7 +14018,7 @@ function generateAlternatives(attrs, options = {}) {
13106
14018
  }
13107
14019
  const alt = attr(attrs, "alt");
13108
14020
  if (alt) {
13109
- add({
14021
+ add2({
13110
14022
  locator: `getByAltText(${quoteJsString(alt)})`,
13111
14023
  method: "getByAltText",
13112
14024
  args: { text: alt },
@@ -13115,7 +14027,7 @@ function generateAlternatives(attrs, options = {}) {
13115
14027
  }
13116
14028
  const title = attr(attrs, "title");
13117
14029
  if (title) {
13118
- add({
14030
+ add2({
13119
14031
  locator: `getByTitle(${quoteJsString(title)})`,
13120
14032
  method: "getByTitle",
13121
14033
  args: { title },
@@ -13133,7 +14045,7 @@ function generateAlternatives(attrs, options = {}) {
13133
14045
  if (scopedCount(anc) !== 1) continue;
13134
14046
  if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
13135
14047
  testIdAnchorDone = true;
13136
- add({
14048
+ add2({
13137
14049
  locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
13138
14050
  method: leaf.method,
13139
14051
  args: { ...leaf.args, anchorTestId: anc.testId },
@@ -13143,7 +14055,7 @@ function generateAlternatives(attrs, options = {}) {
13143
14055
  if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
13144
14056
  idAnchorDone = true;
13145
14057
  const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
13146
- add({
14058
+ add2({
13147
14059
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
13148
14060
  method: leaf.method,
13149
14061
  args: { ...leaf.args, anchorSelector },
@@ -13154,7 +14066,7 @@ function generateAlternatives(attrs, options = {}) {
13154
14066
  dataAnchorDone = true;
13155
14067
  const { name: name2, value } = anc.dataAttr;
13156
14068
  const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
13157
- add({
14069
+ add2({
13158
14070
  locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
13159
14071
  method: leaf.method,
13160
14072
  args: { ...leaf.args, anchorSelector },
@@ -13164,7 +14076,7 @@ function generateAlternatives(attrs, options = {}) {
13164
14076
  const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
13165
14077
  if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
13166
14078
  roleAnchorDone = true;
13167
- add({
14079
+ add2({
13168
14080
  locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
13169
14081
  method: leaf.method,
13170
14082
  args: { ...leaf.args, anchorRole: ancestorRole },
@@ -13173,7 +14085,7 @@ function generateAlternatives(attrs, options = {}) {
13173
14085
  }
13174
14086
  if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
13175
14087
  filterAnchorDone = true;
13176
- add({
14088
+ add2({
13177
14089
  locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
13178
14090
  method: leaf.method,
13179
14091
  args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
@@ -13192,7 +14104,7 @@ function generateAlternatives(attrs, options = {}) {
13192
14104
  );
13193
14105
  const pos = attrs.rolePosition;
13194
14106
  if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
13195
- add({
14107
+ add2({
13196
14108
  locator: `getByRole(${rolePart})`,
13197
14109
  method: "getByRole",
13198
14110
  args: leafArgs,
@@ -13211,7 +14123,7 @@ function generateAlternatives(attrs, options = {}) {
13211
14123
  const countsClasses = counts?.classes;
13212
14124
  const classes = clsStr.split(/\s+/).filter((c) => c.length > 1 && /^[A-Za-z_-][A-Za-z0-9_-]*$/.test(c) && isUnique(countsClasses?.[c])).map((cls) => ({ cls, score: classifyCssStability(cls) })).sort((a, b) => b.score - a.score).slice(0, 3);
13213
14125
  for (const { cls, score } of classes) {
13214
- add({
14126
+ add2({
13215
14127
  locator: `locator(${quoteJsString(`.${cls}`)})`,
13216
14128
  method: "locator",
13217
14129
  args: { selector: `.${cls}` },
@@ -13268,7 +14180,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13268
14180
  const seen = /* @__PURE__ */ new Set();
13269
14181
  const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
13270
14182
  const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
13271
- const add = (l) => {
14183
+ const add2 = (l) => {
13272
14184
  if (!seen.has(l.locator)) {
13273
14185
  seen.add(l.locator);
13274
14186
  out.push(l);
@@ -13278,7 +14190,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13278
14190
  if (anchor.scopedLeafCount !== 1) continue;
13279
14191
  const seg = anchorSegment(anchor);
13280
14192
  if (!seg) continue;
13281
- add({
14193
+ add2({
13282
14194
  locator: `${seg.code}.getByRole(${rolePart})`,
13283
14195
  method: "getByRole",
13284
14196
  args: { ...leafArgs, ...seg.flatArgs },
@@ -13291,7 +14203,7 @@ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
13291
14203
  if (segments.every((s) => s !== null)) {
13292
14204
  const innermost = segments[segments.length - 1];
13293
14205
  const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
13294
- add({
14206
+ add2({
13295
14207
  locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
13296
14208
  method: "getByRole",
13297
14209
  // Flat args describe the innermost anchor (what the healing fingerprint
@@ -14254,11 +15166,31 @@ function parsePauseAt(value, cwd = process.cwd()) {
14254
15166
  return lines.size ? new PauseSet(lines) : NO_BREAKPOINTS;
14255
15167
  }
14256
15168
  var parsed = null;
15169
+ var sent = null;
14257
15170
  function currentPauseSet() {
15171
+ if (sent) return sent;
14258
15172
  const value = process.env[PIWI_LOCAL_DEBUG_ENV.pauseAt];
14259
15173
  if (!parsed || parsed.value !== value) parsed = { value, set: parsePauseAt(value) };
14260
15174
  return parsed.set;
14261
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
+ }
14262
15194
  function pauseGateFromTestInfo(testInfo) {
14263
15195
  const use = testInfo.project?.use ?? {};
14264
15196
  const debug = testInfo._configInternal?.configCLIOverrides?.debug;
@@ -14266,7 +15198,7 @@ function pauseGateFromTestInfo(testInfo) {
14266
15198
  }
14267
15199
  function pauseSkipReason(gate) {
14268
15200
  if (isCi(gate.ci)) return "running under CI \u2014 breakpoints pause local runs only";
14269
- if (gate.headless !== false)
15201
+ if (gate.headless !== false && !gate.channel)
14270
15202
  return "the browser is headless \u2014 re-run with --headed (or set use: { headless: false })";
14271
15203
  if (gate.pwdebug && !["0", "false", "console"].includes(gate.pwdebug))
14272
15204
  return "Playwright's Inspector runs the test (PWDEBUG) \u2014 run without --debug";
@@ -14274,20 +15206,42 @@ function pauseSkipReason(gate) {
14274
15206
  return `Playwright's debugger runs the test (--debug=${gate.debug}) \u2014 run without --debug`;
14275
15207
  return null;
14276
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
+ }
14277
15218
  function atStop(state, callSite) {
14278
- if (callSite && !state.finished) {
14279
- if (state.step) {
14280
- state.step = false;
14281
- 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 };
14282
15224
  }
14283
- if (state.set.matches(callSite)) return { show: "breakpoint" };
14284
15225
  }
14285
15226
  return { go: goOn(state) };
14286
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
+ }
14287
15241
  function goOn(state, answer) {
14288
15242
  if (answer === "step") state.step = true;
14289
15243
  else if (answer === "finish") state.finished = true;
14290
- return !state.finished && (state.step || state.set.size > 0) ? "next" : "resume";
15244
+ return wantsStops(state) ? "next" : "resume";
14291
15245
  }
14292
15246
  function contextDebugger(context) {
14293
15247
  const dbg = context.debugger;
@@ -14423,38 +15377,83 @@ function logPauseSkipOnce(reason) {
14423
15377
  var NAVIGATED = /Execution context was destroyed|Cannot find context with specified id/;
14424
15378
  var MAX_NAVIGATED = 10;
14425
15379
  var HIGHLIGHT_MS = 2e3;
14426
- async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14427
- 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;
14428
15425
  try {
14429
15426
  const highlight = point.target?.highlight;
14430
15427
  if (typeof highlight === "function") {
14431
- let timer;
14432
- shown = await Promise.race([
14433
- Promise.resolve().then(() => highlight.call(point.target)).catch(() => null),
14434
- new Promise((resolve11) => {
14435
- timer = setTimeout(() => resolve11(null), HIGHLIGHT_MS);
14436
- })
14437
- ]);
14438
- clearTimeout(timer);
15428
+ const highlighted = Promise.resolve().then(() => highlight.call(point.target)).catch(() => null);
15429
+ shown2 = await within(highlighted, HIGHLIGHT_MS) ?? null;
14439
15430
  }
14440
15431
  const arg = {
14441
15432
  place: pausePlace(point.location),
14442
15433
  attempt: point.attempt,
14443
15434
  action: point.call,
14444
15435
  locator: null,
14445
- record: !!onRecord
15436
+ record: !!onRecord,
15437
+ ...point.failure ? { failure: true } : {}
14446
15438
  };
14447
15439
  console.log(
14448
- `[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).`
14449
15441
  );
14450
15442
  let navigated = 0;
14451
15443
  for (; ; ) {
14452
15444
  try {
14453
15445
  if (ended?.aborted) return "resume";
14454
- await page.evaluate(showPauseBar, arg);
14455
- 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);
14456
15456
  if (ended?.aborted) return "resume";
14457
- const choice = await page.evaluate(takePauseState);
14458
15457
  navigated = 0;
14459
15458
  if (choice === "resume" || choice === "step" || choice === "finish") return choice;
14460
15459
  if (choice === "record" && onRecord) {
@@ -14464,31 +15463,290 @@ async function pauseHere(page, point, probe, onPick, onRecord, ended) {
14464
15463
  );
14465
15464
  continue;
14466
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
+ }
14467
15473
  if (choice !== "pick") continue;
14468
15474
  const picked = await pickLocator(page, probe, null, ended).catch(() => null);
14469
15475
  if (picked && !ended?.aborted) await onPick(picked);
14470
15476
  } catch (error) {
14471
15477
  if (page.isClosed() || !NAVIGATED.test(String(error)) || ++navigated > MAX_NAVIGATED) throw error;
14472
- await page.waitForLoadState("domcontentloaded").catch(() => void 0);
15478
+ await unlessEnded(
15479
+ page.waitForLoadState("domcontentloaded").catch(() => void 0),
15480
+ ended
15481
+ );
14473
15482
  }
14474
15483
  }
14475
15484
  } catch {
14476
15485
  return "resume";
14477
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('"')) {
14478
15574
  try {
14479
- if (ended?.aborted) await page.evaluate(takePauseState);
14480
- const dispose = shown?.dispose;
14481
- const hide = page.hideHighlight;
14482
- if (typeof dispose === "function") await dispose.call(shown);
14483
- else if (typeof hide === "function") await hide.call(page);
15575
+ return String(JSON.parse(value));
14484
15576
  } catch {
15577
+ return value;
14485
15578
  }
14486
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;
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
+ };
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 };
14487
15745
  }
14488
15746
 
14489
15747
  // src/internal/capture/record-handoff.ts
14490
15748
  var fs26 = __toESM(require("fs"));
14491
- var net = __toESM(require("net"));
15749
+ var net3 = __toESM(require("net"));
14492
15750
  var path29 = __toESM(require("path"));
14493
15751
 
14494
15752
  // ../core/src/ide-recorder.ts
@@ -14504,7 +15762,7 @@ function parseRecordChannel(value) {
14504
15762
  const path34 = text.slice(0, hash);
14505
15763
  return /^[A-Za-z0-9_-]{16,}$/.test(token) && !path34.includes("\0") ? { path: path34, token } : null;
14506
15764
  }
14507
- function isObject2(value) {
15765
+ function isObject3(value) {
14508
15766
  return typeof value === "object" && value !== null && !Array.isArray(value);
14509
15767
  }
14510
15768
  function parseHandoffToTest(line) {
@@ -14514,7 +15772,7 @@ function parseHandoffToTest(line) {
14514
15772
  } catch {
14515
15773
  return null;
14516
15774
  }
14517
- if (!isObject2(value)) return null;
15775
+ if (!isObject3(value)) return null;
14518
15776
  switch (value.type) {
14519
15777
  case "refused":
14520
15778
  return typeof value.message === "string" ? { type: "refused", message: value.message } : null;
@@ -14577,8 +15835,8 @@ async function recordFromHere(options) {
14577
15835
  const use = testInfo.project?.use ?? {};
14578
15836
  const context = page.context();
14579
15837
  return new Promise((resolve11) => {
14580
- const socket = net.connect(channel.path);
14581
- socket.setEncoding("utf8");
15838
+ const socket2 = net3.connect(channel.path);
15839
+ socket2.setEncoding("utf8");
14582
15840
  const answers = /* @__PURE__ */ new Map();
14583
15841
  let calls = 0;
14584
15842
  let attached = false;
@@ -14586,7 +15844,7 @@ async function recordFromHere(options) {
14586
15844
  let settled = false;
14587
15845
  let initScript;
14588
15846
  const send = (message) => {
14589
- if (!socket.destroyed) socket.write(`${JSON.stringify(message)}
15847
+ if (!socket2.destroyed) socket2.write(`${JSON.stringify(message)}
14590
15848
  `);
14591
15849
  };
14592
15850
  const onPage = () => send({ type: "notice", message: NEW_TAB_NOTICE });
@@ -14636,7 +15894,7 @@ async function recordFromHere(options) {
14636
15894
  page.off("close", onGone);
14637
15895
  page.off("crash", onGone);
14638
15896
  ended?.removeEventListener("abort", onEnded);
14639
- socket.end();
15897
+ socket2.end();
14640
15898
  resolve11(recording ? null : said);
14641
15899
  };
14642
15900
  const relayed = (source, payload) => new Promise((answer) => {
@@ -14676,7 +15934,7 @@ async function recordFromHere(options) {
14676
15934
  context.on("page", onPage);
14677
15935
  send({ type: "loaded" });
14678
15936
  };
14679
- socket.on("connect", () => {
15937
+ socket2.on("connect", () => {
14680
15938
  attached = true;
14681
15939
  send({
14682
15940
  type: "attach",
@@ -14685,10 +15943,11 @@ async function recordFromHere(options) {
14685
15943
  line: place.line,
14686
15944
  reason: options.reason,
14687
15945
  testIdAttribute: typeof use.testIdAttribute === "string" ? use.testIdAttribute : null,
14688
- 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 } : {}
14689
15948
  });
14690
15949
  });
14691
- socket.on(
15950
+ socket2.on(
14692
15951
  "data",
14693
15952
  lineReader(
14694
15953
  (line) => {
@@ -14721,11 +15980,516 @@ async function recordFromHere(options) {
14721
15980
  () => finish("the editor sent a message too long to read.")
14722
15981
  )
14723
15982
  );
14724
- socket.on("error", (error) => finish(`the editor could not be reached: ${error.message}`));
14725
- 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."));
14726
15985
  });
14727
15986
  }
14728
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
+
16232
+ // src/internal/capture/css-coverage.ts
16233
+ var crypto8 = __toESM(require("crypto"));
16234
+ var fs27 = __toESM(require("fs"));
16235
+
16236
+ // ../core/src/css-rules.ts
16237
+ var GROUPING = /* @__PURE__ */ new Set(["media", "supports", "layer", "container", "scope", "starting-style", "document"]);
16238
+ function parseCssRules(text) {
16239
+ const rules = [];
16240
+ const open = [];
16241
+ let preludeStart = -1;
16242
+ for (let i = 0; i < text.length; i++) {
16243
+ const c = text[i];
16244
+ if (c === "/" && text[i + 1] === "*") {
16245
+ const end = text.indexOf("*/", i + 2);
16246
+ i = end < 0 ? text.length : end + 1;
16247
+ continue;
16248
+ }
16249
+ if (c === '"' || c === "'") {
16250
+ if (preludeStart < 0) preludeStart = i;
16251
+ for (i++; i < text.length && text[i] !== c && text[i] !== "\n"; i++) if (text[i] === "\\") i++;
16252
+ continue;
16253
+ }
16254
+ if (c === "\\") {
16255
+ if (preludeStart < 0) preludeStart = i;
16256
+ i++;
16257
+ continue;
16258
+ }
16259
+ if ((c === "u" || c === "U") && /^url\(/i.test(text.slice(i, i + 4)) && !/[\w-]/.test(text[i - 1] ?? "")) {
16260
+ let j = i + 4;
16261
+ while (j < text.length && /\s/.test(text[j])) j++;
16262
+ if (text[j] !== '"' && text[j] !== "'") {
16263
+ if (preludeStart < 0) preludeStart = i;
16264
+ for (; j < text.length && text[j] !== ")"; j++) if (text[j] === "\\") j++;
16265
+ i = j;
16266
+ continue;
16267
+ }
16268
+ }
16269
+ if (text.startsWith("<!--", i) || text.startsWith("-->", i)) {
16270
+ i += text.startsWith("<!--", i) ? 3 : 2;
16271
+ continue;
16272
+ }
16273
+ if (c === "{") {
16274
+ const start = preludeStart < 0 ? i : preludeStart;
16275
+ const atRule = /^@([\w-]+)/.exec(text.slice(start, i));
16276
+ const parent = open[open.length - 1];
16277
+ const kind = atRule ? GROUPING.has(atRule[1].toLowerCase()) ? "group" : "other" : parent?.kind === "other" ? "other" : "style";
16278
+ open.push({ kind, start, open: i });
16279
+ preludeStart = -1;
16280
+ continue;
16281
+ }
16282
+ if (c === "}") {
16283
+ const block = open.pop();
16284
+ if (block && block.kind !== "other")
16285
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: i + 1 });
16286
+ preludeStart = -1;
16287
+ continue;
16288
+ }
16289
+ if (c === ";") {
16290
+ preludeStart = -1;
16291
+ continue;
16292
+ }
16293
+ if (preludeStart < 0 && !/\s/.test(c)) preludeStart = i;
16294
+ }
16295
+ for (const block of open) {
16296
+ if (block.kind !== "other")
16297
+ rules.push({ kind: block.kind, start: block.start, open: block.open, end: text.length });
16298
+ }
16299
+ return rules.sort((a, b) => a.start - b.start);
16300
+ }
16301
+ function usedCssRules(rules, used) {
16302
+ const styleAt = /* @__PURE__ */ new Map();
16303
+ const groups = [];
16304
+ for (const rule of rules) {
16305
+ if (rule.kind === "style") styleAt.set(rule.start, rule);
16306
+ else groups.push(rule);
16307
+ }
16308
+ const out = /* @__PURE__ */ new Map();
16309
+ for (const range of used) {
16310
+ const style = styleAt.get(range.startOffset);
16311
+ if (style) {
16312
+ out.set(`${style.start}`, [style.start, style.end]);
16313
+ continue;
16314
+ }
16315
+ const group = groups.find((rule) => rule.start <= range.startOffset && range.startOffset <= rule.open);
16316
+ if (group) out.set(`${group.start}`, [group.start, group.start + 1]);
16317
+ }
16318
+ return [...out.values()].sort((a, b) => a[0] - b[0]);
16319
+ }
16320
+ function allCssRules(rules) {
16321
+ return rules.map((rule) => rule.kind === "style" ? [rule.start, rule.end] : [rule.start, rule.start + 1]);
16322
+ }
16323
+ function cssRuleLines(text, offsets, options = {}) {
16324
+ const starts = lineStarts(text);
16325
+ const lineOffset = options.lineOffset ?? 0;
16326
+ const out = /* @__PURE__ */ new Map();
16327
+ const add2 = (source, range) => {
16328
+ const ranges = out.get(source);
16329
+ if (ranges) ranges.push(range);
16330
+ else out.set(source, [range]);
16331
+ };
16332
+ for (const [startOffset, endOffset] of offsets) {
16333
+ const start = offsetToPosition(starts, startOffset);
16334
+ const end = offsetToPosition(starts, Math.max(startOffset, endOffset - 1));
16335
+ if (!options.lookup) {
16336
+ add2(-1, [start.line + lineOffset + 1, end.line + lineOffset + 1]);
16337
+ continue;
16338
+ }
16339
+ const first = options.lookup.originalAt(start.line, start.column);
16340
+ if (!first) continue;
16341
+ const last = options.lookup.originalAt(end.line, end.column);
16342
+ const lastLine = last && last.source === first.source && last.line >= first.line ? last.line : first.line;
16343
+ add2(first.source, [first.line + 1, lastLine + 1]);
16344
+ }
16345
+ return out;
16346
+ }
16347
+
16348
+ // src/internal/capture/css-coverage.ts
16349
+ var STYLE_SOURCES = /\.(?:css|scss|sass|less|styl|stylus|pcss|postcss|vue|svelte|astro|html?)$/i;
16350
+ var BUILD_DIRS2 = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit)\//;
16351
+ var resolutions = /* @__PURE__ */ new Map();
16352
+ var MAX_CACHED_SHEETS = 2e3;
16353
+ function fileForStyleUrl(url, roots) {
16354
+ let pathname;
16355
+ try {
16356
+ const parsed2 = new URL(url);
16357
+ if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:" && parsed2.protocol !== "file:") return null;
16358
+ pathname = parsed2.pathname;
16359
+ } catch {
16360
+ return null;
16361
+ }
16362
+ return sourceFile(decodeURIComponent(pathname), roots);
16363
+ }
16364
+ function sourceFile(source, roots) {
16365
+ const bare = source.replace(/[?#].*$/, "");
16366
+ if (!STYLE_SOURCES.test(bare) || /(?:^|\/)node_modules\//.test(bare)) return null;
16367
+ const file = resolveSourcePath(bare, roots);
16368
+ if (!file || BUILD_DIRS2.test(file.replace(/\\/g, "/")) || /(?:^|[\\/])node_modules[\\/]/.test(file)) return null;
16369
+ return file;
16370
+ }
16371
+ function styleBlockLine(file, index) {
16372
+ let text;
16373
+ try {
16374
+ text = fs27.readFileSync(file, "utf-8");
16375
+ } catch {
16376
+ return 0;
16377
+ }
16378
+ const blocks = [...text.matchAll(/^<style\b[^>]*>/gim)];
16379
+ const block = blocks[index];
16380
+ if (!block) return 0;
16381
+ return text.slice(0, block.index + block[0].length).split("\n").length - 1;
16382
+ }
16383
+ async function sheetMap(sheet, text, fetchMap) {
16384
+ const ref = sourceMappingUrl(text) ?? sheet.sourceMapURL ?? null;
16385
+ if (!ref) return null;
16386
+ let raw;
16387
+ if (ref.startsWith("data:")) raw = decodeDataUrl(ref);
16388
+ else {
16389
+ try {
16390
+ raw = await fetchMap(new URL(ref, sheet.sourceURL).href);
16391
+ } catch {
16392
+ raw = null;
16393
+ }
16394
+ }
16395
+ if (!raw) return null;
16396
+ try {
16397
+ return decodeSourceMap(JSON.parse(raw));
16398
+ } catch {
16399
+ return null;
16400
+ }
16401
+ }
16402
+ function sameLines(text, rules, file, lineOffset) {
16403
+ let source;
16404
+ try {
16405
+ source = fs27.readFileSync(file, "utf-8");
16406
+ } catch {
16407
+ return false;
16408
+ }
16409
+ const fileLines = source.split(/\r?\n/);
16410
+ const sheetLineStarts = [0];
16411
+ for (let i = 0; i < text.length; i++) if (text[i] === "\n") sheetLineStarts.push(i + 1);
16412
+ let low = 0;
16413
+ for (const rule of rules) {
16414
+ while (low + 1 < sheetLineStarts.length && sheetLineStarts[low + 1] <= rule.start) low++;
16415
+ const head = text.slice(rule.start, rule.open).split("\n")[0].trim();
16416
+ if (head && !(fileLines[low + lineOffset] ?? "").includes(head)) return false;
16417
+ }
16418
+ return true;
16419
+ }
16420
+ async function resolveSheet(sheet, text, dirs, fetchMap) {
16421
+ const lookup = await sheetMap(sheet, text, fetchMap);
16422
+ const rules = parseCssRules(text);
16423
+ if (lookup) {
16424
+ const sources = lookup.sources.map((source) => {
16425
+ const direct = sourceFile(source, dirs);
16426
+ if (direct) return direct;
16427
+ try {
16428
+ return /^[a-z][\w+.-]*:/i.test(source) ? null : fileForStyleUrl(new URL(source, sheet.sourceURL).href, dirs);
16429
+ } catch {
16430
+ return null;
16431
+ }
16432
+ });
16433
+ return { rules, lookup, file: null, lineOffset: 0, sources, exact: true };
16434
+ }
16435
+ if (sheet.devId) {
16436
+ const file2 = sourceFile(sheet.devId, dirs);
16437
+ if (!file2) return null;
16438
+ const index = /[?&]index=(\d+)/.exec(sheet.devId);
16439
+ const lineOffset2 = /\.(?:vue|svelte|astro)$/i.test(file2) && index ? styleBlockLine(file2, Number(index[1])) : 0;
16440
+ return { rules, lookup: null, file: file2, lineOffset: lineOffset2, sources: [], exact: sameLines(text, rules, file2, lineOffset2) };
16441
+ }
16442
+ const file = fileForStyleUrl(sheet.sourceURL, dirs);
16443
+ if (!file) return null;
16444
+ const lineOffset = sheet.isInline ? sheet.startLine : 0;
16445
+ return { rules, lookup: null, file, lineOffset, sources: [], exact: sameLines(text, rules, file, lineOffset) };
16446
+ }
16447
+ function cachedResolution(sheet, text, dirs, fetchMap) {
16448
+ const version = crypto8.createHash("sha1").update(text).digest("hex");
16449
+ const key = [sheet.devId ?? sheet.sourceURL, sheet.isInline ? sheet.startLine : "", dirs.join("\0"), version].join(
16450
+ " "
16451
+ );
16452
+ let pending = resolutions.get(key);
16453
+ if (!pending) {
16454
+ pending = resolveSheet(sheet, text, dirs, fetchMap).catch(() => null);
16455
+ resolutions.set(key, pending);
16456
+ if (resolutions.size > MAX_CACHED_SHEETS) resolutions.delete(resolutions.keys().next().value);
16457
+ }
16458
+ return pending;
16459
+ }
16460
+ function add(into, file, ranges, repoRoot, fileLevel = false) {
16461
+ if (ranges.length === 0 && !fileLevel) return;
16462
+ const key = relativeToRepo(file, repoRoot);
16463
+ into.set(key, [...into.get(key) ?? [], ...ranges]);
16464
+ }
16465
+ async function resolveCssCoverage(sheets, roots, fetchMap) {
16466
+ const reached = /* @__PURE__ */ new Map();
16467
+ const known = /* @__PURE__ */ new Map();
16468
+ for (const sheet of sheets) {
16469
+ if (sheet.text === void 0) continue;
16470
+ const dirs = scriptRoots(roots, sheet.sourceURL, sheet.pageUrl);
16471
+ const resolution = await cachedResolution(sheet, sheet.text, dirs, fetchMap);
16472
+ if (!resolution) continue;
16473
+ const used = usedCssRules(resolution.rules, [...sheet.used.values()]);
16474
+ if (!resolution.exact) {
16475
+ if (resolution.file && used.length > 0) add(reached, resolution.file, [], roots.repoRoot, true);
16476
+ continue;
16477
+ }
16478
+ const options = { lookup: resolution.lookup, lineOffset: resolution.lineOffset };
16479
+ const fileOf2 = (source) => source === -1 ? resolution.file : resolution.sources[source] ?? null;
16480
+ const matched = cssRuleLines(sheet.text, used, options);
16481
+ for (const [source, ranges] of matched) {
16482
+ const file = fileOf2(source);
16483
+ if (file) add(reached, file, ranges, roots.repoRoot);
16484
+ }
16485
+ for (const [source, ranges] of cssRuleLines(sheet.text, allCssRules(resolution.rules), options)) {
16486
+ const file = fileOf2(source);
16487
+ if (file) add(known, file, ranges, roots.repoRoot);
16488
+ }
16489
+ }
16490
+ return { reached, known };
16491
+ }
16492
+
14729
16493
  // src/internal/capture/page-interaction.ts
14730
16494
  function readPageInteraction() {
14731
16495
  const doc = globalThis.document;
@@ -15024,21 +16788,29 @@ function createSink() {
15024
16788
  locatorCalls: /* @__PURE__ */ new Map(),
15025
16789
  stops: [],
15026
16790
  shownStops: /* @__PURE__ */ new Map(),
16791
+ failureHeld: false,
16792
+ frozenAt: null,
16793
+ stopEnds: 0,
16794
+ ending: false,
15027
16795
  released: [],
15028
16796
  releasedRequests: /* @__PURE__ */ new Set(),
15029
16797
  onReleasedRequestsEnded: null,
15030
16798
  releasedQuietSince: 0,
16799
+ releasedUntil: null,
15031
16800
  locatorPages: new LocatorPageLog(),
15032
- codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
16801
+ codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map(), cssFiles: /* @__PURE__ */ new Map(), cssKnown: /* @__PURE__ */ new Map() },
15033
16802
  serverCoverage: /* @__PURE__ */ new Map(),
15034
16803
  baseURL: null
15035
16804
  };
15036
16805
  }
15037
16806
  var MAX_CODE_REACH_PAGES = 16;
15038
16807
  var CODE_REACH_START_WAIT_MS = 2e3;
16808
+ function pageCoverageInWorker() {
16809
+ return browserCoverageInWorker() || cssCoverageInWorker();
16810
+ }
15039
16811
  function maybeStartCodeReach(page, alreadyOpen = false) {
15040
16812
  const sink = currentSink;
15041
- if (!sink || !browserCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
16813
+ if (!sink || !pageCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
15042
16814
  const known = sink.codeReach.sessions.get(page);
15043
16815
  if (known) return known.started;
15044
16816
  if (alreadyOpen) {
@@ -15051,7 +16823,7 @@ function maybeStartCodeReach(page, alreadyOpen = false) {
15051
16823
  return started;
15052
16824
  }
15053
16825
  async function startCodeReachOnOpenPages() {
15054
- if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
16826
+ if (!pageCoverageInWorker() || OPEN_PAGES.size === 0) return;
15055
16827
  const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
15056
16828
  await Promise.race([started, new Promise((resolve11) => setTimeout(resolve11, CODE_REACH_START_WAIT_MS).unref())]);
15057
16829
  }
@@ -15094,22 +16866,28 @@ async function stopSinkCodeReach(sink, closing) {
15094
16866
  async function stopPageCodeReach(sink, page, session, roots) {
15095
16867
  try {
15096
16868
  if (!await session.started || isPageClosed(page)) return;
15097
- const entries = await stopCodeReach(page);
15098
- if (!entries) return;
15099
- const { reached, known } = await resolveCodeReachFunctions(entries, roots, pageMapFetcher(page), page.url(), {
16869
+ const { scripts, sheets } = await stopPageCoverage(page);
16870
+ const fetchMap = pageMapFetcher(page);
16871
+ const append = (into, from) => {
16872
+ for (const [file, ranges] of from) into.set(file, [...into.get(file) ?? [], ...ranges]);
16873
+ };
16874
+ if (sheets.length > 0) {
16875
+ const rules = await resolveCssCoverage(sheets, roots, fetchMap);
16876
+ append(sink.codeReach.cssFiles, rules.reached);
16877
+ append(sink.codeReach.cssKnown, rules.known);
16878
+ }
16879
+ if (!scripts) return;
16880
+ const { reached, known } = await resolveCodeReachFunctions(scripts, roots, fetchMap, page.url(), {
15100
16881
  known: browserFunctionsInWorker()
15101
16882
  });
15102
- for (const [file, functions] of reached) {
15103
- sink.codeReach.files.set(file, [...sink.codeReach.files.get(file) ?? [], ...functions]);
15104
- }
15105
- for (const [file, functions] of known) {
15106
- sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
15107
- }
16883
+ append(sink.codeReach.files, reached);
16884
+ append(sink.codeReach.known, known);
15108
16885
  } catch {
15109
16886
  }
15110
16887
  }
15111
16888
  var currentSink = null;
15112
16889
  var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
16890
+ var KNOWN_CSS_RULES_SENT = /* @__PURE__ */ new Map();
15113
16891
  var PENDING_PROBES = /* @__PURE__ */ new Set();
15114
16892
  async function drainPendingProbes(capMs) {
15115
16893
  if (PENDING_PROBES.size === 0) return;
@@ -15122,6 +16900,25 @@ async function drainPendingProbes(capMs) {
15122
16900
  ]);
15123
16901
  clearTimeout(cap);
15124
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
+ }
15125
16922
  function isPageClosed(page) {
15126
16923
  try {
15127
16924
  return typeof page.isClosed === "function" ? page.isClosed() : false;
@@ -15244,7 +17041,7 @@ function buildPageState(raw, cookies, interaction) {
15244
17041
  }
15245
17042
  async function readPageState(page) {
15246
17043
  try {
15247
- const raw = await page.evaluate(() => {
17044
+ const readRaw = page.evaluate(() => {
15248
17045
  const listStorage = (s) => {
15249
17046
  const out = [];
15250
17047
  try {
@@ -15270,14 +17067,16 @@ async function readPageState(page) {
15270
17067
  sessionStorage: listStorage(globalThis.sessionStorage)
15271
17068
  };
15272
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]);
15273
17079
  if (!raw) return null;
15274
- let cookies = null;
15275
- try {
15276
- cookies = await pageContext(page)?.cookies() ?? null;
15277
- } catch {
15278
- cookies = null;
15279
- }
15280
- const interaction = await page.evaluate(readPageInteraction).catch(() => null);
15281
17080
  return buildPageState(raw, cookies, interaction);
15282
17081
  } catch {
15283
17082
  return null;
@@ -15341,7 +17140,7 @@ async function recordPageWindow(sink, page) {
15341
17140
  }
15342
17141
  async function stashPageState(sink, closing) {
15343
17142
  const page = sink.lastActivePage;
15344
- if (!page || isPageClosed(page)) return;
17143
+ if (!page || isPageClosed(page) || sink.frozenAt !== null) return;
15345
17144
  const belongsToClosing = closing.page === page || closing.context !== void 0 && pageContext(page) === closing.context;
15346
17145
  if (!belongsToClosing) return;
15347
17146
  await internalCall(page, () => readPageBeforeClose(sink, page));
@@ -15353,13 +17152,12 @@ async function readBlankPage(page) {
15353
17152
  return aria != null && aria.trim() === "";
15354
17153
  }
15355
17154
  async function readPageBeforeClose(sink, page) {
15356
- 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]);
15357
17158
  if (vitals) sink.stashedWebVitals = vitals;
15358
- if (sink.probeItem?.level === "server") sink.probeBlankPage = await readBlankPage(page);
15359
- if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
15360
- const pageState = await readPageState(page);
15361
- if (pageState) sink.stashedPageState = pageState;
15362
- }
17159
+ if (blankPage !== null) sink.probeBlankPage = blankPage;
17160
+ if (pageState) sink.stashedPageState = pageState;
15363
17161
  const status = sink.testInfo?.status;
15364
17162
  const sampleAria = async () => {
15365
17163
  const root = page.locator(":root");
@@ -15553,16 +17351,35 @@ function locatorSource(target, method, args) {
15553
17351
  return renderFailing({ method, args });
15554
17352
  }
15555
17353
  var PAGE_PAUSES = /* @__PURE__ */ new WeakMap();
17354
+ var CLOSING_CONTEXTS = /* @__PURE__ */ new WeakSet();
17355
+ var CLOSING_PAGES = /* @__PURE__ */ new WeakMap();
15556
17356
  var PLAYWRIGHT_PAUSES = /* @__PURE__ */ new WeakMap();
15557
- 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) {
15558
17374
  const state = currentSink?.pause;
15559
- if (!state || state.finished || state.set.size === 0) return;
17375
+ if (!state || !wantsStops(state)) return;
15560
17376
  await Promise.all(
15561
17377
  [...contexts].map(
15562
17378
  (context) => context && watchStops(context, onDebuggerStop) ? steerDebugger(context, "pause") : void 0
15563
17379
  )
15564
17380
  );
15565
17381
  }
17382
+ var stopTurn = Promise.resolve();
15566
17383
  function onDebuggerStop(context) {
15567
17384
  const details = contextDebugger(context)?.pausedDetails();
15568
17385
  if (!details || PLAYWRIGHT_PAUSES.has(context)) return;
@@ -15573,13 +17390,38 @@ function onDebuggerStop(context) {
15573
17390
  try {
15574
17391
  if (!sink || !state) return;
15575
17392
  const callSite = stopSite(sink, details.location);
17393
+ const call = callSite ? sink.locatorCalls.get(callSiteLine(callSite)) : void 0;
15576
17394
  const pagePause = PAGE_PAUSES.get(context);
17395
+ const asked = askedOnce(state);
15577
17396
  const decided = pagePause ? { show: "page-pause" } : atStop(state, callSite);
15578
17397
  if ("go" in decided) {
15579
17398
  how = decided.go;
15580
17399
  return;
15581
17400
  }
15582
- 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);
15583
17425
  } catch {
15584
17426
  if (state) how = goOn(state);
15585
17427
  } finally {
@@ -15587,22 +17429,30 @@ function onDebuggerStop(context) {
15587
17429
  }
15588
17430
  })();
15589
17431
  }
17432
+ function endedWaiting(sink, call) {
17433
+ releaseCall(sink, call?.settled ?? Promise.resolve());
17434
+ return "finish";
17435
+ }
15590
17436
  function stopSite(sink, location) {
15591
17437
  if (path30.dirname(path30.resolve(location.file)) !== __dirname) return stopCallSite(location);
15592
17438
  return inFlightCallSite([...sink.locatorCalls.values()].map((call) => call.location));
15593
17439
  }
15594
- function stopPage(sink, context, call) {
15595
- for (const page of [call?.page, sink.lastActivePage]) {
15596
- if (page && !isPageClosed(page) && pageContext(page) === context) return page;
15597
- }
15598
- 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);
15599
17445
  return open[open.length - 1] ?? null;
15600
17446
  }
15601
- async function showStop(sink, context, title, location, reason) {
17447
+ async function showStop(sink, context, title, location, reason, call, of, failure = null) {
15602
17448
  const testInfo = sink.testInfo;
15603
- const call = sink.locatorCalls.get(callSiteLine(location));
15604
- const page = stopPage(sink, context, call);
15605
- 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;
15606
17456
  const onPick = async (picked) => {
15607
17457
  if (call) {
15608
17458
  call.picked = true;
@@ -15613,16 +17463,57 @@ async function showStop(sink, context, title, location, reason) {
15613
17463
  ...picked
15614
17464
  });
15615
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);
15616
17473
  }
15617
17474
  console.log(`[piwi] Locator picked at ${callSiteLine(location)}: ${picked.picked.locator}`);
15618
- await postPickToEditor(process.env[PIWI_LOCAL_DEBUG_ENV.editorSend], pickPayload(picked.picked.locator, location));
17475
+ await postPickToEditor(editorSendAddress(), pickPayload(picked.picked.locator, location));
15619
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;
15620
17480
  const record = { title, location, reason, startedAt: Date.now(), duration: 0 };
15621
17481
  sink.stops.push(record);
15622
17482
  const end = new AbortController();
15623
- const shown = withTestTimeoutHeld(
15624
- testInfo,
15625
- () => 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(
15626
17517
  page,
15627
17518
  () => pauseHere(
15628
17519
  page,
@@ -15630,45 +17521,221 @@ async function showStop(sink, context, title, location, reason) {
15630
17521
  location,
15631
17522
  call: title,
15632
17523
  attempt: testInfo.retry > 0 ? testInfo.retry + 1 : null,
15633
- target: call?.page === page ? call.target : null
17524
+ target: call?.page === page ? call.target : null,
17525
+ ...failure ? { failure: true } : {}
15634
17526
  },
15635
17527
  { fn: probeElementAttrs, arg: CAPTURED_ATTRS_ARG },
15636
17528
  onPick,
15637
- recorderAt(page, testInfo, location, reason === "page-pause" ? "pause" : "breakpoint"),
15638
- end.signal
17529
+ onRecord,
17530
+ over.signal,
17531
+ intents ?? void 0,
17532
+ onIntent
15639
17533
  )
15640
- )
15641
- ).finally(() => {
17534
+ );
17535
+ }).finally(async () => {
15642
17536
  if (!end.signal.aborted) record.duration = Date.now() - record.startedAt;
15643
17537
  else if (end.signal.reason === "ended") releaseCall(sink, call?.settled ?? Promise.resolve());
15644
17538
  sink.shownStops.delete(end);
17539
+ await held2?.release();
15645
17540
  });
15646
17541
  const heldAt = testTimeSlot(testInfo)?.elapsed ?? null;
15647
- sink.shownStops.set(end, { page, record, done: shown.catch(() => void 0), heldAt });
15648
- const answer = await shown;
15649
- if (!end.signal.aborted) return answer;
15650
- 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
+ });
15651
17710
  }
15652
17711
  var END_STOP_MS = 2e3;
15653
17712
  var RELEASED_CALL_MS = 2e3;
15654
17713
  var RELEASED_QUIET_MS = 200;
15655
17714
  async function endShownStops(sink, why, closing) {
15656
17715
  const ending = [];
17716
+ if (why === "ended") sink.stopEnds++;
15657
17717
  let heldAt = Infinity;
15658
- for (const [end, { page, record, done, heldAt: stopHeldAt }] of sink.shownStops) {
15659
- 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;
15660
17720
  if (!end.signal.aborted) {
15661
- 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;
15662
17723
  end.abort(why);
15663
- if (why === "ended" && stopHeldAt !== null) heldAt = Math.min(heldAt, stopHeldAt);
17724
+ if (why === "ended" && stop.heldAt !== null) heldAt = Math.min(heldAt, stop.heldAt);
15664
17725
  }
15665
- ending.push(done);
17726
+ ending.push(stop.done);
15666
17727
  }
15667
17728
  if (sink.testInfo && heldAt !== Infinity) restoreTestTime(sink.testInfo, heldAt);
15668
17729
  await settledWithin(ending, END_STOP_MS);
15669
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
+ }
15670
17736
  function releaseCall(sink, settled) {
15671
17737
  sink.releasedQuietSince = Date.now();
17738
+ sink.releasedUntil = null;
15672
17739
  sink.released.push(settled.then(() => void (sink.releasedQuietSince = Date.now())));
15673
17740
  }
15674
17741
  function releasedRequestsEnded(sink) {
@@ -15677,8 +17744,8 @@ function releasedRequestsEnded(sink) {
15677
17744
  }
15678
17745
  async function awaitReleasedCalls(sink) {
15679
17746
  if (sink.released.length === 0) return;
15680
- const deadline = Date.now() + RELEASED_CALL_MS;
15681
- 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()));
15682
17749
  for (let now = Date.now(); now < deadline; now = Date.now()) {
15683
17750
  if (sink.releasedRequests.size > 0) {
15684
17751
  await settledWithin([releasedRequestsEnded(sink)], deadline - now);
@@ -15708,7 +17775,7 @@ function recorderAt(page, testInfo, location, reason) {
15708
17775
  return async (ended) => {
15709
17776
  console.log(`[piwi] Recording from ${callSiteLine(location)}: the editor writes the steps.`);
15710
17777
  const why = await recordFromHere({ page, testInfo, location, reason, ended });
15711
- 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.");
15712
17779
  else
15713
17780
  console.log(
15714
17781
  why ? `[piwi] Nothing was recorded: ${why}` : "[piwi] The recording ended: the test is paused again."
@@ -15725,10 +17792,10 @@ async function pauseInPlace(page, playwrightPause, location) {
15725
17792
  if (!sink.pause && (!canRecordFromHere() || pauseSkipReason(pauseGateFromTestInfo(testInfo)))) {
15726
17793
  return leaveToPlaywright(context, playwrightPause);
15727
17794
  }
15728
- sink.pause ??= { set: NO_BREAKPOINTS, step: false, finished: false };
17795
+ sink.pause ??= pauseState(NO_BREAKPOINTS);
15729
17796
  watchStops(context, onDebuggerStop);
15730
17797
  await steerDebugger(context, "pause");
15731
- PAGE_PAUSES.set(context, location);
17798
+ PAGE_PAUSES.set(context, { location, page });
15732
17799
  try {
15733
17800
  await playwrightPause({ __testHookKeepTestTimeout: true });
15734
17801
  } finally {
@@ -15911,15 +17978,15 @@ function instrumentPage(page) {
15911
17978
  if (typeof page.close === "function") {
15912
17979
  const originalClose = page.close.bind(page);
15913
17980
  page.close = async (...args) => {
15914
- await drainPendingProbes(1e3);
15915
- const sink = currentSink;
15916
- if (sink) {
15917
- await endShownStops(sink, "closed", { page });
15918
- await stashPageState(sink, { page });
15919
- await maybeOpenPicker(sink, { page });
15920
- 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();
15921
17989
  }
15922
- return originalClose(...args);
15923
17990
  };
15924
17991
  }
15925
17992
  const captureLocators = process.env.PIWI_CAPTURE_LOCATORS !== "false";
@@ -15964,7 +18031,7 @@ function instrumentPage(page) {
15964
18031
  });
15965
18032
  page.on("console", (msg) => {
15966
18033
  const sink = currentSink;
15967
- if (!sink) return;
18034
+ if (!sink || sink.frozenAt !== null) return;
15968
18035
  const type = msg.type();
15969
18036
  if (["warning", "error", "assert"].includes(type)) {
15970
18037
  const location = msg.location();
@@ -15980,7 +18047,7 @@ function instrumentPage(page) {
15980
18047
  try {
15981
18048
  page.on("dialogclosed", (dialog) => {
15982
18049
  const sink = currentSink;
15983
- if (!sink) return;
18050
+ if (!sink || sink.frozenAt !== null) return;
15984
18051
  try {
15985
18052
  sink.dialogs.push({
15986
18053
  type: typeof dialog.type === "function" ? dialog.type() : null,
@@ -15999,12 +18066,14 @@ function instrumentPage(page) {
15999
18066
  if (!sink) return;
16000
18067
  try {
16001
18068
  const request3 = response.request();
18069
+ if (HELD_REQUESTS.has(request3)) return;
16002
18070
  if (isTrackedRequest(request3.url(), request3.resourceType())) sink.openResponses.set(request3, response);
16003
18071
  } catch {
16004
18072
  }
16005
18073
  });
16006
18074
  page.on("request", (request3) => {
16007
18075
  const sink = currentSink;
18076
+ if (sink && sink.frozenAt !== null) HELD_REQUESTS.add(request3);
16008
18077
  if (!sink || sink.released.length === 0) return;
16009
18078
  sink.releasedRequests.add(request3);
16010
18079
  sink.releasedQuietSince = Date.now();
@@ -16015,16 +18084,18 @@ function instrumentPage(page) {
16015
18084
  sink.releasedQuietSince = Date.now();
16016
18085
  if (sink.releasedRequests.size === 0) sink.onReleasedRequestsEnded?.();
16017
18086
  }
16018
- if (!sink || RECORDED_OPEN.has(request3)) return;
18087
+ if (!sink || RECORDED_OPEN.has(request3) || HELD_REQUESTS.has(request3)) return;
16019
18088
  sink.lastActivePage = page;
16020
18089
  const response = sink.openResponses.get(request3) ?? null;
16021
18090
  sink.openResponses.delete(request3);
16022
18091
  recordRequest(sink, request3, failed ? "failed" : "finished", response);
18092
+ if (!failed) void keepResponse(request3, response);
16023
18093
  };
16024
18094
  page.on("requestfinished", (request3) => onRequestEnd(request3, false));
16025
18095
  page.on("requestfailed", (request3) => onRequestEnd(request3, true));
16026
18096
  }
16027
18097
  var RECORDED_OPEN = /* @__PURE__ */ new WeakSet();
18098
+ var HELD_REQUESTS = /* @__PURE__ */ new WeakSet();
16028
18099
  function isTrackedRequest(url, resourceType) {
16029
18100
  if (url.startsWith("data:") || url.startsWith("blob:")) return false;
16030
18101
  return TRACKED_REQUEST_TYPES.includes(resourceType);
@@ -16086,6 +18157,7 @@ function readResponseHeaders(sink, entry, url, headers) {
16086
18157
  }
16087
18158
  }
16088
18159
  function recordApiCall(sink, call) {
18160
+ if (sink.frozenAt !== null) return;
16089
18161
  try {
16090
18162
  const entry = apiCallEntry(call, sink.baseURL);
16091
18163
  if (!entry) return;
@@ -16122,7 +18194,10 @@ function instrumentContext(context) {
16122
18194
  INSTRUMENTED_CONTEXTS.add(context);
16123
18195
  if (typeof context.once === "function") {
16124
18196
  OPEN_CONTEXTS.add(context);
16125
- context.once("close", () => OPEN_CONTEXTS.delete(context));
18197
+ context.once("close", () => {
18198
+ OPEN_CONTEXTS.delete(context);
18199
+ CLOSING_CONTEXTS.add(context);
18200
+ });
16126
18201
  }
16127
18202
  observeOwnApiRequests(context);
16128
18203
  if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
@@ -16140,14 +18215,7 @@ function instrumentContext(context) {
16140
18215
  if (typeof context.close === "function") {
16141
18216
  const originalClose = context.close.bind(context);
16142
18217
  context.close = async (...args) => {
16143
- await drainPendingProbes(1e3);
16144
- const sink = currentSink;
16145
- if (sink) {
16146
- await endShownStops(sink, "closed", { context });
16147
- await stashPageState(sink, { context });
16148
- await maybeOpenPicker(sink, { context });
16149
- await stopSinkCodeReach(sink, { context });
16150
- }
18218
+ await settleContextClose(context);
16151
18219
  return originalClose(...args);
16152
18220
  };
16153
18221
  }
@@ -16160,7 +18228,7 @@ function patchBrowser(browser) {
16160
18228
  browser.newPage = async (...args) => {
16161
18229
  const page = await originalNewPage(...args);
16162
18230
  instrumentPage(page);
16163
- await armBreakpoints([pageContext(page)]);
18231
+ await armStops([pageContext(page)]);
16164
18232
  await awaitFlakeConditions();
16165
18233
  await maybeStartCodeReach(page);
16166
18234
  return page;
@@ -16169,13 +18237,16 @@ function patchBrowser(browser) {
16169
18237
  browser.newContext = async (...args) => {
16170
18238
  const context = await originalNewContext(...args);
16171
18239
  instrumentContext(context);
16172
- await armBreakpoints([context]);
18240
+ await armStops([context]);
16173
18241
  return context;
16174
18242
  };
16175
18243
  if (typeof browser.close === "function") {
16176
18244
  const originalClose = browser.close.bind(browser);
16177
18245
  browser.close = async (...args) => {
16178
18246
  await drainPendingProbes(1e3);
18247
+ for (const context of typeof browser.contexts === "function" ? browser.contexts() : []) {
18248
+ await settleContextClose(context);
18249
+ }
16179
18250
  return originalClose(...args);
16180
18251
  };
16181
18252
  }
@@ -16228,7 +18299,11 @@ async function flushSink(sink, testInfo) {
16228
18299
  clearTimeout(drainDeadline);
16229
18300
  const page = sink.lastActivePage;
16230
18301
  const pageReadable = page !== null && !isPageClosed(page);
16231
- if (pageReadable) await maybeOpenPicker(sink);
18302
+ if (pageReadable) {
18303
+ await holdFailure(sink);
18304
+ await maybeOpenPicker(sink);
18305
+ }
18306
+ const live = pageReadable && sink.frozenAt === null;
16232
18307
  if (sink.stops.length > 0) {
16233
18308
  await testInfo.attach(ATTACHMENT_NAMES.pauses, {
16234
18309
  contentType: "application/json",
@@ -16265,6 +18340,21 @@ async function flushSink(sink, testInfo) {
16265
18340
  body: Buffer.from(JSON.stringify(part))
16266
18341
  });
16267
18342
  }
18343
+ if (cssCoverageInWorker()) {
18344
+ const rules = sink.codeReach.cssFiles.size > 0 ? browserFunctionCoverage(sink.codeReach.cssFiles) : null;
18345
+ if (rules) {
18346
+ await testInfo.attach(ATTACHMENT_NAMES.cssCoverage, {
18347
+ contentType: "application/json",
18348
+ body: Buffer.from(JSON.stringify(rules))
18349
+ });
18350
+ }
18351
+ for (const part of newKnownFunctions(sink.codeReach.cssKnown, KNOWN_CSS_RULES_SENT)) {
18352
+ await testInfo.attach(ATTACHMENT_NAMES.cssRules, {
18353
+ contentType: "application/json",
18354
+ body: Buffer.from(JSON.stringify(part))
18355
+ });
18356
+ }
18357
+ }
16268
18358
  if (sink.serverCoverage.size > 0) {
16269
18359
  const requests = {};
16270
18360
  for (const [origin, ids] of sink.serverCoverage) requests[origin] = [...ids];
@@ -16281,13 +18371,13 @@ async function flushSink(sink, testInfo) {
16281
18371
  }
16282
18372
  if (testInfo.status !== "passed" && testInfo.status !== "skipped") {
16283
18373
  try {
16284
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
18374
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
16285
18375
  if (snapshot) {
16286
18376
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
16287
- contentType: "text/plain",
18377
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
16288
18378
  body: snapshot
16289
18379
  });
16290
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
18380
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
16291
18381
  if (snapshotJson) {
16292
18382
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
16293
18383
  contentType: "application/json",
@@ -16312,7 +18402,7 @@ async function flushSink(sink, testInfo) {
16312
18402
  }
16313
18403
  if (testInfo.status !== "passed" && testInfo.status !== "skipped" && sink.failureScreenshot) {
16314
18404
  const option = sink.failureScreenshot;
16315
- 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;
16316
18406
  if (shot) {
16317
18407
  try {
16318
18408
  await testInfo.attach(FAILURE_SCREENSHOT_ATTACHMENT, { contentType: "image/png", body: shot });
@@ -16322,13 +18412,13 @@ async function flushSink(sink, testInfo) {
16322
18412
  }
16323
18413
  if (testInfo.status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && isDueForAriaSample(testInfo)) {
16324
18414
  try {
16325
- const snapshot = (pageReadable ? await readRootAria(page) : null) ?? sink.stashedAria;
18415
+ const snapshot = (live ? await readRootAria(page) : null) ?? sink.stashedAria;
16326
18416
  if (snapshot) {
16327
18417
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshot, {
16328
- contentType: "text/plain",
18418
+ contentType: ARIA_SNAPSHOT_CONTENT_TYPE,
16329
18419
  body: snapshot
16330
18420
  });
16331
- const snapshotJson = (pageReadable ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
18421
+ const snapshotJson = (live ? await readRootAriaJson(page) : null) ?? sink.stashedAriaJson;
16332
18422
  if (snapshotJson) {
16333
18423
  await testInfo.attach(ATTACHMENT_NAMES.ariaSnapshotJson, {
16334
18424
  contentType: "application/json",
@@ -16374,7 +18464,7 @@ async function flushSink(sink, testInfo) {
16374
18464
  body: Buffer.from(JSON.stringify(sink.networkRequests))
16375
18465
  });
16376
18466
  }
16377
- const webVitals = (pageReadable ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
18467
+ const webVitals = (live ? await internalCall(page, () => readWebVitals(page)) : null) ?? sink.stashedWebVitals;
16378
18468
  if (webVitals) {
16379
18469
  await testInfo.attach(ATTACHMENT_NAMES.webVitals, {
16380
18470
  contentType: "application/json",
@@ -16382,7 +18472,7 @@ async function flushSink(sink, testInfo) {
16382
18472
  });
16383
18473
  }
16384
18474
  if (process.env.PIWI_CAPTURE_PAGE_STATE !== "false") {
16385
- const pageState = (pageReadable ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
18475
+ const pageState = (live ? await internalCall(page, () => readPageState(page)) : null) ?? sink.stashedPageState;
16386
18476
  if (pageState) {
16387
18477
  await testInfo.attach(ATTACHMENT_NAMES.pageState, {
16388
18478
  contentType: "application/json",
@@ -16391,7 +18481,7 @@ async function flushSink(sink, testInfo) {
16391
18481
  }
16392
18482
  }
16393
18483
  if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
16394
- if (pageReadable) await recordPageWindow(sink, page);
18484
+ if (live) await recordPageWindow(sink, page);
16395
18485
  const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
16396
18486
  if (windows.length > 0) {
16397
18487
  await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
@@ -16401,21 +18491,79 @@ async function flushSink(sink, testInfo) {
16401
18491
  }
16402
18492
  }
16403
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
+ }
16404
18548
  var piwiFixtures = {
16405
18549
  // Auto, worker-scoped: the worker's resource ledger, and the API request
16406
18550
  // contexts its tests create, handed out behind the call proxy. Set up before
16407
18551
  // any hook (worker fixtures come first), so a `beforeAll` context is in the
16408
18552
  // ledger and records the calls the tests make through it, and torn down after
16409
18553
  // the browser closed, so its census sees what only the worker's shutdown
16410
- // 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.
16411
18557
  piwiResources: [
16412
18558
  async ({ playwright }, use, workerInfo) => {
16413
18559
  boxCaptureFrames();
16414
18560
  startResourceLedger(playwright, workerInfo.workerIndex);
16415
18561
  observeApiRequests(playwright, apiCallRecorder);
18562
+ await openRunChannel(workerInfo, runControl);
16416
18563
  try {
16417
18564
  await use();
16418
18565
  } finally {
18566
+ closeRunChannel();
16419
18567
  stopResourceLedger();
16420
18568
  }
16421
18569
  },
@@ -16435,10 +18583,12 @@ var piwiFixtures = {
16435
18583
  // never double-wraps a page the browser patch already instrumented.
16436
18584
  page: async ({ page }, use) => {
16437
18585
  instrumentPage(page);
16438
- await armBreakpoints([pageContext(page)]);
18586
+ await armStops([pageContext(page)]);
16439
18587
  await awaitFlakeConditions();
16440
18588
  await maybeStartCodeReach(page);
16441
18589
  await use(page);
18590
+ const sink = currentSink;
18591
+ if (sink) await holdFailure(sink, page);
16442
18592
  },
16443
18593
  // Auto, test-scoped: open a capture sink for the running test and flush it
16444
18594
  // (attach the collected data) at teardown. Runs for every test without being
@@ -16453,12 +18603,16 @@ var piwiFixtures = {
16453
18603
  sink.baseURL = baseURL ?? null;
16454
18604
  if (keepsFailureScreenshots(screenshot, testInfo.retry)) sink.failureScreenshot = screenshot;
16455
18605
  const pauseSet = currentPauseSet();
16456
- if (pauseSet.size) {
16457
- const reason = pauseSkipReason(pauseGateFromTestInfo(testInfo));
16458
- if (reason) logPauseSkipOnce(reason);
16459
- 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);
16460
18611
  }
18612
+ pauseNextTest = false;
16461
18613
  if (sink.pause || canRecordFromHere()) endStopsWithTestFunction(testInfo, sink);
18614
+ if (channel) tellHub(testBegin(testInfo, cannotPause));
18615
+ if (channel && sink.pause) startKeeping();
16462
18616
  if (isFlakeMode()) {
16463
18617
  const plan = loadFlakePlan();
16464
18618
  const role = flakeRoleForTest(plan, {
@@ -16482,14 +18636,18 @@ var piwiFixtures = {
16482
18636
  });
16483
18637
  activeResourceLedger()?.testStarted(testInfo);
16484
18638
  currentSink = sink;
18639
+ const showRunChannel = hideRunChannel();
16485
18640
  try {
16486
- await armBreakpoints(OPEN_CONTEXTS);
18641
+ await armStops(OPEN_CONTEXTS);
16487
18642
  await startCodeReachOnOpenPages();
16488
18643
  await use();
16489
18644
  } finally {
18645
+ sink.ending = true;
16490
18646
  await endShownStops(sink, "ended");
16491
18647
  await awaitReleasedCalls(sink);
18648
+ showRunChannel();
16492
18649
  currentSink = null;
18650
+ if (sink.pause?.pause) pauseNextTest = true;
16493
18651
  if (sink.probeItem) {
16494
18652
  const applied = sink.probeInterception?.applied() ?? false;
16495
18653
  const level = sink.probeItem.level ?? "client";
@@ -16512,7 +18670,12 @@ var piwiFixtures = {
16512
18670
  });
16513
18671
  }
16514
18672
  if (sink.flake) await recordFlakeAttempt(sink.flake, testInfo);
16515
- await flushSink(sink, testInfo);
18673
+ try {
18674
+ await flushSink(sink, testInfo);
18675
+ } finally {
18676
+ stopKeeping();
18677
+ if (runChannelOpen()) tellHub(testEnd(sink, testInfo));
18678
+ }
16516
18679
  await recordResourceCensus(testInfo);
16517
18680
  }
16518
18681
  },
@@ -16546,12 +18709,12 @@ function extendPiwiFixtures(test) {
16546
18709
  }
16547
18710
 
16548
18711
  // src/internal/ai/ai-fixtures.ts
16549
- var fs28 = __toESM(require("fs"));
18712
+ var fs29 = __toESM(require("fs"));
16550
18713
  var path33 = __toESM(require("path"));
16551
18714
  var import_test = require("@playwright/test");
16552
18715
 
16553
18716
  // src/internal/ai/artifact.ts
16554
- var fs27 = __toESM(require("fs"));
18717
+ var fs28 = __toESM(require("fs"));
16555
18718
  var path31 = __toESM(require("path"));
16556
18719
  var ARTIFACT_VERSION = 1;
16557
18720
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
@@ -16645,7 +18808,7 @@ function parseEntry(text) {
16645
18808
  function readEntry(file) {
16646
18809
  let text;
16647
18810
  try {
16648
- text = fs27.readFileSync(file, "utf8");
18811
+ text = fs28.readFileSync(file, "utf8");
16649
18812
  } catch (error) {
16650
18813
  if (error.code === "ENOENT") return null;
16651
18814
  throw error;
@@ -16655,28 +18818,28 @@ function readEntry(file) {
16655
18818
  function writeEntry(file, entry) {
16656
18819
  const canonical = serializeEntry(entry);
16657
18820
  return withEntryLock(file, () => {
16658
- if (fs27.existsSync(file) && fs27.readFileSync(file, "utf8") === canonical) return { written: false };
16659
- fs27.mkdirSync(path31.dirname(file), { recursive: true });
18821
+ if (fs28.existsSync(file) && fs28.readFileSync(file, "utf8") === canonical) return { written: false };
18822
+ fs28.mkdirSync(path31.dirname(file), { recursive: true });
16660
18823
  const tmp = `${file}.${process.pid}.tmp`;
16661
- fs27.writeFileSync(tmp, canonical);
16662
- fs27.renameSync(tmp, file);
18824
+ fs28.writeFileSync(tmp, canonical);
18825
+ fs28.renameSync(tmp, file);
16663
18826
  return { written: true };
16664
18827
  });
16665
18828
  }
16666
18829
  function withEntryLock(file, fn) {
16667
18830
  const lock = `${file}.lock`;
16668
- fs27.mkdirSync(path31.dirname(file), { recursive: true });
18831
+ fs28.mkdirSync(path31.dirname(file), { recursive: true });
16669
18832
  const deadline = Date.now() + 5e3;
16670
18833
  for (; ; ) {
16671
18834
  try {
16672
- const fd = fs27.openSync(lock, "wx");
16673
- fs27.closeSync(fd);
18835
+ const fd = fs28.openSync(lock, "wx");
18836
+ fs28.closeSync(fd);
16674
18837
  break;
16675
18838
  } catch (error) {
16676
18839
  if (error.code !== "EEXIST") throw error;
16677
18840
  if (Date.now() > deadline) {
16678
18841
  try {
16679
- fs27.unlinkSync(lock);
18842
+ fs28.unlinkSync(lock);
16680
18843
  } catch {
16681
18844
  }
16682
18845
  }
@@ -16686,14 +18849,14 @@ function withEntryLock(file, fn) {
16686
18849
  return fn();
16687
18850
  } finally {
16688
18851
  try {
16689
- fs27.unlinkSync(lock);
18852
+ fs28.unlinkSync(lock);
16690
18853
  } catch {
16691
18854
  }
16692
18855
  }
16693
18856
  }
16694
18857
 
16695
18858
  // src/internal/ai/keys.ts
16696
- var crypto7 = __toESM(require("crypto"));
18859
+ var crypto9 = __toESM(require("crypto"));
16697
18860
  var path32 = __toESM(require("path"));
16698
18861
  var DEFAULT_AI_DIR = "__piwi__";
16699
18862
  function normalizeTemplate(template) {
@@ -16701,7 +18864,7 @@ function normalizeTemplate(template) {
16701
18864
  }
16702
18865
  function hashTemplate(normalized, ordinal = 0) {
16703
18866
  const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
16704
- return crypto7.createHash("sha256").update(input).digest("hex").slice(0, 8);
18867
+ return crypto9.createHash("sha256").update(input).digest("hex").slice(0, 8);
16705
18868
  }
16706
18869
  function slug(text, maxLength = 40) {
16707
18870
  const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -17320,15 +19483,15 @@ function relativeToCwd(file) {
17320
19483
  return path33.relative(process.cwd(), file).split(path33.sep).join("/");
17321
19484
  }
17322
19485
  function recordIntents(intents, entry) {
17323
- const add = (locator, kind) => {
19486
+ const add2 = (locator, kind) => {
17324
19487
  intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
17325
19488
  };
17326
19489
  if (entry.kind === "locator") {
17327
- add(locatorSource2(entry.locator), "locator");
19490
+ add2(locatorSource2(entry.locator), "locator");
17328
19491
  return;
17329
19492
  }
17330
- for (const step of entry.steps) add(locatorSource2(step.locator), "run");
17331
- if (entry.postcondition.locator) add(locatorSource2(entry.postcondition.locator), "run");
19493
+ for (const step of entry.steps) add2(locatorSource2(step.locator), "run");
19494
+ if (entry.postcondition.locator) add2(locatorSource2(entry.postcondition.locator), "run");
17332
19495
  }
17333
19496
  function missMessage(template, testTitle, relPath) {
17334
19497
  return [
@@ -17344,7 +19507,7 @@ function createAiApi(page, testInfo, config, used, intents) {
17344
19507
  const readSource = () => {
17345
19508
  if (source === null) {
17346
19509
  try {
17347
- source = fs28.readFileSync(testInfo.file, "utf8");
19510
+ source = fs29.readFileSync(testInfo.file, "utf8");
17348
19511
  } catch {
17349
19512
  source = "";
17350
19513
  }