@piwitests/reporter 0.54.0 → 0.56.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.
@@ -58,7 +58,7 @@ __export(capture_fixtures_exports, {
58
58
  stopResourceLedger: () => stopResourceLedger
59
59
  });
60
60
  module.exports = __toCommonJS(capture_fixtures_exports);
61
- var path13 = __toESM(require("path"));
61
+ var path12 = __toESM(require("path"));
62
62
  var import_node_zlib2 = require("zlib");
63
63
 
64
64
  // ../picker-dom/src/probe.ts
@@ -489,26 +489,26 @@ function installPickerOverlay(arg) {
489
489
  }
490
490
  };
491
491
  mount(highlight, label, banner);
492
- const escJs = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
492
+ const quoteJs = (s) => `'${JSON.stringify(s).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
493
493
  const describe = (el) => {
494
494
  const tag = (el.tagName || "?").toLowerCase();
495
495
  const testId = el.getAttribute && el.getAttribute(testIdAttr);
496
- if (testId) return `getByTestId('${escJs(testId)}')`;
496
+ if (testId) return `getByTestId(${quoteJs(testId)})`;
497
497
  if (el.labels && el.labels.length > 0) {
498
498
  const labelText = (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 80);
499
- if (labelText) return `getByLabel('${escJs(labelText)}')`;
499
+ if (labelText) return `getByLabel(${quoteJs(labelText)})`;
500
500
  }
501
501
  const ariaLabel = el.getAttribute && el.getAttribute("aria-label");
502
- if (ariaLabel) return `getByLabel('${escJs(ariaLabel)}')`;
502
+ if (ariaLabel) return `getByLabel(${quoteJs(ariaLabel)})`;
503
503
  const placeholder = el.getAttribute && el.getAttribute("placeholder");
504
- if (placeholder) return `getByPlaceholder('${escJs(placeholder)}')`;
504
+ if (placeholder) return `getByPlaceholder(${quoteJs(placeholder)})`;
505
505
  const alt = el.getAttribute && el.getAttribute("alt");
506
- if (alt) return `getByAltText('${escJs(alt)}')`;
506
+ if (alt) return `getByAltText(${quoteJs(alt)})`;
507
507
  const titleAttr = el.getAttribute && el.getAttribute("title");
508
- if (titleAttr) return `getByTitle('${escJs(titleAttr)}')`;
509
- if (el.id) return `locator('#${escJs(el.id)}')`;
508
+ if (titleAttr) return `getByTitle(${quoteJs(titleAttr)})`;
509
+ if (el.id) return `locator(${quoteJs(`#${el.id}`)})`;
510
510
  const cls = (el.getAttribute && el.getAttribute("class") || "").split(/\s+/).find((c) => c.length > 1);
511
- return cls ? `locator('.${escJs(cls)}')` : tag;
511
+ return cls ? `locator(${quoteJs(`.${cls}`)})` : tag;
512
512
  };
513
513
  const buildChain = (raw) => {
514
514
  const chain2 = [];
@@ -1198,422 +1198,706 @@ function takePauseState() {
1198
1198
  return typeof state === "string" ? state : null;
1199
1199
  }
1200
1200
 
1201
- // ../core/src/locator-generation.ts
1202
- var TAG_TO_ROLE = {
1203
- a: "link",
1204
- button: "button",
1205
- nav: "navigation",
1206
- main: "main",
1207
- article: "article",
1208
- section: "region",
1209
- form: "form",
1210
- img: "img",
1211
- figure: "figure",
1212
- figcaption: "caption",
1213
- blockquote: "blockquote",
1214
- table: "table",
1215
- caption: "caption",
1216
- thead: "rowgroup",
1217
- tbody: "rowgroup",
1218
- tfoot: "rowgroup",
1219
- tr: "row",
1220
- td: "cell",
1221
- ul: "list",
1222
- ol: "list",
1223
- li: "listitem",
1224
- dialog: "dialog",
1225
- output: "status",
1226
- progress: "progressbar",
1227
- meter: "meter",
1228
- textarea: "textbox",
1229
- h1: "heading",
1230
- h2: "heading",
1231
- h3: "heading",
1232
- h4: "heading",
1233
- h5: "heading",
1234
- h6: "heading",
1235
- details: "group",
1236
- summary: "button",
1237
- search: "search"
1201
+ // ../core/src/locator-chain.ts
1202
+ var LOCATING_METHODS = /* @__PURE__ */ new Set([
1203
+ "getByRole",
1204
+ "getByTestId",
1205
+ "getByText",
1206
+ "getByLabel",
1207
+ "getByPlaceholder",
1208
+ "getByAltText",
1209
+ "getByTitle",
1210
+ "locator",
1211
+ "frameLocator"
1212
+ ]);
1213
+ var NARROWING_METHODS = /* @__PURE__ */ new Set([
1214
+ "filter",
1215
+ "first",
1216
+ "last",
1217
+ "nth",
1218
+ "and",
1219
+ "or",
1220
+ "visible",
1221
+ "contentFrame",
1222
+ "owner"
1223
+ ]);
1224
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
1225
+ var LocatorParseError = class extends Error {
1226
+ constructor(message) {
1227
+ super(message);
1228
+ this.name = "LocatorParseError";
1229
+ }
1238
1230
  };
1239
- var INPUT_TYPE_TO_ROLE = {
1240
- button: "button",
1241
- submit: "button",
1242
- reset: "button",
1243
- image: "button",
1244
- checkbox: "checkbox",
1245
- radio: "radio",
1246
- range: "slider",
1247
- search: "searchbox",
1248
- number: "spinbutton",
1249
- text: "textbox",
1250
- email: "textbox",
1251
- tel: "textbox",
1252
- url: "textbox",
1253
- password: "textbox"
1231
+ var SIMPLE_ESCAPES = {
1232
+ n: "\n",
1233
+ t: " ",
1234
+ r: "\r",
1235
+ b: "\b",
1236
+ f: "\f",
1237
+ v: "\v",
1238
+ "0": "\0"
1254
1239
  };
1255
- var CAPTURED_ATTRIBUTES = [
1256
- "id",
1257
- "class",
1258
- "name",
1259
- "data-testid",
1260
- "placeholder",
1261
- "alt",
1262
- "title",
1263
- "aria-label",
1264
- "aria-labelledby",
1265
- "aria-level",
1266
- "role",
1267
- "type",
1268
- "href",
1269
- "multiple"
1270
- ];
1271
- function resolveAriaRole(attrs) {
1272
- const explicit = attrs.attributes["role"];
1273
- if (explicit) return explicit;
1274
- const tag = attrs.tagName;
1275
- if (!tag) return null;
1276
- if (tag === "input") {
1277
- const type = (attrs.attributes["type"] ?? "text").toLowerCase();
1278
- return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
1279
- }
1280
- if (tag === "select") {
1281
- return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
1282
- }
1283
- if (tag === "a") {
1284
- return attrs.attributes["href"] != null ? "link" : null;
1285
- }
1286
- return TAG_TO_ROLE[tag] ?? null;
1287
- }
1288
- function headingLevel(attrs, role) {
1289
- if (role !== "heading") return null;
1290
- const tagMatch = attrs.tagName.match(/^h([1-6])$/);
1291
- if (tagMatch) return Number(tagMatch[1]);
1292
- const ariaLevel = attrs.attributes["aria-level"];
1293
- if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
1294
- return null;
1295
- }
1296
- var attr = (a, key) => a.attributes[key] || null;
1297
- var esc = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1298
- var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1299
- var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
1300
- function classifyCssStability(className) {
1301
- if (/[a-f0-9]{8,}/i.test(className)) return 10;
1302
- if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
1303
- if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
1304
- className
1305
- ))
1306
- return 25;
1307
- if (/__/.test(className) || /--/.test(className)) return 35;
1308
- if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
1309
- if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
1310
- if (className.includes("_")) return 15;
1311
- if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
1312
- return 15;
1313
- }
1314
- function isAutoGenerated(value) {
1315
- if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
1316
- if (/^[a-f0-9]{8,}$/i.test(value)) return true;
1317
- if (/^[a-z]+-\d{4,}$/.test(value)) return true;
1318
- if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
1319
- value
1320
- )) {
1321
- return true;
1240
+ var Parser = class {
1241
+ constructor(src) {
1242
+ this.src = src;
1243
+ this.i = 0;
1322
1244
  }
1323
- if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
1324
- if (value.startsWith("ng-")) return true;
1325
- if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
1326
- if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
1327
- if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
1328
- return false;
1329
- }
1330
- var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
1331
- function generateAlternatives(attrs, options = {}) {
1332
- const alts = [];
1333
- const seen = /* @__PURE__ */ new Set();
1334
- const add = (loc) => {
1335
- if (!seen.has(loc.locator)) {
1336
- seen.add(loc.locator);
1337
- alts.push(loc);
1338
- }
1339
- };
1340
- const { accessibleName } = attrs;
1341
- const tag = attrs.tagName;
1342
- const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
1343
- const counts = attrs.selectorCounts;
1344
- const isUnique = (n) => n == null || n <= 1;
1345
- const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
1346
- const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
1347
- const testIdAttribute = options.testIdAttribute || "data-testid";
1348
- const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
1349
- const testId = attr(attrs, testIdAttribute);
1350
- if (testId && isUnique(counts?.testId)) {
1351
- add({
1352
- locator: `getByTestId('${esc(testId)}')`,
1353
- method: "getByTestId",
1354
- args: { testId },
1355
- score: 100
1356
- });
1245
+ parseTopLevel() {
1246
+ const chain = this.parseChain();
1247
+ this.skipSpace();
1248
+ if (this.i < this.src.length) this.fail("unexpected text");
1249
+ return chain;
1357
1250
  }
1358
- const level = headingLevel(attrs, role);
1359
- const levelPart = level != null ? `, level: ${level}` : "";
1360
- const withLevel = (base) => level != null ? { ...base, level } : base;
1361
- if (role && accessibleName) {
1362
- add({
1363
- locator: `getByRole('${role}', { name: '${esc(accessibleName)}'${levelPart} })`,
1364
- method: "getByRole",
1365
- args: withLevel({ role, name: accessibleName }),
1366
- score: 90 - ambiguityPenalty(counts?.roleName)
1367
- });
1251
+ fail(message) {
1252
+ throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
1368
1253
  }
1369
- const ariaLabel = attr(attrs, "aria-label");
1370
- if (role && ariaLabel && ariaLabel !== accessibleName) {
1371
- add({
1372
- locator: `getByRole('${role}', { name: '${esc(ariaLabel)}'${levelPart} })`,
1373
- method: "getByRole",
1374
- args: withLevel({ role, name: ariaLabel }),
1375
- score: 85 - ambiguityPenalty(counts?.roleName)
1376
- });
1254
+ skipSpace() {
1255
+ while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
1377
1256
  }
1378
- if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
1379
- const label = attr(attrs, "aria-label");
1380
- const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
1381
- if (labelBacked) {
1382
- add({
1383
- locator: `getByLabel('${esc(accessibleName)}')`,
1384
- method: "getByLabel",
1385
- args: { label: accessibleName },
1386
- score: 85
1387
- });
1388
- }
1257
+ peek() {
1258
+ return this.src[this.i];
1389
1259
  }
1390
- const placeholder = attr(attrs, "placeholder");
1391
- if (placeholder) {
1392
- add({
1393
- locator: `getByPlaceholder('${esc(placeholder)}')`,
1394
- method: "getByPlaceholder",
1395
- args: { placeholder },
1396
- score: 80 - ambiguityPenalty(counts?.placeholder)
1397
- });
1260
+ expect(ch) {
1261
+ this.skipSpace();
1262
+ if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
1263
+ this.i++;
1398
1264
  }
1399
- if (text && text.length < 80) {
1400
- add({
1401
- locator: `getByText('${esc(text)}')`,
1402
- method: "getByText",
1403
- args: { text },
1404
- score: 75 - ambiguityPenalty(counts?.text)
1405
- });
1265
+ identifier() {
1266
+ const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
1267
+ if (!m) this.fail("expected a name");
1268
+ this.i += m[0].length;
1269
+ return m[0];
1406
1270
  }
1407
- const id = attr(attrs, "id");
1408
- if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1409
- const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1410
- add({
1411
- locator: `locator('${esc(selector)}')`,
1412
- method: "locator",
1413
- args: { selector },
1414
- score: 65
1415
- });
1271
+ /**
1272
+ * A chain of calls joined by `.`. With `lenient`, parsing stops before the
1273
+ * first link that doesn't parse instead of failing, so a chain followed by
1274
+ * other text (an error message) is read up to where it ends. `spans` receives
1275
+ * where each top-level call starts and ends.
1276
+ */
1277
+ parseChain(opts = {}) {
1278
+ this.skipSpace();
1279
+ const calls = [];
1280
+ for (; ; ) {
1281
+ const before = this.i;
1282
+ if (calls.length > 0) this.i++;
1283
+ this.skipSpace();
1284
+ const start = this.i;
1285
+ try {
1286
+ calls.push(this.parseCall(calls.length === 0));
1287
+ } catch (error) {
1288
+ if (!opts.lenient || calls.length === 0) throw error;
1289
+ this.i = before;
1290
+ break;
1291
+ }
1292
+ opts.spans?.push({ start, end: this.i });
1293
+ const end = this.i;
1294
+ this.skipSpace();
1295
+ if (this.peek() !== ".") {
1296
+ this.i = end;
1297
+ break;
1298
+ }
1299
+ }
1300
+ return { calls };
1416
1301
  }
1417
- const name = attr(attrs, "name");
1418
- if (name && selectable("name") && isUnique(counts?.name)) {
1419
- const selector = `[name="${escCssAttrValue(name)}"]`;
1420
- add({
1421
- locator: `locator('${esc(selector)}')`,
1422
- method: "locator",
1423
- args: { selector },
1424
- score: 60
1425
- });
1302
+ /** Where the parser stopped. */
1303
+ get position() {
1304
+ return this.i;
1426
1305
  }
1427
- const alt = attr(attrs, "alt");
1428
- if (alt) {
1429
- add({
1430
- locator: `getByAltText('${esc(alt)}')`,
1431
- method: "getByAltText",
1432
- args: { text: alt },
1433
- score: 60 - ambiguityPenalty(counts?.alt)
1434
- });
1306
+ parseCall(first) {
1307
+ this.skipSpace();
1308
+ const method = this.identifier();
1309
+ if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
1310
+ if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
1311
+ this.expect("(");
1312
+ const args = [];
1313
+ this.skipSpace();
1314
+ if (this.peek() === ")") {
1315
+ this.i++;
1316
+ return { method, args };
1317
+ }
1318
+ for (; ; ) {
1319
+ args.push(this.parseValue());
1320
+ this.skipSpace();
1321
+ if (this.peek() === ",") {
1322
+ this.i++;
1323
+ this.skipSpace();
1324
+ if (this.peek() === ")") {
1325
+ this.i++;
1326
+ break;
1327
+ }
1328
+ continue;
1329
+ }
1330
+ this.expect(")");
1331
+ break;
1332
+ }
1333
+ return { method, args };
1435
1334
  }
1436
- const title = attr(attrs, "title");
1437
- if (title) {
1438
- add({
1439
- locator: `getByTitle('${esc(title)}')`,
1440
- method: "getByTitle",
1441
- args: { title },
1442
- score: 50 - ambiguityPenalty(counts?.title)
1443
- });
1335
+ parseValue() {
1336
+ this.skipSpace();
1337
+ const ch = this.peek();
1338
+ if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
1339
+ if (ch === "/") return this.parseRegex();
1340
+ if (ch === "{") return this.parseObject();
1341
+ if (ch !== void 0 && /[-\d]/.test(ch)) {
1342
+ const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
1343
+ if (!m) this.fail("expected a number");
1344
+ this.i += m[0].length;
1345
+ return { type: "number", value: Number(m[0]) };
1346
+ }
1347
+ const rest = this.src.slice(this.i);
1348
+ if (/^true\b/.test(rest)) {
1349
+ this.i += 4;
1350
+ return { type: "boolean", value: true };
1351
+ }
1352
+ if (/^false\b/.test(rest)) {
1353
+ this.i += 5;
1354
+ return { type: "boolean", value: false };
1355
+ }
1356
+ if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
1357
+ return this.fail("expected a value");
1444
1358
  }
1445
- const hasOwnTestId = !!(testId && isUnique(counts?.testId));
1446
- const addAnchoredChains = (leaf, scopedCount, scores) => {
1447
- let testIdAnchorDone = false;
1448
- let idAnchorDone = false;
1449
- let roleAnchorDone = false;
1450
- let dataAnchorDone = false;
1451
- let filterAnchorDone = false;
1452
- for (const anc of attrs.ancestors ?? []) {
1453
- if (scopedCount(anc) !== 1) continue;
1454
- if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
1455
- testIdAnchorDone = true;
1456
- add({
1457
- locator: `getByTestId('${esc(anc.testId)}').${leaf.expr}`,
1458
- method: leaf.method,
1459
- args: { ...leaf.args, anchorTestId: anc.testId },
1460
- score: scores.testId
1461
- });
1462
- }
1463
- if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1464
- idAnchorDone = true;
1465
- const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1466
- add({
1467
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
1468
- method: leaf.method,
1469
- args: { ...leaf.args, anchorSelector },
1470
- score: scores.id
1471
- });
1359
+ /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
1360
+ parseString() {
1361
+ const quote = this.src[this.i];
1362
+ let out = "";
1363
+ this.i++;
1364
+ while (this.i < this.src.length) {
1365
+ const c = this.src[this.i];
1366
+ if (c === quote) {
1367
+ this.i++;
1368
+ return out;
1472
1369
  }
1473
- if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
1474
- dataAnchorDone = true;
1475
- const { name: name2, value } = anc.dataAttr;
1476
- const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
1477
- add({
1478
- locator: `locator('${esc(anchorSelector)}').${leaf.expr}`,
1479
- method: leaf.method,
1480
- args: { ...leaf.args, anchorSelector },
1481
- score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
1482
- });
1370
+ if (c !== "\\") {
1371
+ out += c;
1372
+ this.i++;
1373
+ continue;
1483
1374
  }
1484
- const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
1485
- if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
1486
- roleAnchorDone = true;
1487
- add({
1488
- locator: `getByRole('${esc(ancestorRole)}').${leaf.expr}`,
1489
- method: leaf.method,
1490
- args: { ...leaf.args, anchorRole: ancestorRole },
1491
- score: scores.role
1492
- });
1375
+ const next = this.src[this.i + 1];
1376
+ if (next === void 0) break;
1377
+ const simple = SIMPLE_ESCAPES[next];
1378
+ if (simple !== void 0) {
1379
+ out += simple;
1380
+ this.i += 2;
1381
+ continue;
1493
1382
  }
1494
- if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
1495
- filterAnchorDone = true;
1496
- add({
1497
- locator: `getByRole('${esc(ancestorRole)}').filter({ hasText: '${esc(anc.filterText)}' }).${leaf.expr}`,
1498
- method: leaf.method,
1499
- args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
1500
- score: scores.filter
1501
- });
1383
+ const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
1384
+ if (hex) {
1385
+ out += String.fromCodePoint(parseInt(hex[1], 16));
1386
+ this.i += 1 + hex[0].length;
1387
+ continue;
1502
1388
  }
1389
+ if (next === "u" || next === "x") this.fail("malformed escape");
1390
+ out += next;
1391
+ this.i += 2;
1503
1392
  }
1504
- };
1505
- if (role && !hasOwnTestId) {
1506
- const rolePart = level != null ? `'${role}', { level: ${level} }` : `'${role}'`;
1507
- const leafArgs = withLevel({ role });
1508
- addAnchoredChains(
1509
- { expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
1510
- (anc) => anc.scopedRoleCount,
1511
- { testId: 72, testData: 70, id: 64, data: 60, role: 55, filter: 53 }
1512
- );
1513
- const pos = attrs.rolePosition;
1514
- if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
1515
- add({
1516
- locator: `getByRole(${rolePart})`,
1517
- method: "getByRole",
1518
- args: leafArgs,
1519
- score: 58
1520
- });
1521
- }
1522
- } else if (!role && text && text.length < 80 && !hasOwnTestId) {
1523
- addAnchoredChains(
1524
- { expr: `getByText('${esc(text)}')`, method: "getByText", args: { text }, role: null },
1525
- (anc) => anc.scopedTextCount,
1526
- { testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
1527
- );
1393
+ return this.fail("unterminated string");
1528
1394
  }
1529
- const clsStr = attr(attrs, "class");
1530
- if (clsStr) {
1531
- const countsClasses = counts?.classes;
1532
- 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);
1533
- for (const { cls, score } of classes) {
1534
- add({
1535
- locator: `locator('.${esc(cls)}')`,
1536
- method: "locator",
1537
- args: { selector: `.${cls}` },
1538
- score
1539
- });
1395
+ parseRegex() {
1396
+ let j = this.i + 1;
1397
+ let inClass = false;
1398
+ while (j < this.src.length) {
1399
+ const c = this.src[j];
1400
+ if (c === "\\") {
1401
+ j += 2;
1402
+ continue;
1403
+ }
1404
+ if (c === "[") inClass = true;
1405
+ else if (c === "]") inClass = false;
1406
+ else if (c === "/" && !inClass) break;
1407
+ j++;
1540
1408
  }
1409
+ if (j >= this.src.length) this.fail("unterminated regex");
1410
+ const source = this.src.slice(this.i + 1, j);
1411
+ const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
1412
+ this.i = j + 1 + flags.length;
1413
+ return { type: "regex", source, flags };
1414
+ }
1415
+ parseObject() {
1416
+ this.expect("{");
1417
+ const entries = [];
1418
+ for (; ; ) {
1419
+ this.skipSpace();
1420
+ if (this.peek() === "}") {
1421
+ this.i++;
1422
+ break;
1423
+ }
1424
+ const ch = this.peek();
1425
+ const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
1426
+ this.expect(":");
1427
+ entries.push([key, this.parseValue()]);
1428
+ this.skipSpace();
1429
+ if (this.peek() === ",") {
1430
+ this.i++;
1431
+ continue;
1432
+ }
1433
+ this.expect("}");
1434
+ break;
1435
+ }
1436
+ return { type: "object", entries };
1437
+ }
1438
+ };
1439
+ function parseLocatorChain(expr) {
1440
+ const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
1441
+ if (!trimmed) throw new LocatorParseError("empty expression");
1442
+ return new Parser(trimmed).parseTopLevel();
1443
+ }
1444
+ function tryParseLocatorChain(expr) {
1445
+ try {
1446
+ return parseLocatorChain(expr);
1447
+ } catch {
1448
+ return null;
1449
+ }
1450
+ }
1451
+ function quoteJsString(value) {
1452
+ const json = JSON.stringify(value).slice(1, -1);
1453
+ return `'${json.replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
1454
+ }
1455
+ function parseLeafLocatorCall(expr) {
1456
+ const chain = tryParseLocatorChain(expr);
1457
+ if (!chain) return null;
1458
+ for (let i = chain.calls.length - 1; i >= 0; i--) {
1459
+ if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
1541
1460
  }
1542
- return alts.sort((a, b) => b.score - a.score);
1543
- }
1544
- function approximateAccessibleName(attrs) {
1545
- const a = attrs.attributes;
1546
- const labelText = attrs.labelText || null;
1547
- if (a["aria-labelledby"] && labelText) return labelText;
1548
- const ariaLabel = a["aria-label"];
1549
- if (ariaLabel) return ariaLabel;
1550
- if (["input", "select", "textarea"].includes(attrs.tagName)) {
1551
- if (labelText) return labelText;
1552
- } else if (attrs.textContent) {
1553
- return attrs.textContent;
1554
- }
1555
- const title = a["title"];
1556
- if (title) return title;
1557
- const placeholder = a["placeholder"];
1558
- if (placeholder) return placeholder;
1559
1461
  return null;
1560
1462
  }
1463
+ function locatorCallValues(call, opts = {}) {
1464
+ const value = (arg) => {
1465
+ switch (arg.type) {
1466
+ case "string":
1467
+ case "number":
1468
+ case "boolean":
1469
+ return arg.value;
1470
+ case "regex":
1471
+ return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
1472
+ case "object": {
1473
+ const out = {};
1474
+ for (const [key, v] of arg.entries) {
1475
+ const plain = value(v);
1476
+ if (plain !== void 0) out[key] = plain;
1477
+ }
1478
+ return out;
1479
+ }
1480
+ case "chain":
1481
+ return void 0;
1482
+ }
1483
+ };
1484
+ return call.args.map(value).filter((v) => v !== void 0);
1485
+ }
1561
1486
 
1562
- // ../picker-dom/src/anchor-alternatives.ts
1563
- var escStr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
1564
- var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
1565
- var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
1566
- function anchorSegment(a) {
1567
- if (a.testId && a.testIdCount === 1) {
1568
- return { code: `getByTestId('${escStr(a.testId)}')`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
1569
- }
1570
- if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
1571
- const anchorSelector = cssIdSelector(a.id);
1572
- return { code: `locator('${escStr(anchorSelector)}')`, kind: "id", flatArgs: { anchorSelector } };
1487
+ // ../core/src/locator-generation.ts
1488
+ var TAG_TO_ROLE = {
1489
+ a: "link",
1490
+ button: "button",
1491
+ nav: "navigation",
1492
+ main: "main",
1493
+ article: "article",
1494
+ section: "region",
1495
+ form: "form",
1496
+ img: "img",
1497
+ figure: "figure",
1498
+ figcaption: "caption",
1499
+ blockquote: "blockquote",
1500
+ table: "table",
1501
+ caption: "caption",
1502
+ thead: "rowgroup",
1503
+ tbody: "rowgroup",
1504
+ tfoot: "rowgroup",
1505
+ tr: "row",
1506
+ td: "cell",
1507
+ ul: "list",
1508
+ ol: "list",
1509
+ li: "listitem",
1510
+ dialog: "dialog",
1511
+ output: "status",
1512
+ progress: "progressbar",
1513
+ meter: "meter",
1514
+ textarea: "textbox",
1515
+ h1: "heading",
1516
+ h2: "heading",
1517
+ h3: "heading",
1518
+ h4: "heading",
1519
+ h5: "heading",
1520
+ h6: "heading",
1521
+ details: "group",
1522
+ summary: "button",
1523
+ search: "search"
1524
+ };
1525
+ var INPUT_TYPE_TO_ROLE = {
1526
+ button: "button",
1527
+ submit: "button",
1528
+ reset: "button",
1529
+ image: "button",
1530
+ checkbox: "checkbox",
1531
+ radio: "radio",
1532
+ range: "slider",
1533
+ search: "searchbox",
1534
+ number: "spinbutton",
1535
+ text: "textbox",
1536
+ email: "textbox",
1537
+ tel: "textbox",
1538
+ url: "textbox",
1539
+ password: "textbox"
1540
+ };
1541
+ var CAPTURED_ATTRIBUTES = [
1542
+ "id",
1543
+ "class",
1544
+ "name",
1545
+ "data-testid",
1546
+ "placeholder",
1547
+ "alt",
1548
+ "title",
1549
+ "aria-label",
1550
+ "aria-labelledby",
1551
+ "aria-level",
1552
+ "role",
1553
+ "type",
1554
+ "href",
1555
+ "multiple"
1556
+ ];
1557
+ function resolveAriaRole(attrs) {
1558
+ const explicit = attrs.attributes["role"];
1559
+ if (explicit) return explicit;
1560
+ const tag = attrs.tagName;
1561
+ if (!tag) return null;
1562
+ if (tag === "input") {
1563
+ const type = (attrs.attributes["type"] ?? "text").toLowerCase();
1564
+ return INPUT_TYPE_TO_ROLE[type] ?? "textbox";
1573
1565
  }
1574
- if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
1575
- return {
1576
- code: `getByRole('${escStr(a.role)}', { name: '${escStr(a.ariaLabel)}' })`,
1577
- kind: "labeledRole",
1578
- flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
1579
- };
1566
+ if (tag === "select") {
1567
+ return attrs.attributes["multiple"] != null ? "listbox" : "combobox";
1580
1568
  }
1581
- if (a.role && a.roleCount === 1) {
1582
- return { code: `getByRole('${escStr(a.role)}')`, kind: "role", flatArgs: { anchorRole: a.role } };
1569
+ if (tag === "a") {
1570
+ return attrs.attributes["href"] != null ? "link" : null;
1583
1571
  }
1572
+ return TAG_TO_ROLE[tag] ?? null;
1573
+ }
1574
+ function headingLevel(attrs, role) {
1575
+ if (role !== "heading") return null;
1576
+ const tagMatch = attrs.tagName.match(/^h([1-6])$/);
1577
+ if (tagMatch) return Number(tagMatch[1]);
1578
+ const ariaLevel = attrs.attributes["aria-level"];
1579
+ if (ariaLevel && /^\d+$/.test(ariaLevel)) return Number(ariaLevel);
1584
1580
  return null;
1585
1581
  }
1586
- function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1587
- if (!leaf.role || anchors.length === 0) return [];
1588
- const out = [];
1582
+ var attr = (a, key) => a.attributes[key] || null;
1583
+ var escCssAttrValue = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1584
+ var isCssSafeId = (id) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(id);
1585
+ function classifyCssStability(className) {
1586
+ if (/[a-f0-9]{8,}/i.test(className)) return 10;
1587
+ if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
1588
+ if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
1589
+ className
1590
+ ))
1591
+ return 25;
1592
+ if (/__/.test(className) || /--/.test(className)) return 35;
1593
+ if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
1594
+ if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
1595
+ if (className.includes("_")) return 15;
1596
+ if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
1597
+ return 15;
1598
+ }
1599
+ function isAutoGenerated(value) {
1600
+ if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
1601
+ if (/^[a-f0-9]{8,}$/i.test(value)) return true;
1602
+ if (/^[a-z]+-\d{4,}$/.test(value)) return true;
1603
+ if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
1604
+ value
1605
+ )) {
1606
+ return true;
1607
+ }
1608
+ if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
1609
+ if (value.startsWith("ng-")) return true;
1610
+ if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
1611
+ if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
1612
+ if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
1613
+ return false;
1614
+ }
1615
+ var TEST_DATA_ATTRS = /* @__PURE__ */ new Set(["data-test", "data-test-id", "data-qa", "data-qa-id", "data-cy", "data-e2e"]);
1616
+ function generateAlternatives(attrs, options = {}) {
1617
+ const alts = [];
1589
1618
  const seen = /* @__PURE__ */ new Set();
1590
- const rolePart = leaf.level != null ? `'${leaf.role}', { level: ${leaf.level} }` : `'${leaf.role}'`;
1591
- const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
1592
- const add = (l) => {
1593
- if (!seen.has(l.locator)) {
1594
- seen.add(l.locator);
1595
- out.push(l);
1619
+ const add = (loc) => {
1620
+ if (!seen.has(loc.locator)) {
1621
+ seen.add(loc.locator);
1622
+ alts.push(loc);
1596
1623
  }
1597
1624
  };
1598
- for (const anchor of anchors) {
1599
- if (anchor.scopedLeafCount !== 1) continue;
1600
- const seg = anchorSegment(anchor);
1601
- if (!seg) continue;
1625
+ const { accessibleName } = attrs;
1626
+ const tag = attrs.tagName;
1627
+ const role = options.role !== void 0 ? options.role : resolveAriaRole(attrs);
1628
+ const counts = attrs.selectorCounts;
1629
+ const isUnique = (n) => n == null || n <= 1;
1630
+ const ambiguityPenalty = (n) => n != null && n > 1 ? 45 : 0;
1631
+ const text = attrs.textContent ? attrs.textContent.replace(/\s+/g, " ").trim() : null;
1632
+ const testIdAttribute = options.testIdAttribute || "data-testid";
1633
+ const selectable = (name2) => name2.toLowerCase() !== testIdAttribute.toLowerCase();
1634
+ const testId = attr(attrs, testIdAttribute);
1635
+ if (testId && isUnique(counts?.testId)) {
1602
1636
  add({
1603
- locator: `${seg.code}.getByRole(${rolePart})`,
1637
+ locator: `getByTestId(${quoteJsString(testId)})`,
1638
+ method: "getByTestId",
1639
+ args: { testId },
1640
+ score: 100
1641
+ });
1642
+ }
1643
+ const level = headingLevel(attrs, role);
1644
+ const levelPart = level != null ? `, level: ${level}` : "";
1645
+ const withLevel = (base) => level != null ? { ...base, level } : base;
1646
+ if (role && accessibleName) {
1647
+ add({
1648
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(accessibleName)}${levelPart} })`,
1604
1649
  method: "getByRole",
1605
- args: { ...leafArgs, ...seg.flatArgs },
1606
- score: ANCHOR_KIND_SCORES[seg.kind]
1650
+ args: withLevel({ role, name: accessibleName }),
1651
+ score: 90 - ambiguityPenalty(counts?.roleName)
1607
1652
  });
1608
1653
  }
1609
- if (anchors.length >= 2 && chainLeafCount === 1) {
1610
- const ordered = [...anchors].sort((a, b) => b.depth - a.depth);
1611
- const segments = ordered.map(anchorSegment);
1612
- if (segments.every((s) => s !== null)) {
1613
- const innermost = segments[segments.length - 1];
1614
- const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
1615
- add({
1616
- locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
1654
+ const ariaLabel = attr(attrs, "aria-label");
1655
+ if (role && ariaLabel && ariaLabel !== accessibleName) {
1656
+ add({
1657
+ locator: `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(ariaLabel)}${levelPart} })`,
1658
+ method: "getByRole",
1659
+ args: withLevel({ role, name: ariaLabel }),
1660
+ score: 85 - ambiguityPenalty(counts?.roleName)
1661
+ });
1662
+ }
1663
+ if (accessibleName && ["input", "select", "textarea"].includes(tag)) {
1664
+ const label = attr(attrs, "aria-label");
1665
+ const labelBacked = attrs.hasLabel === void 0 ? true : attrs.hasLabel === true || label === accessibleName;
1666
+ if (labelBacked) {
1667
+ add({
1668
+ locator: `getByLabel(${quoteJsString(accessibleName)})`,
1669
+ method: "getByLabel",
1670
+ args: { label: accessibleName },
1671
+ score: 85
1672
+ });
1673
+ }
1674
+ }
1675
+ const placeholder = attr(attrs, "placeholder");
1676
+ if (placeholder) {
1677
+ add({
1678
+ locator: `getByPlaceholder(${quoteJsString(placeholder)})`,
1679
+ method: "getByPlaceholder",
1680
+ args: { placeholder },
1681
+ score: 80 - ambiguityPenalty(counts?.placeholder)
1682
+ });
1683
+ }
1684
+ if (text && text.length < 80) {
1685
+ add({
1686
+ locator: `getByText(${quoteJsString(text)})`,
1687
+ method: "getByText",
1688
+ args: { text },
1689
+ score: 75 - ambiguityPenalty(counts?.text)
1690
+ });
1691
+ }
1692
+ const id = attr(attrs, "id");
1693
+ if (id && selectable("id") && !isAutoGenerated(id) && isUnique(counts?.id)) {
1694
+ const selector = isCssSafeId(id) ? `#${id}` : `[id="${escCssAttrValue(id)}"]`;
1695
+ add({
1696
+ locator: `locator(${quoteJsString(selector)})`,
1697
+ method: "locator",
1698
+ args: { selector },
1699
+ score: 65
1700
+ });
1701
+ }
1702
+ const name = attr(attrs, "name");
1703
+ if (name && selectable("name") && isUnique(counts?.name)) {
1704
+ const selector = `[name="${escCssAttrValue(name)}"]`;
1705
+ add({
1706
+ locator: `locator(${quoteJsString(selector)})`,
1707
+ method: "locator",
1708
+ args: { selector },
1709
+ score: 60
1710
+ });
1711
+ }
1712
+ const alt = attr(attrs, "alt");
1713
+ if (alt) {
1714
+ add({
1715
+ locator: `getByAltText(${quoteJsString(alt)})`,
1716
+ method: "getByAltText",
1717
+ args: { text: alt },
1718
+ score: 60 - ambiguityPenalty(counts?.alt)
1719
+ });
1720
+ }
1721
+ const title = attr(attrs, "title");
1722
+ if (title) {
1723
+ add({
1724
+ locator: `getByTitle(${quoteJsString(title)})`,
1725
+ method: "getByTitle",
1726
+ args: { title },
1727
+ score: 50 - ambiguityPenalty(counts?.title)
1728
+ });
1729
+ }
1730
+ const hasOwnTestId = !!(testId && isUnique(counts?.testId));
1731
+ const addAnchoredChains = (leaf, scopedCount, scores) => {
1732
+ let testIdAnchorDone = false;
1733
+ let idAnchorDone = false;
1734
+ let roleAnchorDone = false;
1735
+ let dataAnchorDone = false;
1736
+ let filterAnchorDone = false;
1737
+ for (const anc of attrs.ancestors ?? []) {
1738
+ if (scopedCount(anc) !== 1) continue;
1739
+ if (!testIdAnchorDone && anc.testId && anc.testIdCount === 1) {
1740
+ testIdAnchorDone = true;
1741
+ add({
1742
+ locator: `getByTestId(${quoteJsString(anc.testId)}).${leaf.expr}`,
1743
+ method: leaf.method,
1744
+ args: { ...leaf.args, anchorTestId: anc.testId },
1745
+ score: scores.testId
1746
+ });
1747
+ }
1748
+ if (!idAnchorDone && anc.id && selectable("id") && !isAutoGenerated(anc.id) && anc.idCount === 1) {
1749
+ idAnchorDone = true;
1750
+ const anchorSelector = isCssSafeId(anc.id) ? `#${anc.id}` : `[id="${escCssAttrValue(anc.id)}"]`;
1751
+ add({
1752
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1753
+ method: leaf.method,
1754
+ args: { ...leaf.args, anchorSelector },
1755
+ score: scores.id
1756
+ });
1757
+ }
1758
+ if (!dataAnchorDone && anc.dataAttr && selectable(anc.dataAttr.name) && anc.dataAttrCount === 1) {
1759
+ dataAnchorDone = true;
1760
+ const { name: name2, value } = anc.dataAttr;
1761
+ const anchorSelector = `[${name2}="${escCssAttrValue(value)}"]`;
1762
+ add({
1763
+ locator: `locator(${quoteJsString(anchorSelector)}).${leaf.expr}`,
1764
+ method: leaf.method,
1765
+ args: { ...leaf.args, anchorSelector },
1766
+ score: TEST_DATA_ATTRS.has(name2) ? scores.testData : scores.data
1767
+ });
1768
+ }
1769
+ const ancestorRole = anc.role || TAG_TO_ROLE[anc.tag] || null;
1770
+ if (!roleAnchorDone && ancestorRole && ancestorRole !== leaf.role && anc.roleCount === 1) {
1771
+ roleAnchorDone = true;
1772
+ add({
1773
+ locator: `getByRole(${quoteJsString(ancestorRole)}).${leaf.expr}`,
1774
+ method: leaf.method,
1775
+ args: { ...leaf.args, anchorRole: ancestorRole },
1776
+ score: scores.role
1777
+ });
1778
+ }
1779
+ if (!filterAnchorDone && ancestorRole && anc.filterText && anc.filterRoleCount === 1) {
1780
+ filterAnchorDone = true;
1781
+ add({
1782
+ locator: `getByRole(${quoteJsString(ancestorRole)}).filter({ hasText: ${quoteJsString(anc.filterText)} }).${leaf.expr}`,
1783
+ method: leaf.method,
1784
+ args: { ...leaf.args, anchorRole: ancestorRole, anchorHasText: anc.filterText },
1785
+ score: scores.filter
1786
+ });
1787
+ }
1788
+ }
1789
+ };
1790
+ if (role && !hasOwnTestId) {
1791
+ const rolePart = level != null ? `${quoteJsString(role)}, { level: ${level} }` : quoteJsString(role);
1792
+ const leafArgs = withLevel({ role });
1793
+ addAnchoredChains(
1794
+ { expr: `getByRole(${rolePart})`, method: "getByRole", args: leafArgs, role },
1795
+ (anc) => anc.scopedRoleCount,
1796
+ { testId: 72, testData: 70, id: 64, data: 60, role: 55, filter: 53 }
1797
+ );
1798
+ const pos = attrs.rolePosition;
1799
+ if (pos && pos.role === role && (pos.count === 1 || level != null && pos.levelCount === 1)) {
1800
+ add({
1801
+ locator: `getByRole(${rolePart})`,
1802
+ method: "getByRole",
1803
+ args: leafArgs,
1804
+ score: 58
1805
+ });
1806
+ }
1807
+ } else if (!role && text && text.length < 80 && !hasOwnTestId) {
1808
+ addAnchoredChains(
1809
+ { expr: `getByText(${quoteJsString(text)})`, method: "getByText", args: { text }, role: null },
1810
+ (anc) => anc.scopedTextCount,
1811
+ { testId: 68, testData: 66, id: 60, data: 56, role: 51, filter: 49 }
1812
+ );
1813
+ }
1814
+ const clsStr = attr(attrs, "class");
1815
+ if (clsStr) {
1816
+ const countsClasses = counts?.classes;
1817
+ 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);
1818
+ for (const { cls, score } of classes) {
1819
+ add({
1820
+ locator: `locator(${quoteJsString(`.${cls}`)})`,
1821
+ method: "locator",
1822
+ args: { selector: `.${cls}` },
1823
+ score
1824
+ });
1825
+ }
1826
+ }
1827
+ return alts.sort((a, b) => b.score - a.score);
1828
+ }
1829
+ function approximateAccessibleName(attrs) {
1830
+ const a = attrs.attributes;
1831
+ const labelText = attrs.labelText || null;
1832
+ if (a["aria-labelledby"] && labelText) return labelText;
1833
+ const ariaLabel = a["aria-label"];
1834
+ if (ariaLabel) return ariaLabel;
1835
+ if (["input", "select", "textarea"].includes(attrs.tagName)) {
1836
+ if (labelText) return labelText;
1837
+ } else if (attrs.textContent) {
1838
+ return attrs.textContent;
1839
+ }
1840
+ const title = a["title"];
1841
+ if (title) return title;
1842
+ const placeholder = a["placeholder"];
1843
+ if (placeholder) return placeholder;
1844
+ return null;
1845
+ }
1846
+
1847
+ // ../picker-dom/src/anchor-alternatives.ts
1848
+ var cssIdSelector = (id) => /^[A-Za-z_][A-Za-z0-9_-]*$/.test(id) ? `#${id}` : `[id="${id.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"]`;
1849
+ var ANCHOR_KIND_SCORES = { testid: 80, id: 74, labeledRole: 68, role: 62 };
1850
+ function anchorSegment(a) {
1851
+ if (a.testId && a.testIdCount === 1) {
1852
+ return { code: `getByTestId(${quoteJsString(a.testId)})`, kind: "testid", flatArgs: { anchorTestId: a.testId } };
1853
+ }
1854
+ if (a.id && !isAutoGenerated(a.id) && a.idCount === 1) {
1855
+ const anchorSelector = cssIdSelector(a.id);
1856
+ return { code: `locator(${quoteJsString(anchorSelector)})`, kind: "id", flatArgs: { anchorSelector } };
1857
+ }
1858
+ if (a.role && a.ariaLabel && a.labeledRoleCount === 1) {
1859
+ return {
1860
+ code: `getByRole(${quoteJsString(a.role)}, { name: ${quoteJsString(a.ariaLabel)} })`,
1861
+ kind: "labeledRole",
1862
+ flatArgs: { anchorRole: a.role, anchorName: a.ariaLabel }
1863
+ };
1864
+ }
1865
+ if (a.role && a.roleCount === 1) {
1866
+ return { code: `getByRole(${quoteJsString(a.role)})`, kind: "role", flatArgs: { anchorRole: a.role } };
1867
+ }
1868
+ return null;
1869
+ }
1870
+ function generateAnchoredAlternatives(leaf, anchors, chainLeafCount) {
1871
+ if (!leaf.role || anchors.length === 0) return [];
1872
+ const out = [];
1873
+ const seen = /* @__PURE__ */ new Set();
1874
+ const rolePart = leaf.level != null ? `${quoteJsString(leaf.role)}, { level: ${leaf.level} }` : quoteJsString(leaf.role);
1875
+ const leafArgs = leaf.level != null ? { role: leaf.role, level: leaf.level } : { role: leaf.role };
1876
+ const add = (l) => {
1877
+ if (!seen.has(l.locator)) {
1878
+ seen.add(l.locator);
1879
+ out.push(l);
1880
+ }
1881
+ };
1882
+ for (const anchor of anchors) {
1883
+ if (anchor.scopedLeafCount !== 1) continue;
1884
+ const seg = anchorSegment(anchor);
1885
+ if (!seg) continue;
1886
+ add({
1887
+ locator: `${seg.code}.getByRole(${rolePart})`,
1888
+ method: "getByRole",
1889
+ args: { ...leafArgs, ...seg.flatArgs },
1890
+ score: ANCHOR_KIND_SCORES[seg.kind]
1891
+ });
1892
+ }
1893
+ if (anchors.length >= 2 && chainLeafCount === 1) {
1894
+ const ordered = [...anchors].sort((a, b) => b.depth - a.depth);
1895
+ const segments = ordered.map(anchorSegment);
1896
+ if (segments.every((s) => s !== null)) {
1897
+ const innermost = segments[segments.length - 1];
1898
+ const score = Math.min(...segments.map((s) => ANCHOR_KIND_SCORES[s.kind]));
1899
+ add({
1900
+ locator: `${segments.map((s) => s.code).join(".")}.getByRole(${rolePart})`,
1617
1901
  method: "getByRole",
1618
1902
  // Flat args describe the innermost anchor (what the healing fingerprint
1619
1903
  // logic understands); the full chain rides along for transparency.
@@ -1642,21 +1926,21 @@ var MAX_INDETERMINATE = 20;
1642
1926
  var STEP = String.raw`[a-zA-Z][\w.-]*:[1-9]\d*`;
1643
1927
  var LEVEL = `${STEP}(?:/${STEP})*`;
1644
1928
  var ELEMENT_PATH_RE = new RegExp(`^${LEVEL}(?:\\|${LEVEL})*$`);
1645
- function isElementPath(path14) {
1646
- return typeof path14 === "string" && path14.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path14);
1929
+ function isElementPath(path13) {
1930
+ return typeof path13 === "string" && path13.length <= ELEMENT_PATH_MAX_CHARS && ELEMENT_PATH_RE.test(path13);
1647
1931
  }
1648
1932
  var offsetOf = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= 1e7 ? v : null;
1649
1933
  function elementRef(v) {
1650
1934
  if (!v || typeof v !== "object") return null;
1651
- const { path: path14, label } = v;
1652
- if (!isElementPath(path14)) return null;
1653
- return { path: path14, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
1935
+ const { path: path13, label } = v;
1936
+ if (!isElementPath(path13)) return null;
1937
+ return { path: path13, label: typeof label === "string" ? label.slice(0, ELEMENT_LABEL_MAX_CHARS) : "" };
1654
1938
  }
1655
1939
  function textPoint(v) {
1656
1940
  if (!v || typeof v !== "object") return null;
1657
- const { path: path14, offset } = v;
1941
+ const { path: path13, offset } = v;
1658
1942
  const at = offsetOf(offset);
1659
- return isElementPath(path14) && at !== null ? { path: path14, offset: at } : null;
1943
+ return isElementPath(path13) && at !== null ? { path: path13, offset: at } : null;
1660
1944
  }
1661
1945
  function sanitizePageInteraction(raw) {
1662
1946
  if (!raw || typeof raw !== "object") return {};
@@ -1719,6 +2003,23 @@ function textSimilarity(a, b) {
1719
2003
  for (const t of sa) if (sb.has(t)) common++;
1720
2004
  return 2 * common / (sa.size + sb.size);
1721
2005
  }
2006
+ var FUNCTION_WORDS = new Set(
2007
+ "the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
2008
+ );
2009
+ function significantWords(text) {
2010
+ const all = (text ?? "").replace(new RegExp("(\\p{Ll})(\\p{Lu})", "gu"), "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
2011
+ const meaningful = all.filter((w) => w.length >= 3 && !FUNCTION_WORDS.has(w));
2012
+ return new Set(meaningful.length > 0 ? meaningful : all);
2013
+ }
2014
+ function significantWordSimilarity(a, b) {
2015
+ const sa = significantWords(a);
2016
+ const sb = significantWords(b);
2017
+ if (sa.size === 0 && sb.size === 0) return 1;
2018
+ if (sa.size === 0 || sb.size === 0) return 0;
2019
+ let common = 0;
2020
+ for (const w of sa) if (sb.has(w)) common++;
2021
+ return 2 * common / (sa.size + sb.size);
2022
+ }
1722
2023
  function fingerprintPresent(fp, candidates) {
1723
2024
  if (!fp.name) return false;
1724
2025
  return candidates.some(
@@ -1740,7 +2041,7 @@ function matchRenamedElement(fp, candidates) {
1740
2041
  let best = null;
1741
2042
  let bestScore = -1;
1742
2043
  for (const c of pool) {
1743
- const s = textSimilarity(c.name, fp.name);
2044
+ const s = significantWordSimilarity(c.name, fp.name);
1744
2045
  if (s > bestScore) {
1745
2046
  bestScore = s;
1746
2047
  best = c;
@@ -1903,7 +2204,6 @@ var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
1903
2204
  "getByTitle",
1904
2205
  "getByAltText"
1905
2206
  ]);
1906
- var escAttr = (s) => s.replaceAll("\\", "\\\\").replaceAll("'", "\\'");
1907
2207
  var SUGG_TEXT_ROLES = /* @__PURE__ */ new Set([
1908
2208
  "button",
1909
2209
  "link",
@@ -1936,9 +2236,9 @@ function failedNameAndRole(failed) {
1936
2236
  function renderFailing(failed) {
1937
2237
  const { role, name } = failedNameAndRole(failed);
1938
2238
  if (failed.method === "getByRole") {
1939
- return name ? `getByRole('${escAttr(role ?? "")}', { name: '${escAttr(name)}' })` : `getByRole('${escAttr(role ?? "")}')`;
2239
+ return name ? `getByRole(${quoteJsString(role ?? "")}, { name: ${quoteJsString(name)} })` : `getByRole(${quoteJsString(role ?? "")})`;
1940
2240
  }
1941
- return `${failed.method}('${escAttr(name ?? "")}')`;
2241
+ return `${failed.method}(${quoteJsString(name ?? "")})`;
1942
2242
  }
1943
2243
  function freshSuggestions(candidate, failedMethod) {
1944
2244
  const out = [];
@@ -1948,9 +2248,9 @@ function freshSuggestions(candidate, failedMethod) {
1948
2248
  if (!out.includes(s)) out.push(s);
1949
2249
  };
1950
2250
  const levelPart = candidate.level != null ? `, level: ${candidate.level}` : "";
1951
- const roleLoc = `getByRole('${escAttr(role)}', { name: '${escAttr(name)}'${levelPart} })`;
1952
- const textLoc = `getByText('${escAttr(name)}')`;
1953
- const labelLoc = `getByLabel('${escAttr(name)}')`;
2251
+ const roleLoc = `getByRole(${quoteJsString(role)}, { name: ${quoteJsString(name)}${levelPart} })`;
2252
+ const textLoc = `getByText(${quoteJsString(name)})`;
2253
+ const labelLoc = `getByLabel(${quoteJsString(name)})`;
1954
2254
  if (failedMethod === "getByText" && SUGG_TEXT_ROLES.has(role)) push(textLoc);
1955
2255
  if (failedMethod === "getByLabel" && SUGG_FIELD_ROLES.has(role)) push(labelLoc);
1956
2256
  push(roleLoc);
@@ -2037,6 +2337,8 @@ var ATTACHMENT_NAMES = {
2037
2337
  pageInventory: "piwi-page-inventory",
2038
2338
  locatorPages: "piwi-locator-pages",
2039
2339
  codeReach: "piwi-code-reach",
2340
+ browserCoverage: "piwi-browser-coverage",
2341
+ browserFunctions: "piwi-browser-functions",
2040
2342
  serverCoverageRequests: "piwi-server-coverage-requests",
2041
2343
  webVitals: "piwi-web-vitals",
2042
2344
  locatorSuggestion: "piwi-locator-suggestion",
@@ -2279,7 +2581,6 @@ var LocatorPageLog = class {
2279
2581
 
2280
2582
  // src/internal/probe/mode.ts
2281
2583
  var fs2 = __toESM(require("fs"));
2282
- var path3 = __toESM(require("path"));
2283
2584
 
2284
2585
  // src/internal/config/desktop.ts
2285
2586
  var fs = __toESM(require("fs"));
@@ -2416,6 +2717,17 @@ var ENV_FALLBACK_SPECS = [
2416
2717
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
2417
2718
  ];
2418
2719
 
2720
+ // src/internal/support/test-paths.ts
2721
+ var path3 = __toESM(require("path"));
2722
+ function configDirFromConfig(config) {
2723
+ if (config?.configFile) return path3.dirname(config.configFile);
2724
+ if (config?.rootDir) return config.rootDir;
2725
+ return process.cwd();
2726
+ }
2727
+ function relativeTestPath(file, base) {
2728
+ return path3.relative(base, file).split(path3.sep).join("/");
2729
+ }
2730
+
2419
2731
  // src/internal/probe/plan.ts
2420
2732
  function parseRouteKey(routeKey2) {
2421
2733
  const space = routeKey2.indexOf(" ");
@@ -2423,8 +2735,8 @@ function parseRouteKey(routeKey2) {
2423
2735
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2424
2736
  }
2425
2737
  function patternToRegex(pattern) {
2426
- const path14 = pattern.split("?")[0] ?? pattern;
2427
- const escaped = path14.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2738
+ const path13 = pattern.split("?")[0] ?? pattern;
2739
+ const escaped = path13.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2428
2740
  return new RegExp(`^${escaped}$`);
2429
2741
  }
2430
2742
  function suitePathEqual(a, b) {
@@ -2447,13 +2759,13 @@ function matchProbeItem(plan, test) {
2447
2759
  function requestMatchesRoute(method, url, routeKey2) {
2448
2760
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2449
2761
  if (rm && rm !== method.toUpperCase()) return false;
2450
- let path14;
2762
+ let path13;
2451
2763
  try {
2452
- path14 = new URL(url).pathname;
2764
+ path13 = new URL(url).pathname;
2453
2765
  } catch {
2454
- path14 = url.split("?")[0] ?? url;
2766
+ path13 = url.split("?")[0] ?? url;
2455
2767
  }
2456
- return patternToRegex(pattern).test(path14);
2768
+ return patternToRegex(pattern).test(path13);
2457
2769
  }
2458
2770
  function createProbeState() {
2459
2771
  return { navigated: false, matches: 0 };
@@ -2475,10 +2787,10 @@ function isProbeMode() {
2475
2787
  function loadPlan() {
2476
2788
  if (planCache !== void 0) return planCache;
2477
2789
  planCache = null;
2478
- const path14 = process.env[PIWI_PROBE_ENV.plan];
2479
- if (path14) {
2790
+ const path13 = process.env[PIWI_PROBE_ENV.plan];
2791
+ if (path13) {
2480
2792
  try {
2481
- planCache = JSON.parse(fs2.readFileSync(path14, "utf8"));
2793
+ planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
2482
2794
  } catch {
2483
2795
  planCache = null;
2484
2796
  }
@@ -2488,7 +2800,7 @@ function loadPlan() {
2488
2800
  function probeItemForTest(test) {
2489
2801
  const plan = loadPlan();
2490
2802
  if (!plan) return null;
2491
- const filePath = test.file ? path3.relative(process.cwd(), test.file).split(path3.sep).join("/") : null;
2803
+ const filePath = test.file ? relativeTestPath(test.file, test.base ?? process.cwd()) : null;
2492
2804
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2493
2805
  return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2494
2806
  }
@@ -2502,10 +2814,10 @@ function outcomeFromStatus(status, applied) {
2502
2814
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2503
2815
  }
2504
2816
  function recordProbeOutcome(line) {
2505
- const path14 = process.env[PIWI_PROBE_ENV.results];
2506
- if (!path14) return;
2817
+ const path13 = process.env[PIWI_PROBE_ENV.results];
2818
+ if (!path13) return;
2507
2819
  try {
2508
- fs2.appendFileSync(path14, `${JSON.stringify(line)}
2820
+ fs2.appendFileSync(path13, `${JSON.stringify(line)}
2509
2821
  `);
2510
2822
  } catch {
2511
2823
  }
@@ -2518,519 +2830,236 @@ var import_node_zlib = require("zlib");
2518
2830
  var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
2519
2831
  var SERVER_PROBE_FAULTS = [
2520
2832
  "throw",
2521
- "status",
2522
- "delay",
2523
- "dependency",
2524
- "data",
2525
- "extreme",
2526
- "replay",
2527
- "auth",
2528
- "slow-first"
2529
- ];
2530
- function isFaultAllowed(level, fault) {
2531
- const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
2532
- return list.includes(fault);
2533
- }
2534
- var SLOW_FAULT_DELAY_MS = 5e3;
2535
- function faultRouteAction(fault) {
2536
- return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
2537
- }
2538
- function dropOneField(body) {
2539
- let parsed2;
2540
- try {
2541
- parsed2 = JSON.parse(body);
2542
- } catch {
2543
- return body;
2544
- }
2545
- if (Array.isArray(parsed2)) {
2546
- let key;
2547
- for (const el of parsed2) {
2548
- if (el && typeof el === "object" && !Array.isArray(el)) {
2549
- key ??= Object.keys(el)[0];
2550
- if (key) delete el[key];
2551
- }
2552
- }
2553
- return JSON.stringify(parsed2);
2554
- }
2555
- if (parsed2 && typeof parsed2 === "object") {
2556
- const obj = parsed2;
2557
- const key = Object.keys(obj)[0];
2558
- if (key) delete obj[key];
2559
- return JSON.stringify(obj);
2560
- }
2561
- return body;
2562
- }
2563
- function computeFault(fault, input) {
2564
- const base = {
2565
- status: input.status,
2566
- body: input.body,
2567
- contentType: input.contentType ?? null,
2568
- delayMs: 0,
2569
- changed: false
2570
- };
2571
- switch (fault) {
2572
- case "status-500":
2573
- return { ...base, status: 500, changed: input.status !== 500 };
2574
- case "empty-body":
2575
- return { ...base, body: "", changed: input.body.length > 0 };
2576
- case "drop-field": {
2577
- const body = dropOneField(input.body);
2578
- return { ...base, body, changed: body !== input.body };
2579
- }
2580
- case "stale-value": {
2581
- const body = input.priorBody ?? input.body;
2582
- return { ...base, body, changed: body !== input.body };
2583
- }
2584
- case "slow":
2585
- return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
2586
- default:
2587
- return base;
2588
- }
2589
- }
2590
-
2591
- // src/internal/probe/sign.ts
2592
- var import_node_crypto = require("crypto");
2593
- function signProbeMessage(secret, nonce, ts, specJson) {
2594
- return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
2595
- }
2596
- function buildProbeHeader(secret, spec, now = Date.now()) {
2597
- const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
2598
- const specJson = JSON.stringify(spec);
2599
- const sig = signProbeMessage(secret, nonce, now, specJson);
2600
- return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
2601
- }
2602
-
2603
- // src/internal/probe/interception.ts
2604
- function parseProbeTrace(traceHeader) {
2605
- const none = { appliedFault: null, serverError: false };
2606
- if (!traceHeader) return none;
2607
- try {
2608
- const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
2609
- if (!Array.isArray(spans)) return none;
2610
- const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
2611
- if (!root) return none;
2612
- const attrs = root.attrs ?? {};
2613
- const appliedRaw = attrs["piwi.probe.applied"];
2614
- const status = Number(attrs["http.status_code"]) || 0;
2615
- return {
2616
- appliedFault: appliedRaw != null ? String(appliedRaw) : null,
2617
- serverError: root.status === "error" || status >= 500
2618
- };
2619
- } catch {
2620
- return none;
2621
- }
2622
- }
2623
- function fulfillHeaders(responseHeaders, contentType) {
2624
- const headers = { ...responseHeaders };
2625
- delete headers["content-length"];
2626
- delete headers["content-encoding"];
2627
- if (contentType) headers["content-type"] = contentType;
2628
- return headers;
2629
- }
2630
- function originOf(url) {
2631
- try {
2632
- const origin = new URL(url).origin;
2633
- return origin && origin !== "null" ? origin : null;
2634
- } catch {
2635
- return null;
2636
- }
2637
- }
2638
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
2639
- async function performRouteAction(route, action, startedAt) {
2640
- switch (action.kind) {
2641
- case "abort":
2642
- await route.abort("connectionreset");
2643
- return;
2644
- case "status":
2645
- await route.fulfill({ status: action.status, body: "" });
2646
- return;
2647
- case "delay": {
2648
- const response = await route.fetch();
2649
- await sleep(startedAt + action.ms - Date.now());
2650
- await route.fulfill({ response });
2651
- return;
2652
- }
2653
- }
2654
- }
2655
- async function installRouteRules(page, rules) {
2656
- const state = rules.map(() => createProbeState());
2657
- page.on("framenavigated", (frame) => {
2658
- if (frame === page.mainFrame()) for (const s of state) markNavigated(s);
2659
- });
2660
- await page.route("**/*", async (route) => {
2661
- const startedAt = Date.now();
2662
- const request = route.request();
2663
- for (let i = 0; i < rules.length; i++) {
2664
- const rule = rules[i];
2665
- if (rule.done?.() || !rule.matches(request.method(), request.url())) continue;
2666
- if (!shouldAct(state[i], rule.match)) continue;
2667
- await rule.handle(route, startedAt);
2668
- return;
2669
- }
2670
- await route.fallback();
2671
- });
2672
- return { state };
2673
- }
2674
- async function installProbeInterception(page, item) {
2675
- if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2676
- return { applied: () => false, appliedFault: () => null, serverError: () => false };
2677
- }
2678
- let applied = false;
2679
- let appliedFault = null;
2680
- let serverError = false;
2681
- let appOrigin = null;
2682
- const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2683
- page.on("framenavigated", (frame) => {
2684
- if (frame === page.mainFrame()) {
2685
- const origin = originOf(page.url());
2686
- if (origin) appOrigin = origin;
2687
- }
2688
- });
2689
- const handle = async (route, startedAt) => {
2690
- const request = route.request();
2691
- if (item.level === "server") {
2692
- if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
2693
- await route.fallback();
2694
- return;
2695
- }
2696
- const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
2697
- if (item.dependency) spec.dependency = item.dependency;
2698
- try {
2699
- const response = await route.fetch({
2700
- headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
2701
- });
2702
- const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
2703
- appliedFault = trace.appliedFault;
2704
- serverError = trace.serverError;
2705
- applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
2706
- await route.fulfill({ response });
2707
- } catch {
2708
- applied = false;
2709
- await route.fallback();
2710
- }
2711
- return;
2712
- }
2713
- try {
2714
- const action = faultRouteAction(item.fault);
2715
- if (action) {
2716
- applied = true;
2717
- appliedFault = item.fault;
2718
- await performRouteAction(route, action, startedAt);
2719
- return;
2720
- }
2721
- const response = await route.fetch();
2722
- const body = await response.text();
2723
- const out = computeFault(item.fault, {
2724
- status: response.status(),
2725
- body,
2726
- contentType: response.headers()["content-type"] ?? null
2727
- });
2728
- applied = out.changed;
2729
- if (applied) appliedFault = item.fault;
2730
- const headers = fulfillHeaders(response.headers(), out.contentType);
2731
- await route.fulfill({ status: out.status, body: out.body, headers });
2732
- } catch {
2733
- applied = false;
2734
- await route.fallback();
2735
- }
2736
- };
2737
- await installRouteRules(page, [
2738
- {
2739
- matches: (method, url) => requestMatchesRoute(method, url, item.routeKey),
2740
- match: Math.max(1, item.nth ?? 1),
2741
- handle,
2742
- done: () => applied
2743
- }
2744
- ]);
2745
- return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2746
- }
2747
-
2748
- // src/internal/flake/mode.ts
2749
- var fs3 = __toESM(require("fs"));
2750
- var path4 = __toESM(require("path"));
2751
-
2752
- // ../core/src/locator-chain.ts
2753
- var LOCATING_METHODS = /* @__PURE__ */ new Set([
2754
- "getByRole",
2755
- "getByTestId",
2756
- "getByText",
2757
- "getByLabel",
2758
- "getByPlaceholder",
2759
- "getByAltText",
2760
- "getByTitle",
2761
- "locator",
2762
- "frameLocator"
2763
- ]);
2764
- var NARROWING_METHODS = /* @__PURE__ */ new Set([
2765
- "filter",
2766
- "first",
2767
- "last",
2768
- "nth",
2769
- "and",
2770
- "or",
2771
- "visible",
2772
- "contentFrame",
2773
- "owner"
2774
- ]);
2775
- var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
2776
- var LocatorParseError = class extends Error {
2777
- constructor(message) {
2778
- super(message);
2779
- this.name = "LocatorParseError";
2780
- }
2781
- };
2782
- var SIMPLE_ESCAPES = {
2783
- n: "\n",
2784
- t: " ",
2785
- r: "\r",
2786
- b: "\b",
2787
- f: "\f",
2788
- v: "\v",
2789
- "0": "\0"
2790
- };
2791
- var Parser = class {
2792
- constructor(src) {
2793
- this.src = src;
2794
- this.i = 0;
2795
- }
2796
- parseTopLevel() {
2797
- const chain = this.parseChain();
2798
- this.skipSpace();
2799
- if (this.i < this.src.length) this.fail("unexpected text");
2800
- return chain;
2801
- }
2802
- fail(message) {
2803
- throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
2804
- }
2805
- skipSpace() {
2806
- while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
2807
- }
2808
- peek() {
2809
- return this.src[this.i];
2810
- }
2811
- expect(ch) {
2812
- this.skipSpace();
2813
- if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
2814
- this.i++;
2815
- }
2816
- identifier() {
2817
- const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
2818
- if (!m) this.fail("expected a name");
2819
- this.i += m[0].length;
2820
- return m[0];
2821
- }
2822
- /**
2823
- * A chain of calls joined by `.`. With `lenient`, parsing stops before the
2824
- * first link that doesn't parse instead of failing, so a chain followed by
2825
- * other text (an error message) is read up to where it ends. `spans` receives
2826
- * where each top-level call starts and ends.
2827
- */
2828
- parseChain(opts = {}) {
2829
- this.skipSpace();
2830
- const calls = [];
2831
- for (; ; ) {
2832
- const before = this.i;
2833
- if (calls.length > 0) this.i++;
2834
- this.skipSpace();
2835
- const start = this.i;
2836
- try {
2837
- calls.push(this.parseCall(calls.length === 0));
2838
- } catch (error) {
2839
- if (!opts.lenient || calls.length === 0) throw error;
2840
- this.i = before;
2841
- break;
2842
- }
2843
- opts.spans?.push({ start, end: this.i });
2844
- const end = this.i;
2845
- this.skipSpace();
2846
- if (this.peek() !== ".") {
2847
- this.i = end;
2848
- break;
2849
- }
2850
- }
2851
- return { calls };
2852
- }
2853
- /** Where the parser stopped. */
2854
- get position() {
2855
- return this.i;
2856
- }
2857
- parseCall(first) {
2858
- this.skipSpace();
2859
- const method = this.identifier();
2860
- if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
2861
- if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
2862
- this.expect("(");
2863
- const args = [];
2864
- this.skipSpace();
2865
- if (this.peek() === ")") {
2866
- this.i++;
2867
- return { method, args };
2868
- }
2869
- for (; ; ) {
2870
- args.push(this.parseValue());
2871
- this.skipSpace();
2872
- if (this.peek() === ",") {
2873
- this.i++;
2874
- this.skipSpace();
2875
- if (this.peek() === ")") {
2876
- this.i++;
2877
- break;
2878
- }
2879
- continue;
2880
- }
2881
- this.expect(")");
2882
- break;
2883
- }
2884
- return { method, args };
2885
- }
2886
- parseValue() {
2887
- this.skipSpace();
2888
- const ch = this.peek();
2889
- if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
2890
- if (ch === "/") return this.parseRegex();
2891
- if (ch === "{") return this.parseObject();
2892
- if (ch !== void 0 && /[-\d]/.test(ch)) {
2893
- const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
2894
- if (!m) this.fail("expected a number");
2895
- this.i += m[0].length;
2896
- return { type: "number", value: Number(m[0]) };
2897
- }
2898
- const rest = this.src.slice(this.i);
2899
- if (/^true\b/.test(rest)) {
2900
- this.i += 4;
2901
- return { type: "boolean", value: true };
2902
- }
2903
- if (/^false\b/.test(rest)) {
2904
- this.i += 5;
2905
- return { type: "boolean", value: false };
2906
- }
2907
- if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
2908
- return this.fail("expected a value");
2909
- }
2910
- /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
2911
- parseString() {
2912
- const quote = this.src[this.i];
2913
- let out = "";
2914
- this.i++;
2915
- while (this.i < this.src.length) {
2916
- const c = this.src[this.i];
2917
- if (c === quote) {
2918
- this.i++;
2919
- return out;
2920
- }
2921
- if (c !== "\\") {
2922
- out += c;
2923
- this.i++;
2924
- continue;
2925
- }
2926
- const next = this.src[this.i + 1];
2927
- if (next === void 0) break;
2928
- const simple = SIMPLE_ESCAPES[next];
2929
- if (simple !== void 0) {
2930
- out += simple;
2931
- this.i += 2;
2932
- continue;
2933
- }
2934
- const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
2935
- if (hex) {
2936
- out += String.fromCodePoint(parseInt(hex[1], 16));
2937
- this.i += 1 + hex[0].length;
2938
- continue;
2939
- }
2940
- if (next === "u" || next === "x") this.fail("malformed escape");
2941
- out += next;
2942
- this.i += 2;
2943
- }
2944
- return this.fail("unterminated string");
2833
+ "status",
2834
+ "delay",
2835
+ "dependency",
2836
+ "data",
2837
+ "extreme",
2838
+ "replay",
2839
+ "auth",
2840
+ "slow-first"
2841
+ ];
2842
+ function isFaultAllowed(level, fault) {
2843
+ const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
2844
+ return list.includes(fault);
2845
+ }
2846
+ var SLOW_FAULT_DELAY_MS = 5e3;
2847
+ function faultRouteAction(fault) {
2848
+ return fault === "slow" ? { kind: "delay", ms: SLOW_FAULT_DELAY_MS } : null;
2849
+ }
2850
+ function dropOneField(body) {
2851
+ let parsed2;
2852
+ try {
2853
+ parsed2 = JSON.parse(body);
2854
+ } catch {
2855
+ return body;
2945
2856
  }
2946
- parseRegex() {
2947
- let j = this.i + 1;
2948
- let inClass = false;
2949
- while (j < this.src.length) {
2950
- const c = this.src[j];
2951
- if (c === "\\") {
2952
- j += 2;
2953
- continue;
2857
+ if (Array.isArray(parsed2)) {
2858
+ let key;
2859
+ for (const el of parsed2) {
2860
+ if (el && typeof el === "object" && !Array.isArray(el)) {
2861
+ key ??= Object.keys(el)[0];
2862
+ if (key) delete el[key];
2954
2863
  }
2955
- if (c === "[") inClass = true;
2956
- else if (c === "]") inClass = false;
2957
- else if (c === "/" && !inClass) break;
2958
- j++;
2959
2864
  }
2960
- if (j >= this.src.length) this.fail("unterminated regex");
2961
- const source = this.src.slice(this.i + 1, j);
2962
- const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
2963
- this.i = j + 1 + flags.length;
2964
- return { type: "regex", source, flags };
2865
+ return JSON.stringify(parsed2);
2965
2866
  }
2966
- parseObject() {
2967
- this.expect("{");
2968
- const entries = [];
2969
- for (; ; ) {
2970
- this.skipSpace();
2971
- if (this.peek() === "}") {
2972
- this.i++;
2973
- break;
2974
- }
2975
- const ch = this.peek();
2976
- const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
2977
- this.expect(":");
2978
- entries.push([key, this.parseValue()]);
2979
- this.skipSpace();
2980
- if (this.peek() === ",") {
2981
- this.i++;
2982
- continue;
2983
- }
2984
- this.expect("}");
2985
- break;
2867
+ if (parsed2 && typeof parsed2 === "object") {
2868
+ const obj = parsed2;
2869
+ const key = Object.keys(obj)[0];
2870
+ if (key) delete obj[key];
2871
+ return JSON.stringify(obj);
2872
+ }
2873
+ return body;
2874
+ }
2875
+ function computeFault(fault, input) {
2876
+ const base = {
2877
+ status: input.status,
2878
+ body: input.body,
2879
+ contentType: input.contentType ?? null,
2880
+ delayMs: 0,
2881
+ changed: false
2882
+ };
2883
+ switch (fault) {
2884
+ case "status-500":
2885
+ return { ...base, status: 500, changed: input.status !== 500 };
2886
+ case "empty-body":
2887
+ return { ...base, body: "", changed: input.body.length > 0 };
2888
+ case "drop-field": {
2889
+ const body = dropOneField(input.body);
2890
+ return { ...base, body, changed: body !== input.body };
2986
2891
  }
2987
- return { type: "object", entries };
2892
+ case "stale-value": {
2893
+ const body = input.priorBody ?? input.body;
2894
+ return { ...base, body, changed: body !== input.body };
2895
+ }
2896
+ case "slow":
2897
+ return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
2898
+ default:
2899
+ return base;
2988
2900
  }
2989
- };
2990
- function parseLocatorChain(expr) {
2991
- const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
2992
- if (!trimmed) throw new LocatorParseError("empty expression");
2993
- return new Parser(trimmed).parseTopLevel();
2994
2901
  }
2995
- function tryParseLocatorChain(expr) {
2902
+
2903
+ // src/internal/probe/sign.ts
2904
+ var import_node_crypto = require("crypto");
2905
+ function signProbeMessage(secret, nonce, ts, specJson) {
2906
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
2907
+ }
2908
+ function buildProbeHeader(secret, spec, now = Date.now()) {
2909
+ const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
2910
+ const specJson = JSON.stringify(spec);
2911
+ const sig = signProbeMessage(secret, nonce, now, specJson);
2912
+ return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
2913
+ }
2914
+
2915
+ // src/internal/probe/interception.ts
2916
+ function parseProbeTrace(traceHeader) {
2917
+ const none = { appliedFault: null, serverError: false };
2918
+ if (!traceHeader) return none;
2996
2919
  try {
2997
- return parseLocatorChain(expr);
2920
+ const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
2921
+ if (!Array.isArray(spans)) return none;
2922
+ const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
2923
+ if (!root) return none;
2924
+ const attrs = root.attrs ?? {};
2925
+ const appliedRaw = attrs["piwi.probe.applied"];
2926
+ const status = Number(attrs["http.status_code"]) || 0;
2927
+ return {
2928
+ appliedFault: appliedRaw != null ? String(appliedRaw) : null,
2929
+ serverError: root.status === "error" || status >= 500
2930
+ };
2931
+ } catch {
2932
+ return none;
2933
+ }
2934
+ }
2935
+ function fulfillHeaders(responseHeaders, contentType) {
2936
+ const headers = { ...responseHeaders };
2937
+ delete headers["content-length"];
2938
+ delete headers["content-encoding"];
2939
+ if (contentType) headers["content-type"] = contentType;
2940
+ return headers;
2941
+ }
2942
+ function originOf(url) {
2943
+ try {
2944
+ const origin = new URL(url).origin;
2945
+ return origin && origin !== "null" ? origin : null;
2998
2946
  } catch {
2999
2947
  return null;
3000
2948
  }
3001
2949
  }
3002
- function parseLeafLocatorCall(expr) {
3003
- const chain = tryParseLocatorChain(expr);
3004
- if (!chain) return null;
3005
- for (let i = chain.calls.length - 1; i >= 0; i--) {
3006
- if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
2950
+ var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
2951
+ async function performRouteAction(route, action, startedAt) {
2952
+ switch (action.kind) {
2953
+ case "abort":
2954
+ await route.abort("connectionreset");
2955
+ return;
2956
+ case "status":
2957
+ await route.fulfill({ status: action.status, body: "" });
2958
+ return;
2959
+ case "delay": {
2960
+ const response = await route.fetch();
2961
+ await sleep(startedAt + action.ms - Date.now());
2962
+ await route.fulfill({ response });
2963
+ return;
2964
+ }
3007
2965
  }
3008
- return null;
3009
2966
  }
3010
- function locatorCallValues(call, opts = {}) {
3011
- const value = (arg) => {
3012
- switch (arg.type) {
3013
- case "string":
3014
- case "number":
3015
- case "boolean":
3016
- return arg.value;
3017
- case "regex":
3018
- return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
3019
- case "object": {
3020
- const out = {};
3021
- for (const [key, v] of arg.entries) {
3022
- const plain = value(v);
3023
- if (plain !== void 0) out[key] = plain;
3024
- }
3025
- return out;
2967
+ async function installRouteRules(page, rules) {
2968
+ const state = rules.map(() => createProbeState());
2969
+ page.on("framenavigated", (frame) => {
2970
+ if (frame === page.mainFrame()) for (const s of state) markNavigated(s);
2971
+ });
2972
+ await page.route("**/*", async (route) => {
2973
+ const startedAt = Date.now();
2974
+ const request = route.request();
2975
+ for (let i = 0; i < rules.length; i++) {
2976
+ const rule = rules[i];
2977
+ if (rule.done?.() || !rule.matches(request.method(), request.url())) continue;
2978
+ if (!shouldAct(state[i], rule.match)) continue;
2979
+ await rule.handle(route, startedAt);
2980
+ return;
2981
+ }
2982
+ await route.fallback();
2983
+ });
2984
+ return { state };
2985
+ }
2986
+ async function installProbeInterception(page, item) {
2987
+ if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2988
+ return { applied: () => false, appliedFault: () => null, serverError: () => false };
2989
+ }
2990
+ let applied = false;
2991
+ let appliedFault = null;
2992
+ let serverError = false;
2993
+ let appOrigin = null;
2994
+ const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2995
+ page.on("framenavigated", (frame) => {
2996
+ if (frame === page.mainFrame()) {
2997
+ const origin = originOf(page.url());
2998
+ if (origin) appOrigin = origin;
2999
+ }
3000
+ });
3001
+ const handle = async (route, startedAt) => {
3002
+ const request = route.request();
3003
+ if (item.level === "server") {
3004
+ if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
3005
+ await route.fallback();
3006
+ return;
3026
3007
  }
3027
- case "chain":
3028
- return void 0;
3008
+ const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
3009
+ if (item.dependency) spec.dependency = item.dependency;
3010
+ try {
3011
+ const response = await route.fetch({
3012
+ headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
3013
+ });
3014
+ const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
3015
+ appliedFault = trace.appliedFault;
3016
+ serverError = trace.serverError;
3017
+ applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
3018
+ await route.fulfill({ response });
3019
+ } catch {
3020
+ applied = false;
3021
+ await route.fallback();
3022
+ }
3023
+ return;
3024
+ }
3025
+ try {
3026
+ const action = faultRouteAction(item.fault);
3027
+ if (action) {
3028
+ applied = true;
3029
+ appliedFault = item.fault;
3030
+ await performRouteAction(route, action, startedAt);
3031
+ return;
3032
+ }
3033
+ const response = await route.fetch();
3034
+ const body = await response.text();
3035
+ const out = computeFault(item.fault, {
3036
+ status: response.status(),
3037
+ body,
3038
+ contentType: response.headers()["content-type"] ?? null
3039
+ });
3040
+ applied = out.changed;
3041
+ if (applied) appliedFault = item.fault;
3042
+ const headers = fulfillHeaders(response.headers(), out.contentType);
3043
+ await route.fulfill({ status: out.status, body: out.body, headers });
3044
+ } catch {
3045
+ applied = false;
3046
+ await route.fallback();
3029
3047
  }
3030
3048
  };
3031
- return call.args.map(value).filter((v) => v !== void 0);
3049
+ await installRouteRules(page, [
3050
+ {
3051
+ matches: (method, url) => requestMatchesRoute(method, url, item.routeKey),
3052
+ match: Math.max(1, item.nth ?? 1),
3053
+ handle,
3054
+ done: () => applied
3055
+ }
3056
+ ]);
3057
+ return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
3032
3058
  }
3033
3059
 
3060
+ // src/internal/flake/mode.ts
3061
+ var fs3 = __toESM(require("fs"));
3062
+
3034
3063
  // ../core/src/error-parse.ts
3035
3064
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
3036
3065
  function stripAnsi(text) {
@@ -3053,11 +3082,20 @@ function extractMessageHead(text) {
3053
3082
  let head = text;
3054
3083
  const callLogIdx = head.indexOf("\nCall log:");
3055
3084
  if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
3056
- const stackIdx = head.search(/\n\s+at /);
3085
+ const stackIdx = stackStart(head);
3057
3086
  if (stackIdx !== -1) head = head.slice(0, stackIdx);
3058
3087
  const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
3059
3088
  return lines.slice(0, 5).join("\n");
3060
3089
  }
3090
+ function stackStart(text) {
3091
+ const runs = /\s+/g;
3092
+ for (let run = runs.exec(text); run; run = runs.exec(text)) {
3093
+ if (!text.startsWith("at ", runs.lastIndex)) continue;
3094
+ const lineBreak = run[0].indexOf("\n");
3095
+ if (lineBreak !== -1 && lineBreak < run[0].length - 1) return run.index + lineBreak;
3096
+ }
3097
+ return -1;
3098
+ }
3061
3099
  function extractSelector(text) {
3062
3100
  const match = SELECTOR_FN_RE.exec(text);
3063
3101
  if (!match) return null;
@@ -3078,7 +3116,7 @@ function extractSelector(text) {
3078
3116
  }
3079
3117
  var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
3080
3118
  function extractStackFrames(text) {
3081
- const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
3119
+ const frameRe = /^[ \t]+at (?:.*? \()?([^()\s][^()\n]*?):(\d+):(\d+)\)?[ \t]*$/gm;
3082
3120
  const frames = [];
3083
3121
  let m;
3084
3122
  while ((m = frameRe.exec(text)) !== null) {
@@ -3096,6 +3134,11 @@ function extractTopFrameFile(text) {
3096
3134
  }
3097
3135
  var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
3098
3136
  var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
3137
+ function readLocatorHeader(text) {
3138
+ const header = /^[ \t]*Locator:[ \t]*(.+)$/m.exec(text);
3139
+ return header ? header[1].trim() : null;
3140
+ }
3141
+ var MAX_PARSED_ERROR_CHARS = 64 * 1024;
3099
3142
 
3100
3143
  // ../core/src/error-signature.ts
3101
3144
  var UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
@@ -3338,12 +3381,12 @@ function loadFlakePlan() {
3338
3381
  throw new Error(`[Piwi Dashboard] ${errorMessage(error)} (${file})`);
3339
3382
  }
3340
3383
  }
3341
- function relativeTestFile(file) {
3342
- return file ? path4.relative(process.cwd(), file).split(path4.sep).join("/") : null;
3384
+ function relativeTestFile(file, base = process.cwd()) {
3385
+ return file ? relativeTestPath(file, base) : null;
3343
3386
  }
3344
3387
  function flakeRoleForTest(plan, test) {
3345
3388
  const running = {
3346
- file: relativeTestFile(test.file),
3389
+ file: relativeTestFile(test.file, test.base),
3347
3390
  title: test.title,
3348
3391
  suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
3349
3392
  };
@@ -3459,7 +3502,7 @@ function flakeResultLine(plan, attempt) {
3459
3502
  experimentId: plan.experimentId,
3460
3503
  armId: plan.arm.id,
3461
3504
  role: attempt.role,
3462
- file: relativeTestFile(attempt.file),
3505
+ file: relativeTestFile(attempt.file, attempt.base),
3463
3506
  title: attempt.title,
3464
3507
  project: attempt.project,
3465
3508
  browserName: attempt.browserName,
@@ -3485,20 +3528,468 @@ function recordFlakeResult(line) {
3485
3528
  }
3486
3529
  }
3487
3530
 
3488
- // ../core/src/error-text.ts
3489
- function joinErrorMessages(errors) {
3490
- if (!errors || errors.length === 0) return null;
3491
- const seen = /* @__PURE__ */ new Set();
3492
- const parts = [];
3493
- for (const err of errors) {
3494
- const msg = err.message ?? "";
3495
- if (!msg) continue;
3496
- const dedupeKey = msg.trim().slice(0, 200);
3497
- if (seen.has(dedupeKey)) continue;
3498
- seen.add(dedupeKey);
3499
- parts.push(msg);
3500
- }
3501
- return parts.join("\n---\n") || null;
3531
+ // ../core/src/error-text.ts
3532
+ function joinErrorMessages(errors) {
3533
+ if (!errors || errors.length === 0) return null;
3534
+ const seen = /* @__PURE__ */ new Set();
3535
+ const parts = [];
3536
+ for (const err of errors) {
3537
+ const msg = err.message ?? "";
3538
+ if (!msg) continue;
3539
+ const dedupeKey = msg.trim().slice(0, 200);
3540
+ if (seen.has(dedupeKey)) continue;
3541
+ seen.add(dedupeKey);
3542
+ parts.push(msg);
3543
+ }
3544
+ return parts.join("\n---\n") || null;
3545
+ }
3546
+
3547
+ // ../core/src/coverage.ts
3548
+ var MAX_COVERAGE_FILES = 2e3;
3549
+ var MAX_COVERAGE_RANGES = 1e4;
3550
+ var MAX_LINE = 1e6;
3551
+ var MAX_PATH_LENGTH = 500;
3552
+ var BLOB_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
3553
+ function isRecord(value) {
3554
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3555
+ }
3556
+ function isRuntime(value) {
3557
+ return value === "browser" || value === "server";
3558
+ }
3559
+ function isLine(value) {
3560
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_LINE;
3561
+ }
3562
+ function validRanges(raw) {
3563
+ if (!Array.isArray(raw)) return [];
3564
+ const out = [];
3565
+ for (const item of raw) {
3566
+ if (!Array.isArray(item) || item.length !== 2) continue;
3567
+ const [start, end] = item;
3568
+ if (isLine(start) && isLine(end) && start <= end) out.push([start, end]);
3569
+ }
3570
+ return out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
3571
+ }
3572
+ function normalizeLineRanges(ranges) {
3573
+ const out = [];
3574
+ for (const range of validRanges(ranges)) {
3575
+ const last = out[out.length - 1];
3576
+ if (last && range[0] <= last[1] + 1) last[1] = Math.max(last[1], range[1]);
3577
+ else out.push(range);
3578
+ }
3579
+ return out;
3580
+ }
3581
+ function normalizeFunctionRanges(ranges) {
3582
+ const out = [];
3583
+ for (const range of validRanges(ranges)) {
3584
+ const last = out[out.length - 1];
3585
+ if (!last || last[0] !== range[0] || last[1] !== range[1]) out.push(range);
3586
+ }
3587
+ return out;
3588
+ }
3589
+ function normalizeCoveragePath(raw) {
3590
+ if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
3591
+ const path13 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3592
+ if (!path13 || path13.startsWith("../") || path13.startsWith("/") || /^[A-Za-z]:\//.test(path13)) return null;
3593
+ if (/(^|\/)node_modules\//.test(path13)) return null;
3594
+ return path13;
3595
+ }
3596
+ function fileEntry(path13, runtime, blob, functions = [], lines = []) {
3597
+ const file = { path: path13, runtime };
3598
+ if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
3599
+ if (functions.length > 0) file.functions = functions;
3600
+ if (lines.length > 0) file.lines = lines;
3601
+ return file;
3602
+ }
3603
+ function captureOf(files, incomplete) {
3604
+ return incomplete ? { v: 1, files, incomplete: true } : { v: 1, files };
3605
+ }
3606
+ function compareFiles(a, b) {
3607
+ if (a.path !== b.path) return a.path < b.path ? -1 : 1;
3608
+ if (a.runtime !== b.runtime) return a.runtime < b.runtime ? -1 : 1;
3609
+ return 0;
3610
+ }
3611
+ function sanitizeCoverage(raw) {
3612
+ if (!isRecord(raw) || raw.v !== 1 || !Array.isArray(raw.files)) return null;
3613
+ const files = [];
3614
+ for (const item of raw.files) {
3615
+ if (!isRecord(item) || !isRuntime(item.runtime)) continue;
3616
+ const path13 = normalizeCoveragePath(item.path);
3617
+ if (path13 === null) continue;
3618
+ const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
3619
+ files.push(
3620
+ fileEntry(path13, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3621
+ );
3622
+ }
3623
+ const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
3624
+ if (!merged) return null;
3625
+ let incomplete = merged.incomplete === true || merged.files.length > MAX_COVERAGE_FILES;
3626
+ let budget = MAX_COVERAGE_RANGES;
3627
+ const capped = merged.files.slice(0, MAX_COVERAGE_FILES).map((file) => {
3628
+ const count = (file.functions?.length ?? 0) + (file.lines?.length ?? 0);
3629
+ if (count <= budget) {
3630
+ budget -= count;
3631
+ return file;
3632
+ }
3633
+ incomplete = true;
3634
+ return fileEntry(file.path, file.runtime);
3635
+ });
3636
+ return captureOf(capped, incomplete);
3637
+ }
3638
+ function mergeCoverage(...captures2) {
3639
+ const entries = /* @__PURE__ */ new Map();
3640
+ let incomplete = false;
3641
+ for (const capture of captures2) {
3642
+ if (!capture) continue;
3643
+ if (capture.incomplete) incomplete = true;
3644
+ for (const file of capture.files) {
3645
+ const key = `${file.runtime}:${file.path}`;
3646
+ let entry = entries.get(key);
3647
+ if (!entry) {
3648
+ entry = { path: file.path, runtime: file.runtime, blobs: /* @__PURE__ */ new Set(), functions: [], lines: [] };
3649
+ entries.set(key, entry);
3650
+ }
3651
+ if (file.blob) entry.blobs.add(file.blob);
3652
+ for (const range of file.functions ?? []) entry.functions.push(range);
3653
+ for (const range of file.lines ?? []) entry.lines.push(range);
3654
+ }
3655
+ }
3656
+ if (entries.size === 0) return null;
3657
+ const files = [...entries.values()].map((entry) => {
3658
+ if (entry.blobs.size > 1) incomplete = true;
3659
+ const blob = entry.blobs.size === 1 ? [...entry.blobs][0] : void 0;
3660
+ return fileEntry(
3661
+ entry.path,
3662
+ entry.runtime,
3663
+ blob,
3664
+ normalizeFunctionRanges(entry.functions),
3665
+ normalizeLineRanges(entry.lines)
3666
+ );
3667
+ });
3668
+ return captureOf(files.sort(compareFiles), incomplete);
3669
+ }
3670
+
3671
+ // ../core/src/source-map.ts
3672
+ var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3673
+ var BASE64_VALUE = new Int8Array(128).fill(-1);
3674
+ for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;
3675
+ function decodeMappings(mappings, sourceBase) {
3676
+ const lines = [];
3677
+ let current = { columns: [], sources: [], originalLines: [] };
3678
+ let column = 0;
3679
+ let source = 0;
3680
+ let originalLine = 0;
3681
+ const fields = [];
3682
+ let value = 0;
3683
+ let shift = 0;
3684
+ const flush = () => {
3685
+ if (fields.length === 0) return;
3686
+ column += fields[0];
3687
+ if (fields.length >= 4) {
3688
+ source += fields[1];
3689
+ originalLine += fields[2];
3690
+ current.columns.push(column);
3691
+ current.sources.push(sourceBase + source);
3692
+ current.originalLines.push(originalLine);
3693
+ } else {
3694
+ current.columns.push(column);
3695
+ current.sources.push(-1);
3696
+ current.originalLines.push(-1);
3697
+ }
3698
+ fields.length = 0;
3699
+ };
3700
+ for (let i = 0; i <= mappings.length; i++) {
3701
+ const ch = i < mappings.length ? mappings.charCodeAt(i) : 59;
3702
+ if (ch === 59 || ch === 44) {
3703
+ flush();
3704
+ if (ch === 59) {
3705
+ lines.push(current);
3706
+ current = { columns: [], sources: [], originalLines: [] };
3707
+ column = 0;
3708
+ }
3709
+ continue;
3710
+ }
3711
+ const digit = ch < 128 ? BASE64_VALUE[ch] : -1;
3712
+ if (digit < 0) continue;
3713
+ value += (digit & 31) << shift;
3714
+ if (digit & 32) {
3715
+ shift += 5;
3716
+ continue;
3717
+ }
3718
+ fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
3719
+ value = 0;
3720
+ shift = 0;
3721
+ }
3722
+ for (const line of lines) {
3723
+ if (line.columns.every((c, i) => i === 0 || c >= line.columns[i - 1])) continue;
3724
+ const order = line.columns.map((_, i) => i).sort((a, b) => line.columns[a] - line.columns[b]);
3725
+ line.columns = order.map((i) => line.columns[i]);
3726
+ line.sources = order.map((i) => line.sources[i]);
3727
+ line.originalLines = order.map((i) => line.originalLines[i]);
3728
+ }
3729
+ return lines;
3730
+ }
3731
+ function joinRoot(root, source) {
3732
+ if (!root || /^[a-z][\w+.-]*:/i.test(source) || source.startsWith("/")) return source;
3733
+ return root.endsWith("/") ? root + source : `${root}/${source}`;
3734
+ }
3735
+ function decodeLines(raw) {
3736
+ const sources = [];
3737
+ const decoded = [];
3738
+ const add = (map, lineOffset, columnOffset) => {
3739
+ if (map.sections) {
3740
+ for (const section of map.sections) {
3741
+ if (!add(section.map, section.offset.line, section.offset.column)) return false;
3742
+ }
3743
+ return true;
3744
+ }
3745
+ if (typeof map.mappings !== "string" || !Array.isArray(map.sources)) return false;
3746
+ const base = sources.length;
3747
+ for (const s of map.sources) sources.push(joinRoot(map.sourceRoot, s ?? ""));
3748
+ decodeMappings(map.mappings, base).forEach((segments, i) => {
3749
+ const at = lineOffset + i;
3750
+ const shifted = i === 0 && columnOffset ? segments.columns.map((c) => c + columnOffset) : segments.columns;
3751
+ const existing = decoded[at];
3752
+ if (!existing)
3753
+ decoded[at] = { columns: shifted, sources: segments.sources, originalLines: segments.originalLines };
3754
+ else {
3755
+ existing.columns.push(...shifted);
3756
+ existing.sources.push(...segments.sources);
3757
+ existing.originalLines.push(...segments.originalLines);
3758
+ }
3759
+ });
3760
+ return true;
3761
+ };
3762
+ return add(raw, 0, 0) ? { sources, lines: decoded } : null;
3763
+ }
3764
+ function compactSegments(lines) {
3765
+ let total = 0;
3766
+ for (const line of lines) total += line?.columns.length ?? 0;
3767
+ const segments = {
3768
+ lineStarts: new Int32Array(lines.length + 1),
3769
+ columns: new Int32Array(total),
3770
+ sources: new Int32Array(total),
3771
+ originalLines: new Int32Array(total)
3772
+ };
3773
+ let at = 0;
3774
+ for (let n = 0; n < lines.length; n++) {
3775
+ segments.lineStarts[n] = at;
3776
+ const line = lines[n];
3777
+ if (!line) continue;
3778
+ segments.columns.set(line.columns, at);
3779
+ segments.sources.set(line.sources, at);
3780
+ segments.originalLines.set(line.originalLines, at);
3781
+ at += line.columns.length;
3782
+ }
3783
+ segments.lineStarts[lines.length] = at;
3784
+ return segments;
3785
+ }
3786
+ function decodeSourceMap(raw) {
3787
+ if (raw.version !== void 0 && raw.version !== 3) return null;
3788
+ const decoded = decodeLines(raw);
3789
+ if (!decoded) return null;
3790
+ const { sources } = decoded;
3791
+ const segments = compactSegments(decoded.lines);
3792
+ const lineCount = segments.lineStarts.length - 1;
3793
+ const originalAt = (line, column) => {
3794
+ if (line < 0 || line >= lineCount) return null;
3795
+ let lo = segments.lineStarts[line];
3796
+ let hi = segments.lineStarts[line + 1] - 1;
3797
+ if (lo > hi || segments.columns[lo] > column) return null;
3798
+ while (lo < hi) {
3799
+ const mid = lo + hi + 1 >> 1;
3800
+ if (segments.columns[mid] <= column) lo = mid;
3801
+ else hi = mid - 1;
3802
+ }
3803
+ const source = segments.sources[lo];
3804
+ return source < 0 ? null : { source, line: segments.originalLines[lo] };
3805
+ };
3806
+ return {
3807
+ sources,
3808
+ sourceAt(line, column) {
3809
+ return originalAt(line, column)?.source ?? null;
3810
+ },
3811
+ originalAt,
3812
+ firstOriginalLineBetween(source, start, end) {
3813
+ let first = null;
3814
+ eachSegmentBetween(segments, start, end, (i) => {
3815
+ if (segments.sources[i] !== source || segments.originalLines[i] < 0) return false;
3816
+ first = segments.originalLines[i];
3817
+ return true;
3818
+ });
3819
+ return first;
3820
+ },
3821
+ originalLinesBetween(source, start, end, fromLine = 0) {
3822
+ let first = Number.POSITIVE_INFINITY;
3823
+ let last = -1;
3824
+ eachSegmentBetween(segments, start, end, (i) => {
3825
+ const line = segments.originalLines[i];
3826
+ if (segments.sources[i] === source && line >= fromLine) {
3827
+ if (line < first) first = line;
3828
+ if (line > last) last = line;
3829
+ }
3830
+ return false;
3831
+ });
3832
+ return last < 0 ? null : [first, last];
3833
+ }
3834
+ };
3835
+ }
3836
+ function eachSegmentBetween(segments, start, end, visit) {
3837
+ const lineCount = segments.lineStarts.length - 1;
3838
+ for (let line = Math.max(start.line, 0); line <= end.line && line < lineCount; line++) {
3839
+ let i = segments.lineStarts[line];
3840
+ const stop = segments.lineStarts[line + 1];
3841
+ if (line === start.line) {
3842
+ let hi = stop;
3843
+ while (i < hi) {
3844
+ const mid = i + hi >> 1;
3845
+ if (segments.columns[mid] < start.column) i = mid + 1;
3846
+ else hi = mid;
3847
+ }
3848
+ }
3849
+ for (; i < stop; i++) {
3850
+ if (line === end.line && segments.columns[i] >= end.column) return;
3851
+ if (visit(i)) return;
3852
+ }
3853
+ }
3854
+ }
3855
+ function lineStarts(text) {
3856
+ const starts = [0];
3857
+ for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
3858
+ return starts;
3859
+ }
3860
+ function offsetToPosition(starts, offset) {
3861
+ let lo = 0;
3862
+ let hi = starts.length - 1;
3863
+ while (lo < hi) {
3864
+ const mid = lo + hi + 1 >> 1;
3865
+ if (starts[mid] <= offset) lo = mid;
3866
+ else hi = mid - 1;
3867
+ }
3868
+ return { line: lo, column: offset - starts[lo] };
3869
+ }
3870
+ function sourceMappingUrl(script) {
3871
+ const tail = script.slice(-4096);
3872
+ const matches = [...tail.matchAll(/(?:\/\/|\/\*)[#@]\s*sourceMappingURL=([^\s*'"]+)/g)];
3873
+ return matches.length ? matches[matches.length - 1][1] : null;
3874
+ }
3875
+ function decodeDataUrl(url) {
3876
+ const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
3877
+ if (!m) return null;
3878
+ try {
3879
+ if (!m[2]) return decodeURIComponent(m[3]);
3880
+ const binary = globalThis.atob(m[3]);
3881
+ const bytes = new Uint8Array(binary.length);
3882
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3883
+ return new TextDecoder().decode(bytes);
3884
+ } catch {
3885
+ return null;
3886
+ }
3887
+ }
3888
+ function normalizeSourcePath(source) {
3889
+ let s = source.replace(/[?#].*$/, "");
3890
+ s = s.replace(/^webpack:\/\/[^/]*\//, "").replace(/^webpack:\/\//, "");
3891
+ s = s.replace(/^(?:vite|rollup|turbopack|ng):\/*/, "");
3892
+ s = s.replace(/^\[project\]\//, "");
3893
+ s = s.replace(/^file:\/\//, "");
3894
+ s = s.replace(/^\/@fs\//, "/");
3895
+ s = s.replace(/^\/@id\//, "");
3896
+ try {
3897
+ s = decodeURIComponent(s);
3898
+ } catch {
3899
+ }
3900
+ s = s.replace(/\\/g, "/");
3901
+ if (/^\/[A-Za-z]:\//.test(s)) s = s.slice(1);
3902
+ s = s.replace(/(^|\/)\.\//g, "$1");
3903
+ return s;
3904
+ }
3905
+
3906
+ // ../core/src/code-reach.ts
3907
+ var MAX_CODE_REACH_FILES = 2e3;
3908
+ function scriptText(source) {
3909
+ return { length: source.length, starts: Uint32Array.from(lineStarts(source)) };
3910
+ }
3911
+ var textOf = (entry, text) => text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
3912
+ function executedFunctionRanges(entry, length, all = false) {
3913
+ length ??= entry.source?.length ?? Number.POSITIVE_INFINITY;
3914
+ const out = [];
3915
+ entry.functions.forEach((fn, i) => {
3916
+ const whole = fn.ranges[0];
3917
+ if (!whole || !all && whole.count <= 0) return;
3918
+ const topLevel = whole.startOffset === 0 && (whole.endOffset >= length || i === 0 && fn.functionName === "");
3919
+ if (topLevel) return;
3920
+ out.push({ start: whole.startOffset, end: whole.endOffset });
3921
+ });
3922
+ return out;
3923
+ }
3924
+ function scriptRan(entry, text) {
3925
+ return executedFunctionRanges(entry, text?.length).length > 0;
3926
+ }
3927
+ function reachedFunctionLines(entry, map, text) {
3928
+ return functionLines(entry, map, text, false);
3929
+ }
3930
+ function knownFunctionLines(entry, map, text) {
3931
+ return functionLines(entry, map, text, true);
3932
+ }
3933
+ function functionLines(entry, map, text, all) {
3934
+ const out = /* @__PURE__ */ new Map();
3935
+ const read = textOf(entry, text);
3936
+ const ranges = executedFunctionRanges(entry, read?.length, all);
3937
+ if (!ranges.length || !read) return out;
3938
+ const add = (source, range) => {
3939
+ const list = out.get(source);
3940
+ if (list) list.push(range);
3941
+ else out.set(source, [range]);
3942
+ };
3943
+ const starts = read.starts;
3944
+ for (const range of ranges) {
3945
+ const start = offsetToPosition(starts, range.start);
3946
+ if (!map) {
3947
+ const last = offsetToPosition(starts, Math.max(range.start, range.end - 1));
3948
+ add(-1, [start.line + 1, last.line + 1]);
3949
+ continue;
3950
+ }
3951
+ const at = map.originalAt(start.line, start.column);
3952
+ if (at === null) continue;
3953
+ const end = offsetToPosition(starts, range.end);
3954
+ const first = map.firstOriginalLineBetween(at.source, start, end);
3955
+ if (first === null) continue;
3956
+ const header = first === at.line || first === at.line + 1 ? at.line : first;
3957
+ const lines = map.originalLinesBetween(at.source, start, end, header);
3958
+ if (lines) add(at.source, [header + 1, lines[1] + 1]);
3959
+ }
3960
+ return out;
3961
+ }
3962
+ function finalizeCodeReach(files) {
3963
+ const out = /* @__PURE__ */ new Set();
3964
+ for (const raw of files) {
3965
+ const file = normalizeCoveragePath(raw);
3966
+ if (file !== null) out.add(file);
3967
+ }
3968
+ return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
3969
+ }
3970
+
3971
+ // ../core/src/mask.ts
3972
+ var DATA_URI_RE = /\bdata:[a-z0-9.+-]+\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+/gi;
3973
+ var JWT_RE = /\beyJ[\w-]{10,}\.[\w-]{5,}\.[\w-]{5,}\b/g;
3974
+ var LONG_HEX_RE2 = /\b[0-9a-f]{32,}\b/gi;
3975
+ function maskTokenLike(text) {
3976
+ return text.replace(DATA_URI_RE, "data:[masked]").replace(JWT_RE, "[masked-token]").replace(LONG_HEX_RE2, "[masked-hex]");
3977
+ }
3978
+ var CREDENTIAL_PARAM_RE = /token|code|secret|password|passwd|pwd|key|session|auth|sig|credential|assertion|nonce|otp|jwt/i;
3979
+ function maskUrlFragment(hash) {
3980
+ const body = hash.startsWith("#") ? hash.slice(1) : hash;
3981
+ const query = body.indexOf("?");
3982
+ const route = query >= 0 ? body.slice(0, query) : body.includes("=") ? "" : body;
3983
+ const params = query >= 0 ? body.slice(query + 1) : body.includes("=") ? body : null;
3984
+ const masked = params === null ? null : params.split("&").map((param) => {
3985
+ const eq = param.indexOf("=");
3986
+ if (eq < 0) return maskTokenLike(param);
3987
+ const name = param.slice(0, eq);
3988
+ const value = param.slice(eq + 1);
3989
+ return `${name}=${value && CREDENTIAL_PARAM_RE.test(name) ? "[masked]" : maskTokenLike(value)}`;
3990
+ }).join("&");
3991
+ const out = maskTokenLike(route) + (masked === null ? "" : query >= 0 ? `?${masked}` : masked);
3992
+ return hash.startsWith("#") ? `#${out}` : out;
3502
3993
  }
3503
3994
 
3504
3995
  // src/internal/capture/inspect-on-failure.ts
@@ -3537,8 +4028,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
3537
4028
  }
3538
4029
 
3539
4030
  // src/internal/capture/pick-on-failure.ts
3540
- var path5 = __toESM(require("path"));
3541
- var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
4031
+ var path4 = __toESM(require("path"));
3542
4032
  function parseLeafLocatorExpression(rawExpr) {
3543
4033
  const leaf = parseLeafLocatorCall(rawExpr);
3544
4034
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -3547,14 +4037,13 @@ function deriveFailedLocator(testInfo) {
3547
4037
  const info = testInfo;
3548
4038
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
3549
4039
  for (const err of errors) {
3550
- const text = `${err.message ?? ""}
3551
- ${err.stack ?? ""}`.replace(ANSI_RE2, "");
3552
- const line = /^\s*Locator:\s*(.+)$/m.exec(text);
3553
- if (!line) continue;
3554
- const parsed2 = parseLeafLocatorExpression(line[1].trim());
4040
+ const header = readLocatorHeader(stripAnsi(`${err.message ?? ""}
4041
+ ${err.stack ?? ""}`));
4042
+ if (!header) continue;
4043
+ const parsed2 = parseLeafLocatorExpression(header);
3555
4044
  if (!parsed2) continue;
3556
4045
  const loc = err.location;
3557
- const location = loc ? `${path5.relative(process.cwd(), loc.file).split(path5.sep).join("/")}:${loc.line}:${loc.column}` : null;
4046
+ const location = loc ? `${path4.relative(process.cwd(), loc.file).split(path4.sep).join("/")}:${loc.line}:${loc.column}` : null;
3558
4047
  return { method: parsed2.method, args: parsed2.args, location };
3559
4048
  }
3560
4049
  return null;
@@ -3707,7 +4196,7 @@ function applyPickToSnapshots(snapshots, pick) {
3707
4196
  }
3708
4197
 
3709
4198
  // src/internal/capture/pause-at.ts
3710
- var path6 = __toESM(require("path"));
4199
+ var path5 = __toESM(require("path"));
3711
4200
 
3712
4201
  // ../core/src/editor-send.ts
3713
4202
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -3751,9 +4240,9 @@ var PauseSet = class {
3751
4240
  var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
3752
4241
  function callSiteFile(file, cwd) {
3753
4242
  const slashed = file.replace(/\\/g, "/");
3754
- const absolute = path6.isAbsolute(file) || path6.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
3755
- let rel = absolute ? path6.relative(cwd, file) : path6.normalize(slashed);
3756
- rel = rel.split(path6.sep).join("/").replace(/\\/g, "/");
4243
+ const absolute = path5.isAbsolute(file) || path5.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
4244
+ let rel = absolute ? path5.relative(cwd, file) : path5.normalize(slashed);
4245
+ rel = rel.split(path5.sep).join("/").replace(/\\/g, "/");
3757
4246
  if (rel.startsWith("./")) rel = rel.slice(2);
3758
4247
  return rel;
3759
4248
  }
@@ -3909,385 +4398,194 @@ function isServerGranularity(value) {
3909
4398
  return typeof value === "string" && SERVER_VALUES.has(value);
3910
4399
  }
3911
4400
  function browserCoverageInWorker() {
3912
- return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || process.env[PIWI_ENV_KEYS.coverageBrowser] === "files";
3913
- }
3914
- function serverCoverageInWorker() {
3915
- return isServerGranularity(process.env[PIWI_ENV_KEYS.coverageServer]);
3916
- }
3917
-
3918
- // src/internal/support/aria-sampling.ts
3919
- var path7 = __toESM(require("path"));
3920
- var os3 = __toESM(require("os"));
3921
- var fs4 = __toESM(require("fs"));
3922
-
3923
- // src/internal/support/instance-id.ts
3924
- var crypto = __toESM(require("crypto"));
3925
- var os2 = __toESM(require("os"));
3926
- function hashForProject(projectName) {
3927
- return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
3928
- }
3929
-
3930
- // src/internal/support/aria-sampling.ts
3931
- function ariaSampleIdentity(filePath, title) {
3932
- return `${filePath}\0${title}`;
3933
- }
3934
- function getAriaSampleFilePath(projectName) {
3935
- return path7.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
3936
- }
3937
- var cachedSets = /* @__PURE__ */ new Map();
3938
- function loadAriaSampleSet(projectName) {
3939
- if (cachedSets.has(projectName)) return cachedSets.get(projectName);
3940
- let set = null;
3941
- try {
3942
- const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
3943
- const parsed2 = JSON.parse(raw);
3944
- if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
3945
- set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
3946
- }
3947
- } catch {
3948
- set = null;
3949
- }
3950
- cachedSets.set(projectName, set);
3951
- return set;
3952
- }
3953
- function relativeTestFile2(file) {
3954
- return path7.relative(process.cwd(), file).split(path7.sep).join("/");
3955
- }
3956
- function isDueForAriaSample(testInfo) {
3957
- const projectName = process.env.PIWI_PROJECT_NAME;
3958
- if (!projectName) return false;
3959
- const set = loadAriaSampleSet(projectName);
3960
- if (!set || set.size === 0) return false;
3961
- return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
3962
- }
3963
-
3964
- // src/internal/capture/quiet-capture.ts
3965
- var import_node_module = require("module");
3966
- var path9 = __toESM(require("path"));
3967
-
3968
- // src/internal/support/reporter-version.ts
3969
- var fs5 = __toESM(require("fs"));
3970
- var path8 = __toESM(require("path"));
3971
- var CANDIDATE_OFFSETS = ["..", "../../.."];
3972
- function findOwnPackageJson(fromDir) {
3973
- for (const offset of CANDIDATE_OFFSETS) {
3974
- try {
3975
- const pkgPath = path8.resolve(fromDir, offset, "package.json");
3976
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
3977
- const { name, version } = pkg;
3978
- if (name === "@piwitests/reporter" && typeof version === "string")
3979
- return { version, root: path8.dirname(pkgPath) };
3980
- } catch {
3981
- }
3982
- }
3983
- return void 0;
3984
- }
3985
-
3986
- // src/internal/capture/quiet-capture.ts
3987
- var BOXED_PREFIX_EXPORTS = [
3988
- [
3989
- "playwright-core/lib/coreBundle",
3990
- (mod) => mod?.utils?.setBoxedStackPrefixes
3991
- ],
3992
- ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
3993
- ];
3994
- var boxing;
3995
- var resolvedPlaywrightPackageJson;
3996
- function resolvePlaywrightPackageJson() {
3997
- if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
3998
- try {
3999
- const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
4000
- resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
4001
- } catch {
4002
- resolvedPlaywrightPackageJson = null;
4003
- }
4004
- return resolvedPlaywrightPackageJson;
4005
- }
4006
- function resolveBoxing() {
4007
- try {
4008
- const root = findOwnPackageJson(__dirname)?.root;
4009
- if (!root) return null;
4010
- const playwrightPackageJson = resolvePlaywrightPackageJson();
4011
- if (!playwrightPackageJson) return null;
4012
- const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
4013
- for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
4014
- let set;
4015
- try {
4016
- set = pick(requireFromPlaywright(moduleId));
4017
- } catch {
4018
- continue;
4019
- }
4020
- if (typeof set !== "function") continue;
4021
- return {
4022
- set,
4023
- prefixes: [path9.dirname(playwrightPackageJson), path9.join(root, "dist") + path9.sep]
4024
- };
4025
- }
4026
- } catch {
4027
- }
4028
- return null;
4029
- }
4030
- function boxCaptureFrames(extraPrefixes = []) {
4031
- const first = boxing === void 0;
4032
- if (first) boxing = resolveBoxing();
4033
- if (!boxing) return;
4034
- const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
4035
- if (!first && added.length === 0) return;
4036
- boxing.prefixes.push(...added);
4037
- try {
4038
- boxing.set([...boxing.prefixes]);
4039
- } catch {
4040
- }
4041
- }
4042
- function callMethod(target, prop, args) {
4043
- return target[prop](...args);
4044
- }
4045
- function internalCall(owner, run) {
4046
- const wrap = owner?._wrapApiCall;
4047
- if (typeof wrap !== "function") return run();
4048
- return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
4049
- }
4050
-
4051
- // src/internal/capture/code-reach.ts
4052
- var crypto2 = __toESM(require("crypto"));
4053
- var fs6 = __toESM(require("fs"));
4054
- var path10 = __toESM(require("path"));
4055
- var import_node_child_process = require("child_process");
4056
-
4057
- // ../core/src/coverage.ts
4058
- var MAX_PATH_LENGTH = 500;
4059
- function normalizeCoveragePath(raw) {
4060
- if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
4061
- const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
4062
- if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
4063
- if (/(^|\/)node_modules\//.test(path14)) return null;
4064
- return path14;
4401
+ const browser = process.env[PIWI_ENV_KEYS.coverageBrowser];
4402
+ return process.env[PIWI_ENV_KEYS.captureCodeReach] === "true" || browser === "files" || browser === "functions";
4065
4403
  }
4066
-
4067
- // ../core/src/source-map.ts
4068
- var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
4069
- var BASE64_VALUE = new Int8Array(128).fill(-1);
4070
- for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;
4071
- function decodeMappings(mappings, sourceBase) {
4072
- const lines = [];
4073
- let current = { columns: [], sources: [] };
4074
- let column = 0;
4075
- let source = 0;
4076
- const fields = [];
4077
- let value = 0;
4078
- let shift = 0;
4079
- const flush = () => {
4080
- if (fields.length === 0) return;
4081
- column += fields[0];
4082
- if (fields.length >= 4) {
4083
- source += fields[1];
4084
- current.columns.push(column);
4085
- current.sources.push(sourceBase + source);
4086
- } else {
4087
- current.columns.push(column);
4088
- current.sources.push(-1);
4089
- }
4090
- fields.length = 0;
4091
- };
4092
- for (let i = 0; i <= mappings.length; i++) {
4093
- const ch = i < mappings.length ? mappings.charCodeAt(i) : 59;
4094
- if (ch === 59 || ch === 44) {
4095
- flush();
4096
- if (ch === 59) {
4097
- lines.push(current);
4098
- current = { columns: [], sources: [] };
4099
- column = 0;
4100
- }
4101
- continue;
4102
- }
4103
- const digit = ch < 128 ? BASE64_VALUE[ch] : -1;
4104
- if (digit < 0) continue;
4105
- value += (digit & 31) << shift;
4106
- if (digit & 32) {
4107
- shift += 5;
4108
- continue;
4109
- }
4110
- fields.push(value & 1 ? -(value >>> 1) : value >>> 1);
4111
- value = 0;
4112
- shift = 0;
4113
- }
4114
- for (const line of lines) {
4115
- if (line.columns.every((c, i) => i === 0 || c >= line.columns[i - 1])) continue;
4116
- const order = line.columns.map((_, i) => i).sort((a, b) => line.columns[a] - line.columns[b]);
4117
- line.columns = order.map((i) => line.columns[i]);
4118
- line.sources = order.map((i) => line.sources[i]);
4119
- }
4120
- return lines;
4404
+ function browserFunctionsInWorker() {
4405
+ return process.env[PIWI_ENV_KEYS.coverageBrowser] === "functions";
4121
4406
  }
4122
- function joinRoot(root, source) {
4123
- if (!root || /^[a-z][\w+.-]*:/i.test(source) || source.startsWith("/")) return source;
4124
- return root.endsWith("/") ? root + source : `${root}/${source}`;
4407
+ function browserFunctionCoverage(files) {
4408
+ return sanitizeCoverage({
4409
+ v: 1,
4410
+ files: [...files].map(([path13, functions]) => ({ path: path13, runtime: "browser", functions }))
4411
+ });
4125
4412
  }
4126
- function decodeSourceMap(raw) {
4127
- const sources = [];
4128
- const lines = [];
4129
- const add = (map, lineOffset, columnOffset) => {
4130
- if (map.sections) {
4131
- for (const section of map.sections) {
4132
- if (!add(section.map, section.offset.line, section.offset.column)) return false;
4133
- }
4134
- return true;
4135
- }
4136
- if (typeof map.mappings !== "string" || !Array.isArray(map.sources)) return false;
4137
- const base = sources.length;
4138
- for (const s of map.sources) sources.push(joinRoot(map.sourceRoot, s ?? ""));
4139
- decodeMappings(map.mappings, base).forEach((segments, i) => {
4140
- const at = lineOffset + i;
4141
- const shifted = i === 0 && columnOffset ? segments.columns.map((c) => c + columnOffset) : segments.columns;
4142
- const existing = lines[at];
4143
- if (!existing) lines[at] = { columns: shifted, sources: segments.sources };
4144
- else {
4145
- existing.columns.push(...shifted);
4146
- existing.sources.push(...segments.sources);
4147
- }
4148
- });
4149
- return true;
4150
- };
4151
- if (raw.version !== void 0 && raw.version !== 3) return null;
4152
- if (!add(raw, 0, 0)) return null;
4153
- return {
4154
- sources,
4155
- sourceAt(line, column) {
4156
- const segments = lines[line];
4157
- if (!segments || segments.columns.length === 0) return null;
4158
- let lo = 0;
4159
- let hi = segments.columns.length - 1;
4160
- if (segments.columns[0] > column) return null;
4161
- while (lo < hi) {
4162
- const mid = lo + hi + 1 >> 1;
4163
- if (segments.columns[mid] <= column) lo = mid;
4164
- else hi = mid - 1;
4165
- }
4166
- const source = segments.sources[lo];
4167
- return source < 0 ? null : source;
4168
- },
4169
- mapsBetween(source, start, end) {
4170
- for (let line = start.line; line <= end.line && line < lines.length; line++) {
4171
- const segments = lines[line];
4172
- if (!segments) continue;
4173
- let i = 0;
4174
- if (line === start.line) {
4175
- let hi = segments.columns.length;
4176
- while (i < hi) {
4177
- const mid = i + hi >> 1;
4178
- if (segments.columns[mid] < start.column) i = mid + 1;
4179
- else hi = mid;
4180
- }
4181
- }
4182
- for (; i < segments.columns.length; i++) {
4183
- if (line === end.line && segments.columns[i] >= end.column) return false;
4184
- if (segments.sources[i] === source) return true;
4185
- }
4186
- }
4187
- return false;
4188
- }
4413
+ function newKnownFunctions(known, sent) {
4414
+ const parts = [];
4415
+ let files = [];
4416
+ let budget = MAX_COVERAGE_RANGES;
4417
+ const close = () => {
4418
+ if (files.length > 0) parts.push({ v: 1, files: files.sort((a, b) => a.path < b.path ? -1 : 1) });
4419
+ files = [];
4420
+ budget = MAX_COVERAGE_RANGES;
4189
4421
  };
4422
+ for (const [raw, ranges] of known) {
4423
+ const path13 = normalizeCoveragePath(raw);
4424
+ if (path13 === null) continue;
4425
+ let named = sent.get(path13);
4426
+ let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4427
+ if (fresh.length === 0) continue;
4428
+ if (!named) sent.set(path13, named = /* @__PURE__ */ new Set());
4429
+ for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4430
+ while (fresh.length > 0) {
4431
+ if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
4432
+ const part = fresh.slice(0, budget);
4433
+ fresh = fresh.slice(part.length);
4434
+ budget -= part.length;
4435
+ files.push({ path: path13, allFunctions: part });
4436
+ }
4437
+ }
4438
+ close();
4439
+ return parts;
4190
4440
  }
4191
- function lineStarts(text) {
4192
- const starts = [0];
4193
- for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) starts.push(i + 1);
4194
- return starts;
4441
+ function serverCoverageInWorker() {
4442
+ return isServerGranularity(process.env[PIWI_ENV_KEYS.coverageServer]);
4195
4443
  }
4196
- function offsetToPosition(starts, offset) {
4197
- let lo = 0;
4198
- let hi = starts.length - 1;
4199
- while (lo < hi) {
4200
- const mid = lo + hi + 1 >> 1;
4201
- if (starts[mid] <= offset) lo = mid;
4202
- else hi = mid - 1;
4203
- }
4204
- return { line: lo, column: offset - starts[lo] };
4444
+
4445
+ // src/internal/support/aria-sampling.ts
4446
+ var path6 = __toESM(require("path"));
4447
+ var os3 = __toESM(require("os"));
4448
+ var fs4 = __toESM(require("fs"));
4449
+
4450
+ // src/internal/support/instance-id.ts
4451
+ var crypto = __toESM(require("crypto"));
4452
+ var os2 = __toESM(require("os"));
4453
+ function hashForProject(projectName) {
4454
+ return crypto.createHash("sha1").update(projectName).digest("hex").slice(0, 16);
4205
4455
  }
4206
- function sourceMappingUrl(script) {
4207
- const tail = script.slice(-4096);
4208
- const matches = [...tail.matchAll(/(?:\/\/|\/\*)[#@]\s*sourceMappingURL=([^\s*'"]+)/g)];
4209
- return matches.length ? matches[matches.length - 1][1] : null;
4456
+
4457
+ // src/internal/support/aria-sampling.ts
4458
+ function ariaSampleIdentity(filePath, title) {
4459
+ return `${filePath}\0${title}`;
4210
4460
  }
4211
- function decodeDataUrl(url) {
4212
- const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(url);
4213
- if (!m) return null;
4214
- try {
4215
- if (!m[2]) return decodeURIComponent(m[3]);
4216
- const binary = globalThis.atob(m[3]);
4217
- const bytes = new Uint8Array(binary.length);
4218
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
4219
- return new TextDecoder().decode(bytes);
4220
- } catch {
4221
- return null;
4222
- }
4461
+ function getAriaSampleFilePath(projectName) {
4462
+ return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4223
4463
  }
4224
- function normalizeSourcePath(source) {
4225
- let s = source.replace(/[?#].*$/, "");
4226
- s = s.replace(/^webpack:\/\/[^/]*\//, "").replace(/^webpack:\/\//, "");
4227
- s = s.replace(/^(?:vite|rollup|turbopack|ng):\/*/, "");
4228
- s = s.replace(/^\[project\]\//, "");
4229
- s = s.replace(/^file:\/\//, "");
4230
- s = s.replace(/^\/@fs\//, "/");
4231
- s = s.replace(/^\/@id\//, "");
4464
+ var cachedSets = /* @__PURE__ */ new Map();
4465
+ function loadAriaSampleSet(projectName) {
4466
+ if (cachedSets.has(projectName)) return cachedSets.get(projectName);
4467
+ let set = null;
4232
4468
  try {
4233
- s = decodeURIComponent(s);
4469
+ const raw = fs4.readFileSync(getAriaSampleFilePath(projectName), "utf8");
4470
+ const parsed2 = JSON.parse(raw);
4471
+ if (parsed2.projectName === projectName && Array.isArray(parsed2.identities)) {
4472
+ set = new Set(parsed2.identities.filter((x) => typeof x === "string"));
4473
+ }
4234
4474
  } catch {
4475
+ set = null;
4235
4476
  }
4236
- s = s.replace(/\\/g, "/");
4237
- if (/^\/[A-Za-z]:\//.test(s)) s = s.slice(1);
4238
- s = s.replace(/(^|\/)\.\//g, "$1");
4239
- return s;
4477
+ cachedSets.set(projectName, set);
4478
+ return set;
4240
4479
  }
4241
-
4242
- // ../core/src/code-reach.ts
4243
- var MAX_CODE_REACH_FILES = 2e3;
4244
- function executedFunctionRanges(entry) {
4245
- const length = entry.source?.length ?? Number.POSITIVE_INFINITY;
4246
- const out = [];
4247
- entry.functions.forEach((fn, i) => {
4248
- const whole = fn.ranges[0];
4249
- if (!whole || whole.count <= 0) return;
4250
- const topLevel = whole.startOffset === 0 && (whole.endOffset >= length || i === 0 && fn.functionName === "");
4251
- if (topLevel) return;
4252
- out.push({ start: whole.startOffset, end: whole.endOffset });
4253
- });
4254
- return out;
4480
+ function isDueForAriaSample(testInfo) {
4481
+ const projectName = process.env.PIWI_PROJECT_NAME;
4482
+ if (!projectName) return false;
4483
+ const set = loadAriaSampleSet(projectName);
4484
+ if (!set || set.size === 0) return false;
4485
+ return set.has(
4486
+ ariaSampleIdentity(relativeTestPath(testInfo.file, configDirFromConfig(testInfo.config)), testInfo.title)
4487
+ );
4255
4488
  }
4256
- function executedFunctionOffsets(entry) {
4257
- return executedFunctionRanges(entry).map((range) => range.start);
4489
+
4490
+ // src/internal/capture/quiet-capture.ts
4491
+ var import_node_module = require("module");
4492
+ var path8 = __toESM(require("path"));
4493
+
4494
+ // src/internal/support/reporter-version.ts
4495
+ var fs5 = __toESM(require("fs"));
4496
+ var path7 = __toESM(require("path"));
4497
+ var CANDIDATE_OFFSETS = ["..", "../../.."];
4498
+ function findOwnPackageJson(fromDir) {
4499
+ for (const offset of CANDIDATE_OFFSETS) {
4500
+ try {
4501
+ const pkgPath = path7.resolve(fromDir, offset, "package.json");
4502
+ const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4503
+ const { name, version } = pkg;
4504
+ if (name === "@piwitests/reporter" && typeof version === "string")
4505
+ return { version, root: path7.dirname(pkgPath) };
4506
+ } catch {
4507
+ }
4508
+ }
4509
+ return void 0;
4258
4510
  }
4259
- function scriptRan(entry) {
4260
- return executedFunctionOffsets(entry).length > 0;
4511
+
4512
+ // src/internal/capture/quiet-capture.ts
4513
+ var BOXED_PREFIX_EXPORTS = [
4514
+ [
4515
+ "playwright-core/lib/coreBundle",
4516
+ (mod) => mod?.utils?.setBoxedStackPrefixes
4517
+ ],
4518
+ ["playwright-core/lib/utils", (mod) => mod?.setBoxedStackPrefixes]
4519
+ ];
4520
+ var boxing;
4521
+ var resolvedPlaywrightPackageJson;
4522
+ function resolvePlaywrightPackageJson() {
4523
+ if (resolvedPlaywrightPackageJson !== void 0) return resolvedPlaywrightPackageJson;
4524
+ try {
4525
+ const testPackageJson = (0, import_node_module.createRequire)(__filename).resolve("@playwright/test/package.json");
4526
+ resolvedPlaywrightPackageJson = (0, import_node_module.createRequire)(testPackageJson).resolve("playwright/package.json");
4527
+ } catch {
4528
+ resolvedPlaywrightPackageJson = null;
4529
+ }
4530
+ return resolvedPlaywrightPackageJson;
4261
4531
  }
4262
- function reachedSources(entry, map) {
4263
- const ranges = executedFunctionRanges(entry);
4264
- const reached = /* @__PURE__ */ new Set();
4265
- if (!ranges.length || entry.source === void 0) return reached;
4266
- const starts = lineStarts(entry.source);
4267
- for (const range of ranges) {
4268
- const start = offsetToPosition(starts, range.start);
4269
- const source = map.sourceAt(start.line, start.column);
4270
- if (source === null || reached.has(source)) continue;
4271
- if (map.mapsBetween(source, start, offsetToPosition(starts, range.end))) reached.add(source);
4532
+ function resolveBoxing() {
4533
+ try {
4534
+ const root = findOwnPackageJson(__dirname)?.root;
4535
+ if (!root) return null;
4536
+ const playwrightPackageJson = resolvePlaywrightPackageJson();
4537
+ if (!playwrightPackageJson) return null;
4538
+ const requireFromPlaywright = (0, import_node_module.createRequire)(playwrightPackageJson);
4539
+ for (const [moduleId, pick] of BOXED_PREFIX_EXPORTS) {
4540
+ let set;
4541
+ try {
4542
+ set = pick(requireFromPlaywright(moduleId));
4543
+ } catch {
4544
+ continue;
4545
+ }
4546
+ if (typeof set !== "function") continue;
4547
+ return {
4548
+ set,
4549
+ prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
4550
+ };
4551
+ }
4552
+ } catch {
4272
4553
  }
4273
- return reached;
4554
+ return null;
4274
4555
  }
4275
- function finalizeCodeReach(files) {
4276
- const out = /* @__PURE__ */ new Set();
4277
- for (const raw of files) {
4278
- const file = normalizeCoveragePath(raw);
4279
- if (file !== null) out.add(file);
4556
+ function boxCaptureFrames(extraPrefixes = []) {
4557
+ const first = boxing === void 0;
4558
+ if (first) boxing = resolveBoxing();
4559
+ if (!boxing) return;
4560
+ const added = extraPrefixes.filter((prefix) => !boxing.prefixes.includes(prefix));
4561
+ if (!first && added.length === 0) return;
4562
+ boxing.prefixes.push(...added);
4563
+ try {
4564
+ boxing.set([...boxing.prefixes]);
4565
+ } catch {
4280
4566
  }
4281
- return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
4567
+ }
4568
+ function callMethod(target, prop, args) {
4569
+ return target[prop](...args);
4570
+ }
4571
+ function internalCall(owner, run) {
4572
+ const wrap = owner?._wrapApiCall;
4573
+ if (typeof wrap !== "function") return run();
4574
+ return wrap.call(owner, run, { internal: true, title: "Piwi capture" });
4282
4575
  }
4283
4576
 
4284
4577
  // src/internal/capture/code-reach.ts
4578
+ var crypto2 = __toESM(require("crypto"));
4579
+ var fs6 = __toESM(require("fs"));
4580
+ var path9 = __toESM(require("path"));
4581
+ var import_node_child_process = require("child_process");
4285
4582
  var MAX_MAP_BYTES = 20 * 1024 * 1024;
4286
4583
  var MAP_TIMEOUT_MS = 5e3;
4287
4584
  var SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]sx?|vue|svelte|astro|marko|html?)$/i;
4288
4585
  var REPLAYED_SCRIPT = /^(?:about|rsc):/i;
4586
+ var NEVER_REACHED = /\/node_modules\/|\/@vite\//;
4289
4587
  var BUILD_DIRS = /(?:^|\/)(?:dist|build|out|\.output|\.next|\.nuxt|\.svelte-kit|public|static|wwwroot)\//;
4290
- var MAX_CACHED_SCRIPTS = 500;
4588
+ var MAX_CACHED_SCRIPTS = 4e3;
4291
4589
  var LatestByUrl = class {
4292
4590
  constructor() {
4293
4591
  this.entries = /* @__PURE__ */ new Map();
@@ -4322,13 +4620,37 @@ function repositoryRoot(from) {
4322
4620
  }).trim();
4323
4621
  } catch {
4324
4622
  }
4325
- cachedRepoRoot = { from, root: path10.resolve(root) };
4623
+ cachedRepoRoot = { from, root: path9.resolve(root) };
4326
4624
  return cachedRepoRoot.root;
4327
4625
  }
4328
4626
  function codeReachRoots(configDir, configured) {
4329
4627
  const repoRoot = repositoryRoot(configDir);
4330
- const roots = configured?.length ? configured.map((r) => path10.resolve(configDir, r)) : [configDir];
4331
- return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], repoRoot };
4628
+ const resolve3 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path9.resolve(configDir, r));
4629
+ const global = Array.isArray(configured) ? resolve3(configured) : resolve3(configured?.["*"]);
4630
+ const byOrigin = /* @__PURE__ */ new Map();
4631
+ if (configured && !Array.isArray(configured)) {
4632
+ for (const [key, dirs] of Object.entries(configured)) {
4633
+ const origin = key === "*" ? null : originOf2(key);
4634
+ const resolved = resolve3(dirs);
4635
+ if (origin && resolved.length) byOrigin.set(origin, resolved);
4636
+ }
4637
+ }
4638
+ const roots = global.length ? global : [configDir];
4639
+ return { roots: [.../* @__PURE__ */ new Set([...roots, repoRoot])], byOrigin, repoRoot };
4640
+ }
4641
+ function originOf2(url) {
4642
+ if (!url) return null;
4643
+ try {
4644
+ const origin = new URL(url).origin;
4645
+ return origin === "null" ? null : origin;
4646
+ } catch {
4647
+ return null;
4648
+ }
4649
+ }
4650
+ function scriptRoots(roots, scriptUrl, pageUrl) {
4651
+ if (!roots.byOrigin?.size) return roots.roots;
4652
+ const own = roots.byOrigin.get(originOf2(scriptUrl) ?? "") ?? roots.byOrigin.get(originOf2(pageUrl) ?? "");
4653
+ return own ? [.../* @__PURE__ */ new Set([...own, ...roots.roots])] : roots.roots;
4332
4654
  }
4333
4655
  function isFile(file) {
4334
4656
  try {
@@ -4340,10 +4662,10 @@ function isFile(file) {
4340
4662
  function resolveSourcePath(source, roots) {
4341
4663
  const normalized = normalizeSourcePath(source);
4342
4664
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
4343
- if (path10.isAbsolute(normalized) && isFile(normalized)) return path10.resolve(normalized);
4344
- const relative9 = normalized.replace(/^\/+/, "");
4665
+ if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
4666
+ const relative7 = normalized.replace(/^\/+/, "");
4345
4667
  for (const root of roots) {
4346
- const candidate = path10.resolve(root, relative9);
4668
+ const candidate = path9.resolve(root, relative7);
4347
4669
  if (isFile(candidate)) return candidate;
4348
4670
  }
4349
4671
  return null;
@@ -4375,7 +4697,7 @@ function fileForUrl(url, roots, bases) {
4375
4697
  return file;
4376
4698
  }
4377
4699
  function relativeToRepo(file, repoRoot) {
4378
- return path10.relative(repoRoot, file).split(path10.sep).join("/");
4700
+ return path9.relative(repoRoot, file).split(path9.sep).join("/");
4379
4701
  }
4380
4702
  function scriptVersion(entry) {
4381
4703
  return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -4412,50 +4734,96 @@ async function resolveScript(entry, roots, bases, fetchMap) {
4412
4734
  const version = scriptVersion(entry);
4413
4735
  const cached = scriptCache.get(entry.url, version);
4414
4736
  if (cached) return cached;
4415
- const file = fileForUrl(entry.url, roots.roots, bases);
4737
+ const file = fileForUrl(entry.url, roots, bases);
4416
4738
  const lookup = await loadMap(entry, fetchMap);
4417
4739
  let resolution;
4418
- if (file) resolution = { kind: "file", file, lookup, source: entry.source };
4419
- else resolution = lookup ? { kind: "map", lookup, files: [], source: entry.source } : { kind: "none" };
4420
- if (entry.source !== void 0) scriptCache.set(entry.url, version, resolution);
4740
+ const text = entry.source === void 0 ? void 0 : scriptText(entry.source);
4741
+ if (file) resolution = { kind: "file", file, lookup, text };
4742
+ else resolution = lookup ? { kind: "map", lookup, files: [], text } : { kind: "none" };
4743
+ if (text) scriptCache.set(entry.url, version, resolution);
4421
4744
  return resolution;
4422
4745
  }
4423
- function withSource(entry, resolution) {
4424
- if (entry.source !== void 0 || resolution.source === void 0) return entry;
4425
- return { ...entry, source: resolution.source };
4746
+ function textOf2(entry, resolution) {
4747
+ return resolution.text ?? (entry.source === void 0 ? void 0 : scriptText(entry.source));
4748
+ }
4749
+ async function resolveCodeReach(entries, roots, fetchMap, pageUrl) {
4750
+ return finalizeCodeReach((await resolveReach(entries, roots, fetchMap, pageUrl)).keys());
4751
+ }
4752
+ async function resolveCodeReachFunctions(entries, roots, fetchMap, pageUrl, options = {}) {
4753
+ return resolveFunctions(entries, roots, fetchMap, pageUrl, options.known === true);
4754
+ }
4755
+ async function resolveReach(entries, roots, fetchMap, pageUrl) {
4756
+ return (await resolveFunctions(entries, roots, fetchMap, pageUrl, false)).reached;
4426
4757
  }
4427
- async function resolveCodeReach(entries, roots, fetchMap) {
4428
- const files = /* @__PURE__ */ new Set();
4758
+ var knownCache = new LatestByUrl();
4759
+ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
4760
+ const files = /* @__PURE__ */ new Map();
4761
+ const known = /* @__PURE__ */ new Map();
4762
+ const addTo = (into, file, functions) => {
4763
+ const path13 = relativeToRepo(file, roots.repoRoot);
4764
+ const listed = into.get(path13);
4765
+ if (listed) listed.push(...functions);
4766
+ else into.set(path13, [...functions]);
4767
+ };
4768
+ const add = (file, functions) => addTo(files, file, functions);
4429
4769
  const bases = viteBases(entries);
4430
4770
  const scripts = entries.filter((entry) => entry.url && entry.functions?.length && !REPLAYED_SCRIPT.test(entry.url));
4431
- const known = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
4771
+ const cached = scripts.map((entry) => scriptCache.get(entry.url, scriptVersion(entry)));
4432
4772
  for (const [i, covered] of scripts.entries()) {
4433
- const resolution = known[i] ?? await resolveScript(covered, roots, bases, fetchMap);
4773
+ const dirs = scriptRoots(roots, covered.url, covered.pageUrl ?? pageUrl);
4774
+ const resolution = cached[i] ?? await resolveScript(covered, dirs, bases, fetchMap);
4775
+ const mapFile = (resolved, index) => {
4776
+ if (resolved.files[index] === void 0) {
4777
+ const source = resolved.lookup.sources[index] ?? "";
4778
+ let absolute = resolveSourcePath(source, dirs);
4779
+ if (!absolute && !/^[a-z][\w+.-]*:/i.test(source)) {
4780
+ try {
4781
+ absolute = fileForUrl(new URL(source, covered.url).href, dirs, bases);
4782
+ } catch {
4783
+ absolute = null;
4784
+ }
4785
+ }
4786
+ resolved.files[index] = absolute;
4787
+ }
4788
+ return resolved.files[index] ?? null;
4789
+ };
4434
4790
  if (resolution.kind === "file") {
4435
- const entry = withSource(covered, resolution);
4436
- const ran = resolution.lookup ? reachedSources(entry, resolution.lookup).size > 0 : scriptRan(entry);
4437
- if (resolution.file && ran) files.add(relativeToRepo(resolution.file, roots.repoRoot));
4791
+ if (!resolution.file) continue;
4792
+ const text = textOf2(covered, resolution);
4793
+ if (resolution.lookup) {
4794
+ const reached = reachedFunctionLines(covered, resolution.lookup, text);
4795
+ if (reached.size > 0) add(resolution.file, [...reached.values()].flat());
4796
+ } else if (scriptRan(covered, text)) {
4797
+ add(resolution.file, reachedFunctionLines(covered, null, text).get(-1) ?? []);
4798
+ }
4438
4799
  } else if (resolution.kind === "map") {
4439
- const entry = withSource(covered, resolution);
4440
- for (const index of reachedSources(entry, resolution.lookup)) {
4441
- if (resolution.files[index] === void 0) {
4442
- const source = resolution.lookup.sources[index] ?? "";
4443
- let absolute = resolveSourcePath(source, roots.roots);
4444
- if (!absolute && !/^[a-z][\w+.-]*:/i.test(source)) {
4445
- try {
4446
- absolute = fileForUrl(new URL(source, entry.url).href, roots.roots, bases);
4447
- } catch {
4448
- absolute = null;
4449
- }
4450
- }
4451
- resolution.files[index] = absolute;
4800
+ for (const [index, functions] of reachedFunctionLines(covered, resolution.lookup, textOf2(covered, resolution))) {
4801
+ const file = mapFile(resolution, index);
4802
+ if (file) add(file, functions);
4803
+ }
4804
+ }
4805
+ if (!withKnown || resolution.kind === "none") continue;
4806
+ const version = scriptVersion(covered);
4807
+ let listed = knownCache.get(covered.url, version);
4808
+ if (!listed || listed.count < covered.functions.length) {
4809
+ const byFile = /* @__PURE__ */ new Map();
4810
+ const text = textOf2(covered, resolution);
4811
+ if (resolution.kind === "file" && resolution.file) {
4812
+ const lines = knownFunctionLines(covered, resolution.lookup, text);
4813
+ const all = resolution.lookup ? [...lines.values()].flat() : lines.get(-1) ?? [];
4814
+ if (all.length > 0) byFile.set(resolution.file, all);
4815
+ } else if (resolution.kind === "map") {
4816
+ for (const [index, functions] of knownFunctionLines(covered, resolution.lookup, text)) {
4817
+ const file = mapFile(resolution, index);
4818
+ if (file) byFile.set(file, [...byFile.get(file) ?? [], ...functions]);
4452
4819
  }
4453
- const file = resolution.files[index];
4454
- if (file) files.add(relativeToRepo(file, roots.repoRoot));
4455
4820
  }
4821
+ listed = { count: covered.functions.length, files: byFile };
4822
+ knownCache.set(covered.url, version, listed);
4456
4823
  }
4824
+ for (const [file, functions] of listed.files) addTo(known, file, functions);
4457
4825
  }
4458
- return finalizeCodeReach(files);
4826
+ return { reached: files, known };
4459
4827
  }
4460
4828
  function pageMapFetcher(page) {
4461
4829
  return async (url) => {
@@ -4479,12 +4847,57 @@ function isChromiumPage(page) {
4479
4847
  }
4480
4848
  }
4481
4849
  var captures = /* @__PURE__ */ new WeakMap();
4850
+ var MARK_WORLD = "piwi-code-reach";
4851
+ var MARK_FUNCTION = "piwiCodeReachMark";
4852
+ var MARK_SOURCE = `addEventListener('beforeunload', function ${MARK_FUNCTION}() {});`;
4853
+ var markedDocuments = /* @__PURE__ */ new WeakMap();
4854
+ function marksOf(page) {
4855
+ let marks = markedDocuments.get(page);
4856
+ if (!marks) {
4857
+ marks = /* @__PURE__ */ new Set();
4858
+ markedDocuments.set(page, marks);
4859
+ }
4860
+ return marks;
4861
+ }
4862
+ async function markCurrentDocument(page, cdp) {
4863
+ try {
4864
+ const { frameTree } = await internalCall(page, () => cdp.send("Page.getFrameTree"));
4865
+ const marks = marksOf(page);
4866
+ if (marks.has(frameTree.frame.loaderId)) return;
4867
+ const world = await internalCall(
4868
+ page,
4869
+ () => cdp.send("Page.createIsolatedWorld", { frameId: frameTree.frame.id, worldName: MARK_WORLD })
4870
+ );
4871
+ await internalCall(
4872
+ page,
4873
+ () => cdp.send("Runtime.evaluate", { expression: MARK_SOURCE, contextId: world.executionContextId })
4874
+ );
4875
+ marks.add(frameTree.frame.loaderId);
4876
+ } catch {
4877
+ }
4878
+ }
4879
+ function onPaused(page, capture, event) {
4880
+ const leaving = event.reason === "EventListener" && event.data?.eventName === "listener:beforeunload" && (event.callFrames?.[0]?.functionName ?? MARK_FUNCTION) === MARK_FUNCTION;
4881
+ const read = leaving ? readCoverage(page, capture.cdp) : null;
4882
+ if (read) {
4883
+ const merging = read.then(
4884
+ (coverage) => mergeCoverageRead(capture.ran, capture.scripts, coverage, capture.keepIdle)
4885
+ );
4886
+ capture.reading.add(merging);
4887
+ void merging.finally(() => capture.reading.delete(merging));
4888
+ }
4889
+ void (read ?? Promise.resolve()).finally(() => {
4890
+ internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
4891
+ });
4892
+ });
4893
+ }
4482
4894
  function versionKey(url, hash) {
4483
4895
  return `${hash} ${url}`;
4484
4896
  }
4485
4897
  function onScriptParsed(page, capture, event) {
4486
4898
  if (!event.url || REPLAYED_SCRIPT.test(event.url)) return;
4487
- capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash });
4899
+ capture.scripts.set(event.scriptId, { url: event.url, hash: event.hash, pageUrl: capture.pageUrl });
4900
+ if (NEVER_REACHED.test(event.url)) return;
4488
4901
  const key = versionKey(event.url, event.hash);
4489
4902
  if (capture.sources.has(key) || scriptCache.get(event.url, event.hash)) return;
4490
4903
  const source = internalCall(
@@ -4499,6 +4912,44 @@ function onScriptParsed(page, capture, event) {
4499
4912
  );
4500
4913
  capture.sources.set(key, source);
4501
4914
  }
4915
+ var READ_WAIT_MS = 5e3;
4916
+ function readCoverage(page, cdp) {
4917
+ let timer;
4918
+ const late = new Promise((resolve3) => {
4919
+ timer = setTimeout(() => resolve3([]), READ_WAIT_MS);
4920
+ });
4921
+ const taken = internalCall(page, () => cdp.send("Profiler.takePreciseCoverage")).then(
4922
+ (answer) => answer.result,
4923
+ () => []
4924
+ );
4925
+ return Promise.race([taken, late]).finally(() => clearTimeout(timer));
4926
+ }
4927
+ function mergeCoverageRead(ran, scripts, read, keepIdle = true) {
4928
+ for (const covered of read) {
4929
+ const script = scripts.get(covered.scriptId);
4930
+ if (!script) continue;
4931
+ const key = versionKey(script.url, script.hash);
4932
+ let known = ran.get(key);
4933
+ if (!known) {
4934
+ known = { script, functions: [], byRange: /* @__PURE__ */ new Map() };
4935
+ ran.set(key, known);
4936
+ }
4937
+ for (const fn of covered.functions) {
4938
+ const whole = fn.ranges[0];
4939
+ if (!whole) continue;
4940
+ if (!keepIdle && whole.count <= 0) continue;
4941
+ const range = `${whole.startOffset}:${whole.endOffset}`;
4942
+ const seen = known.byRange.get(range);
4943
+ if (seen) {
4944
+ if (whole.count > 0 && seen.ranges[0].count <= 0) seen.ranges[0].count = whole.count;
4945
+ continue;
4946
+ }
4947
+ const copy = { ...fn, ranges: fn.ranges.map((r) => ({ ...r })) };
4948
+ known.byRange.set(range, copy);
4949
+ known.functions.push(copy);
4950
+ }
4951
+ }
4952
+ }
4502
4953
  async function detach(page, cdp) {
4503
4954
  try {
4504
4955
  await internalCall(page, () => cdp.detach());
@@ -4510,21 +4961,35 @@ async function startCodeReach(page) {
4510
4961
  let cdp = null;
4511
4962
  try {
4512
4963
  cdp = await internalCall(page, () => page.context().newCDPSession(page));
4513
- const capture = { cdp, scripts: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
4964
+ const capture = {
4965
+ cdp,
4966
+ scripts: /* @__PURE__ */ new Map(),
4967
+ sources: /* @__PURE__ */ new Map(),
4968
+ ran: /* @__PURE__ */ new Map(),
4969
+ keepIdle: browserFunctionsInWorker(),
4970
+ reading: /* @__PURE__ */ new Set(),
4971
+ pageUrl: page.url()
4972
+ };
4973
+ const marks = marksOf(page);
4514
4974
  cdp.on("Debugger.scriptParsed", (event) => onScriptParsed(page, capture, event));
4515
- cdp.on("Debugger.paused", () => {
4516
- internalCall(page, () => capture.cdp.send("Debugger.resume")).catch(() => {
4517
- });
4975
+ cdp.on("Debugger.paused", (event) => onPaused(page, capture, event));
4976
+ cdp.on("Page.frameNavigated", (event) => {
4977
+ if (event.frame.parentId) return;
4978
+ marks.add(event.frame.loaderId);
4979
+ capture.pageUrl = event.frame.url;
4518
4980
  });
4519
4981
  await internalCall(
4520
4982
  page,
4521
4983
  () => Promise.all([
4522
4984
  capture.cdp.send("Debugger.enable"),
4523
- capture.cdp.send("Debugger.setSkipAllPauses", { skip: true }),
4524
4985
  capture.cdp.send("Profiler.enable"),
4525
- capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false })
4986
+ capture.cdp.send("Profiler.startPreciseCoverage", { callCount: false, detailed: false }),
4987
+ capture.cdp.send("Page.enable"),
4988
+ capture.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: MARK_SOURCE, worldName: MARK_WORLD }),
4989
+ capture.cdp.send("DOMDebugger.setEventListenerBreakpoint", { eventName: "beforeunload" })
4526
4990
  ])
4527
4991
  );
4992
+ await markCurrentDocument(page, capture.cdp);
4528
4993
  captures.set(page, capture);
4529
4994
  return true;
4530
4995
  } catch {
@@ -4538,17 +5003,17 @@ async function stopCodeReach(page) {
4538
5003
  captures.delete(page);
4539
5004
  const { cdp } = capture;
4540
5005
  try {
4541
- const coverage = await internalCall(page, async () => {
5006
+ const last = await internalCall(page, async () => {
4542
5007
  const taken = await cdp.send("Profiler.takePreciseCoverage");
4543
5008
  await cdp.send("Profiler.stopPreciseCoverage");
4544
- return taken;
5009
+ return taken.result;
4545
5010
  });
5011
+ await Promise.all(capture.reading);
5012
+ mergeCoverageRead(capture.ran, capture.scripts, last, capture.keepIdle);
4546
5013
  const entries = [];
4547
- for (const script of coverage.result) {
4548
- const parsed2 = capture.scripts.get(script.scriptId);
4549
- if (!parsed2) continue;
4550
- const entry = { url: parsed2.url, hash: parsed2.hash, functions: script.functions };
4551
- const source = await capture.sources.get(versionKey(parsed2.url, parsed2.hash));
5014
+ for (const [key, { script, functions }] of capture.ran) {
5015
+ const entry = { url: script.url, hash: script.hash, functions, pageUrl: script.pageUrl };
5016
+ const source = await capture.sources.get(key);
4552
5017
  if (source != null) entry.source = source;
4553
5018
  entries.push(entry);
4554
5019
  }
@@ -4562,7 +5027,7 @@ async function stopCodeReach(page) {
4562
5027
 
4563
5028
  // src/internal/capture/resource-ledger.ts
4564
5029
  var fs8 = __toESM(require("fs"));
4565
- var path12 = __toESM(require("path"));
5030
+ var path11 = __toESM(require("path"));
4566
5031
  var import_node_module2 = require("module");
4567
5032
 
4568
5033
  // src/internal/capture/worker-metrics.ts
@@ -4570,7 +5035,7 @@ var import_node_perf_hooks = require("perf_hooks");
4570
5035
 
4571
5036
  // src/internal/support/system-readers.ts
4572
5037
  var fs7 = __toESM(require("fs"));
4573
- var path11 = __toESM(require("path"));
5038
+ var path10 = __toESM(require("path"));
4574
5039
  var CLK_TCK = 100;
4575
5040
  function readText(file) {
4576
5041
  try {
@@ -4656,7 +5121,7 @@ var ProcFs = class {
4656
5121
  }
4657
5122
  /** Whether this looks like a Linux `/proc` at all. */
4658
5123
  available() {
4659
- return fs7.existsSync(path11.join(this.root, "stat"));
5124
+ return fs7.existsSync(path10.join(this.root, "stat"));
4660
5125
  }
4661
5126
  /** Every process, keyed by pid. */
4662
5127
  table() {
@@ -4669,32 +5134,32 @@ var ProcFs = class {
4669
5134
  }
4670
5135
  for (const name of names) {
4671
5136
  if (!/^\d+$/.test(name)) continue;
4672
- const text = readText(path11.join(this.root, name, "stat"));
5137
+ const text = readText(path10.join(this.root, name, "stat"));
4673
5138
  const stat = text ? parseProcStat(Number(name), text) : null;
4674
5139
  if (stat) out.set(stat.pid, stat);
4675
5140
  }
4676
5141
  return out;
4677
5142
  }
4678
5143
  stat(pid) {
4679
- const text = readText(path11.join(this.root, String(pid), "stat"));
5144
+ const text = readText(path10.join(this.root, String(pid), "stat"));
4680
5145
  return text ? parseProcStat(pid, text) : null;
4681
5146
  }
4682
5147
  runWaitMs(pid) {
4683
- const text = readText(path11.join(this.root, String(pid), "schedstat"));
5148
+ const text = readText(path10.join(this.root, String(pid), "schedstat"));
4684
5149
  return text ? parseSchedstat(text) : null;
4685
5150
  }
4686
5151
  cmdline(pid) {
4687
- return (readText(path11.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
5152
+ return (readText(path10.join(this.root, String(pid), "cmdline")) ?? "").split("\0").join(" ").trim();
4688
5153
  }
4689
5154
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
4690
5155
  statusKb(pid, key) {
4691
- const text = readText(path11.join(this.root, String(pid), "status"));
5156
+ const text = readText(path10.join(this.root, String(pid), "status"));
4692
5157
  return text ? parseKbField(text, key) : null;
4693
5158
  }
4694
5159
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
4695
5160
  async pssKb(pid) {
4696
5161
  try {
4697
- return parseKbField(await fs7.promises.readFile(path11.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
5162
+ return parseKbField(await fs7.promises.readFile(path10.join(this.root, String(pid), "smaps_rollup"), "utf8"), "Pss");
4698
5163
  } catch {
4699
5164
  return null;
4700
5165
  }
@@ -4702,29 +5167,29 @@ var ProcFs = class {
4702
5167
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
4703
5168
  resetPeakRss(pid) {
4704
5169
  try {
4705
- fs7.writeFileSync(path11.join(this.root, String(pid), "clear_refs"), "5");
5170
+ fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
4706
5171
  return true;
4707
5172
  } catch {
4708
5173
  return false;
4709
5174
  }
4710
5175
  }
4711
5176
  cpuTicks() {
4712
- const text = readText(path11.join(this.root, "stat"));
5177
+ const text = readText(path10.join(this.root, "stat"));
4713
5178
  return text ? parseCpuTicks(text) : null;
4714
5179
  }
4715
5180
  /** Total stall time of a resource's pressure file, in µs. */
4716
5181
  pressureUs(resource) {
4717
- const text = readText(path11.join(this.root, "pressure", resource));
5182
+ const text = readText(path10.join(this.root, "pressure", resource));
4718
5183
  return text ? parsePsiTotal(text) : null;
4719
5184
  }
4720
5185
  memAvailableKb() {
4721
- const text = readText(path11.join(this.root, "meminfo"));
5186
+ const text = readText(path10.join(this.root, "meminfo"));
4722
5187
  return text ? parseKbField(text, "MemAvailable") : null;
4723
5188
  }
4724
5189
  /** Open file descriptors of a process. */
4725
5190
  fdCount(pid) {
4726
5191
  try {
4727
- return fs7.readdirSync(path11.join(this.root, String(pid), "fd")).length;
5192
+ return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
4728
5193
  } catch {
4729
5194
  return null;
4730
5195
  }
@@ -4886,10 +5351,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
4886
5351
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
4887
5352
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
4888
5353
  function relativePath(file) {
4889
- return path12.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path12.sep).join("/");
5354
+ return path11.relative(process.cwd(), file.replace(/^file:\/\//, "")).split(path11.sep).join("/");
4890
5355
  }
4891
5356
  function isLibraryFile(file) {
4892
- return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path12.sep}node_modules${path12.sep}`) || file.includes("/node_modules/") || file === __filename;
5357
+ return file.startsWith("node:") || file.startsWith("internal/") || file.includes(`${path11.sep}node_modules${path11.sep}`) || file.includes("/node_modules/") || file === __filename;
4893
5358
  }
4894
5359
  function userSite(stack) {
4895
5360
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -4937,7 +5402,7 @@ function playwrightTestInfoSource() {
4937
5402
  try {
4938
5403
  const packageJson = resolvePlaywrightPackageJson();
4939
5404
  if (!packageJson) return () => null;
4940
- const globals = (0, import_node_module2.createRequire)(packageJson)(path12.join(path12.dirname(packageJson), "lib", "globals.js"));
5405
+ const globals = (0, import_node_module2.createRequire)(packageJson)(path11.join(path11.dirname(packageJson), "lib", "globals.js"));
4941
5406
  const read = globals.currentTestInfo;
4942
5407
  if (typeof read !== "function") return () => null;
4943
5408
  return () => {
@@ -5721,52 +6186,87 @@ function createSink() {
5721
6186
  userPick: null,
5722
6187
  pause: null,
5723
6188
  locatorPages: new LocatorPageLog(),
5724
- codeReach: null,
6189
+ codeReach: { sessions: /* @__PURE__ */ new Map(), files: /* @__PURE__ */ new Map(), known: /* @__PURE__ */ new Map() },
5725
6190
  serverCoverage: /* @__PURE__ */ new Map(),
5726
6191
  baseURL: null
5727
6192
  };
5728
6193
  }
5729
- function maybeStartCodeReach(page) {
6194
+ var MAX_CODE_REACH_PAGES = 16;
6195
+ var CODE_REACH_START_WAIT_MS = 2e3;
6196
+ function maybeStartCodeReach(page, alreadyOpen = false) {
5730
6197
  const sink = currentSink;
5731
- if (!sink || !browserCoverageInWorker()) return Promise.resolve(false);
5732
- if (sink.codeReach) return sink.codeReach.page === page ? sink.codeReach.started : Promise.resolve(false);
6198
+ if (!sink || !browserCoverageInWorker() || isPageClosed(page)) return Promise.resolve(false);
6199
+ const known = sink.codeReach.sessions.get(page);
6200
+ if (known) return known.started;
6201
+ if (alreadyOpen) {
6202
+ let recording = 0;
6203
+ for (const [other, session] of sink.codeReach.sessions) if (!session.stopped && !isPageClosed(other)) recording++;
6204
+ if (recording >= MAX_CODE_REACH_PAGES) return Promise.resolve(false);
6205
+ }
5733
6206
  const started = startCodeReach(page);
5734
- sink.codeReach = { page, started, stopped: false, files: null };
6207
+ sink.codeReach.sessions.set(page, { started, stopped: false });
5735
6208
  return started;
5736
6209
  }
6210
+ async function startCodeReachOnOpenPages() {
6211
+ if (!browserCoverageInWorker() || OPEN_PAGES.size === 0) return;
6212
+ const started = Promise.all([...OPEN_PAGES].map((page) => maybeStartCodeReach(page, true)));
6213
+ await Promise.race([started, new Promise((resolve3) => setTimeout(resolve3, CODE_REACH_START_WAIT_MS).unref())]);
6214
+ }
5737
6215
  function configuredCodeReachRoots() {
5738
6216
  const raw = process.env.PIWI_CODE_REACH_ROOTS?.trim();
5739
6217
  if (!raw) return null;
6218
+ const dirs = (value) => (Array.isArray(value) ? value : [value]).filter((r) => typeof r === "string" && r !== "");
5740
6219
  try {
5741
6220
  const parsed2 = JSON.parse(raw);
5742
- if (Array.isArray(parsed2)) return parsed2.filter((r) => typeof r === "string" && r !== "");
6221
+ if (Array.isArray(parsed2)) return dirs(parsed2);
6222
+ if (parsed2 && typeof parsed2 === "object") {
6223
+ return Object.fromEntries(Object.entries(parsed2).map(([origin, value]) => [origin, dirs(value)]));
6224
+ }
5743
6225
  } catch {
5744
6226
  }
5745
- return raw.split(path13.delimiter).filter(Boolean);
6227
+ return raw.split(path12.delimiter).filter(Boolean);
5746
6228
  }
5747
6229
  async function stopSinkCodeReach(sink, closing) {
5748
- const session = sink.codeReach;
5749
- if (!session || session.stopped) return;
5750
- if (closing && closing.page !== session.page && (!closing.context || pageContext(session.page) !== closing.context)) {
5751
- return;
6230
+ const stopping = [];
6231
+ const underway = [];
6232
+ for (const [page, session] of sink.codeReach.sessions) {
6233
+ if (session.stopped) {
6234
+ if (!closing && session.stopping) underway.push(session.stopping);
6235
+ continue;
6236
+ }
6237
+ if (closing && closing.page !== page && (!closing.context || pageContext(page) !== closing.context)) continue;
6238
+ session.stopped = true;
6239
+ stopping.push([page, session]);
6240
+ }
6241
+ if (stopping.length > 0) {
6242
+ const info = sink.testInfo;
6243
+ const roots = codeReachRoots(configDirFromConfig(info?.config), configuredCodeReachRoots());
6244
+ for (const [page, session] of stopping) {
6245
+ session.stopping = stopPageCodeReach(sink, page, session, roots);
6246
+ underway.push(session.stopping);
6247
+ }
5752
6248
  }
5753
- session.stopped = true;
6249
+ await Promise.all(underway);
6250
+ }
6251
+ async function stopPageCodeReach(sink, page, session, roots) {
5754
6252
  try {
5755
- if (!await session.started || isPageClosed(session.page)) return;
5756
- const entries = await stopCodeReach(session.page);
6253
+ if (!await session.started || isPageClosed(page)) return;
6254
+ const entries = await stopCodeReach(page);
5757
6255
  if (!entries) return;
5758
- const info = sink.testInfo;
5759
- const configFile = info?.config.configFile;
5760
- const configDir = configFile ? path13.dirname(configFile) : info?.config.rootDir ?? process.cwd();
5761
- session.files = await resolveCodeReach(
5762
- entries,
5763
- codeReachRoots(configDir, configuredCodeReachRoots()),
5764
- pageMapFetcher(session.page)
5765
- );
6256
+ const { reached, known } = await resolveCodeReachFunctions(entries, roots, pageMapFetcher(page), page.url(), {
6257
+ known: browserFunctionsInWorker()
6258
+ });
6259
+ for (const [file, functions] of reached) {
6260
+ sink.codeReach.files.set(file, [...sink.codeReach.files.get(file) ?? [], ...functions]);
6261
+ }
6262
+ for (const [file, functions] of known) {
6263
+ sink.codeReach.known.set(file, [...sink.codeReach.known.get(file) ?? [], ...functions]);
6264
+ }
5766
6265
  } catch {
5767
6266
  }
5768
6267
  }
5769
6268
  var currentSink = null;
6269
+ var KNOWN_FUNCTIONS_SENT = /* @__PURE__ */ new Map();
5770
6270
  var PENDING_PROBES = /* @__PURE__ */ new Set();
5771
6271
  async function drainPendingProbes(capMs) {
5772
6272
  if (PENDING_PROBES.size === 0) return;
@@ -5882,8 +6382,8 @@ function buildPageState(raw, cookies, interaction) {
5882
6382
  length: typeof e.length === "number" ? e.length : 0
5883
6383
  }));
5884
6384
  return {
5885
- url: raw.url,
5886
- hash: raw.hash || null,
6385
+ url: raw.url.split("#")[0],
6386
+ hash: raw.hash ? maskUrlFragment(raw.hash) : null,
5887
6387
  historyState: historyState || null,
5888
6388
  localStorage: capStorage(raw.localStorage),
5889
6389
  sessionStorage: capStorage(raw.sessionStorage),
@@ -6063,6 +6563,7 @@ async function maybeOpenPicker(sink, closing) {
6063
6563
  applyPickToSnapshots(sink.capturedLocators, pick);
6064
6564
  }
6065
6565
  var INSTRUMENTED_PAGES = /* @__PURE__ */ new WeakSet();
6566
+ var OPEN_PAGES = /* @__PURE__ */ new Set();
6066
6567
  var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
6067
6568
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
6068
6569
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
@@ -6343,6 +6844,10 @@ var CANCELLED_REQUEST = /^net::ERR_ABORTED$|^NS_BINDING_ABORTED$|^NS_ERROR_ABORT
6343
6844
  function instrumentPage(page) {
6344
6845
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
6345
6846
  INSTRUMENTED_PAGES.add(page);
6847
+ if (typeof page.once === "function") {
6848
+ OPEN_PAGES.add(page);
6849
+ page.once("close", () => OPEN_PAGES.delete(page));
6850
+ }
6346
6851
  void maybeStartCodeReach(page);
6347
6852
  observeOwnApiRequests(page);
6348
6853
  const probeSink = currentSink;
@@ -6679,10 +7184,24 @@ async function flushSink(sink, testInfo) {
6679
7184
  });
6680
7185
  }
6681
7186
  await stopSinkCodeReach(sink);
6682
- if (sink.codeReach?.files?.length) {
7187
+ if (sink.codeReach.files.size > 0) {
6683
7188
  await testInfo.attach(ATTACHMENT_NAMES.codeReach, {
6684
7189
  contentType: "application/json",
6685
- body: Buffer.from(JSON.stringify(sink.codeReach.files))
7190
+ body: Buffer.from(JSON.stringify(finalizeCodeReach(sink.codeReach.files.keys())))
7191
+ });
7192
+ const functions = browserFunctionsInWorker() ? browserFunctionCoverage(sink.codeReach.files) : null;
7193
+ if (functions) {
7194
+ await testInfo.attach(ATTACHMENT_NAMES.browserCoverage, {
7195
+ contentType: "application/json",
7196
+ body: Buffer.from(JSON.stringify(functions))
7197
+ });
7198
+ }
7199
+ }
7200
+ const known = browserFunctionsInWorker() ? newKnownFunctions(sink.codeReach.known, KNOWN_FUNCTIONS_SENT) : [];
7201
+ for (const part of known) {
7202
+ await testInfo.attach(ATTACHMENT_NAMES.browserFunctions, {
7203
+ contentType: "application/json",
7204
+ body: Buffer.from(JSON.stringify(part))
6686
7205
  });
6687
7206
  }
6688
7207
  if (sink.serverCoverage.size > 0) {
@@ -6881,6 +7400,7 @@ var piwiFixtures = {
6881
7400
  const plan = loadFlakePlan();
6882
7401
  const role = flakeRoleForTest(plan, {
6883
7402
  file: testInfo.file,
7403
+ base: configDirFromConfig(testInfo.config),
6884
7404
  title: testInfo.title,
6885
7405
  titlePath: testInfo.titlePath,
6886
7406
  project: testInfo.project.name
@@ -6894,11 +7414,13 @@ var piwiFixtures = {
6894
7414
  sink.probeItem = probeItemForTest({
6895
7415
  title: testInfo.title,
6896
7416
  file: testInfo.file,
6897
- titlePath: testInfo.titlePath
7417
+ titlePath: testInfo.titlePath,
7418
+ base: configDirFromConfig(testInfo.config)
6898
7419
  });
6899
7420
  activeResourceLedger()?.testStarted(testInfo);
6900
7421
  currentSink = sink;
6901
7422
  try {
7423
+ await startCodeReachOnOpenPages();
6902
7424
  await use();
6903
7425
  } finally {
6904
7426
  currentSink = null;
@@ -6937,6 +7459,7 @@ async function recordFlakeAttempt(flake, testInfo) {
6937
7459
  flakeResultLine(flake.plan, {
6938
7460
  role: flake.role,
6939
7461
  file: testInfo.file,
7462
+ base: configDirFromConfig(testInfo.config),
6940
7463
  title: testInfo.title,
6941
7464
  project: testInfo.project.name || null,
6942
7465
  browserName: flake.browserName,