@piwitests/reporter 0.55.0 → 0.57.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);
@@ -2281,7 +2581,6 @@ var LocatorPageLog = class {
2281
2581
 
2282
2582
  // src/internal/probe/mode.ts
2283
2583
  var fs2 = __toESM(require("fs"));
2284
- var path3 = __toESM(require("path"));
2285
2584
 
2286
2585
  // src/internal/config/desktop.ts
2287
2586
  var fs = __toESM(require("fs"));
@@ -2418,6 +2717,17 @@ var ENV_FALLBACK_SPECS = [
2418
2717
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
2419
2718
  ];
2420
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
+
2421
2731
  // src/internal/probe/plan.ts
2422
2732
  function parseRouteKey(routeKey2) {
2423
2733
  const space = routeKey2.indexOf(" ");
@@ -2425,8 +2735,8 @@ function parseRouteKey(routeKey2) {
2425
2735
  return { method: routeKey2.slice(0, space).toUpperCase(), pattern: routeKey2.slice(space + 1) };
2426
2736
  }
2427
2737
  function patternToRegex(pattern) {
2428
- const path14 = pattern.split("?")[0] ?? pattern;
2429
- 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, "[^/]+");
2430
2740
  return new RegExp(`^${escaped}$`);
2431
2741
  }
2432
2742
  function suitePathEqual(a, b) {
@@ -2449,13 +2759,13 @@ function matchProbeItem(plan, test) {
2449
2759
  function requestMatchesRoute(method, url, routeKey2) {
2450
2760
  const { method: rm, pattern } = parseRouteKey(routeKey2);
2451
2761
  if (rm && rm !== method.toUpperCase()) return false;
2452
- let path14;
2762
+ let path13;
2453
2763
  try {
2454
- path14 = new URL(url).pathname;
2764
+ path13 = new URL(url).pathname;
2455
2765
  } catch {
2456
- path14 = url.split("?")[0] ?? url;
2766
+ path13 = url.split("?")[0] ?? url;
2457
2767
  }
2458
- return patternToRegex(pattern).test(path14);
2768
+ return patternToRegex(pattern).test(path13);
2459
2769
  }
2460
2770
  function createProbeState() {
2461
2771
  return { navigated: false, matches: 0 };
@@ -2477,10 +2787,10 @@ function isProbeMode() {
2477
2787
  function loadPlan() {
2478
2788
  if (planCache !== void 0) return planCache;
2479
2789
  planCache = null;
2480
- const path14 = process.env[PIWI_PROBE_ENV.plan];
2481
- if (path14) {
2790
+ const path13 = process.env[PIWI_PROBE_ENV.plan];
2791
+ if (path13) {
2482
2792
  try {
2483
- planCache = JSON.parse(fs2.readFileSync(path14, "utf8"));
2793
+ planCache = JSON.parse(fs2.readFileSync(path13, "utf8"));
2484
2794
  } catch {
2485
2795
  planCache = null;
2486
2796
  }
@@ -2490,7 +2800,7 @@ function loadPlan() {
2490
2800
  function probeItemForTest(test) {
2491
2801
  const plan = loadPlan();
2492
2802
  if (!plan) return null;
2493
- 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;
2494
2804
  const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2495
2805
  return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2496
2806
  }
@@ -2504,10 +2814,10 @@ function outcomeFromStatus(status, applied) {
2504
2814
  return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2505
2815
  }
2506
2816
  function recordProbeOutcome(line) {
2507
- const path14 = process.env[PIWI_PROBE_ENV.results];
2508
- if (!path14) return;
2817
+ const path13 = process.env[PIWI_PROBE_ENV.results];
2818
+ if (!path13) return;
2509
2819
  try {
2510
- fs2.appendFileSync(path14, `${JSON.stringify(line)}
2820
+ fs2.appendFileSync(path13, `${JSON.stringify(line)}
2511
2821
  `);
2512
2822
  } catch {
2513
2823
  }
@@ -2541,498 +2851,215 @@ function dropOneField(body) {
2541
2851
  let parsed2;
2542
2852
  try {
2543
2853
  parsed2 = JSON.parse(body);
2544
- } catch {
2545
- return body;
2546
- }
2547
- if (Array.isArray(parsed2)) {
2548
- let key;
2549
- for (const el of parsed2) {
2550
- if (el && typeof el === "object" && !Array.isArray(el)) {
2551
- key ??= Object.keys(el)[0];
2552
- if (key) delete el[key];
2553
- }
2554
- }
2555
- return JSON.stringify(parsed2);
2556
- }
2557
- if (parsed2 && typeof parsed2 === "object") {
2558
- const obj = parsed2;
2559
- const key = Object.keys(obj)[0];
2560
- if (key) delete obj[key];
2561
- return JSON.stringify(obj);
2562
- }
2563
- return body;
2564
- }
2565
- function computeFault(fault, input) {
2566
- const base = {
2567
- status: input.status,
2568
- body: input.body,
2569
- contentType: input.contentType ?? null,
2570
- delayMs: 0,
2571
- changed: false
2572
- };
2573
- switch (fault) {
2574
- case "status-500":
2575
- return { ...base, status: 500, changed: input.status !== 500 };
2576
- case "empty-body":
2577
- return { ...base, body: "", changed: input.body.length > 0 };
2578
- case "drop-field": {
2579
- const body = dropOneField(input.body);
2580
- return { ...base, body, changed: body !== input.body };
2581
- }
2582
- case "stale-value": {
2583
- const body = input.priorBody ?? input.body;
2584
- return { ...base, body, changed: body !== input.body };
2585
- }
2586
- case "slow":
2587
- return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
2588
- default:
2589
- return base;
2590
- }
2591
- }
2592
-
2593
- // src/internal/probe/sign.ts
2594
- var import_node_crypto = require("crypto");
2595
- function signProbeMessage(secret, nonce, ts, specJson) {
2596
- return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
2597
- }
2598
- function buildProbeHeader(secret, spec, now = Date.now()) {
2599
- const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
2600
- const specJson = JSON.stringify(spec);
2601
- const sig = signProbeMessage(secret, nonce, now, specJson);
2602
- return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
2603
- }
2604
-
2605
- // src/internal/probe/interception.ts
2606
- function parseProbeTrace(traceHeader) {
2607
- const none = { appliedFault: null, serverError: false };
2608
- if (!traceHeader) return none;
2609
- try {
2610
- const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
2611
- if (!Array.isArray(spans)) return none;
2612
- const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
2613
- if (!root) return none;
2614
- const attrs = root.attrs ?? {};
2615
- const appliedRaw = attrs["piwi.probe.applied"];
2616
- const status = Number(attrs["http.status_code"]) || 0;
2617
- return {
2618
- appliedFault: appliedRaw != null ? String(appliedRaw) : null,
2619
- serverError: root.status === "error" || status >= 500
2620
- };
2621
- } catch {
2622
- return none;
2623
- }
2624
- }
2625
- function fulfillHeaders(responseHeaders, contentType) {
2626
- const headers = { ...responseHeaders };
2627
- delete headers["content-length"];
2628
- delete headers["content-encoding"];
2629
- if (contentType) headers["content-type"] = contentType;
2630
- return headers;
2631
- }
2632
- function originOf(url) {
2633
- try {
2634
- const origin = new URL(url).origin;
2635
- return origin && origin !== "null" ? origin : null;
2636
- } catch {
2637
- return null;
2638
- }
2639
- }
2640
- var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, Math.max(0, ms)));
2641
- async function performRouteAction(route, action, startedAt) {
2642
- switch (action.kind) {
2643
- case "abort":
2644
- await route.abort("connectionreset");
2645
- return;
2646
- case "status":
2647
- await route.fulfill({ status: action.status, body: "" });
2648
- return;
2649
- case "delay": {
2650
- const response = await route.fetch();
2651
- await sleep(startedAt + action.ms - Date.now());
2652
- await route.fulfill({ response });
2653
- return;
2654
- }
2655
- }
2656
- }
2657
- async function installRouteRules(page, rules) {
2658
- const state = rules.map(() => createProbeState());
2659
- page.on("framenavigated", (frame) => {
2660
- if (frame === page.mainFrame()) for (const s of state) markNavigated(s);
2661
- });
2662
- await page.route("**/*", async (route) => {
2663
- const startedAt = Date.now();
2664
- const request = route.request();
2665
- for (let i = 0; i < rules.length; i++) {
2666
- const rule = rules[i];
2667
- if (rule.done?.() || !rule.matches(request.method(), request.url())) continue;
2668
- if (!shouldAct(state[i], rule.match)) continue;
2669
- await rule.handle(route, startedAt);
2670
- return;
2671
- }
2672
- await route.fallback();
2673
- });
2674
- return { state };
2675
- }
2676
- async function installProbeInterception(page, item) {
2677
- if (!isFaultAllowed(item.level ?? "client", item.fault)) {
2678
- return { applied: () => false, appliedFault: () => null, serverError: () => false };
2679
- }
2680
- let applied = false;
2681
- let appliedFault = null;
2682
- let serverError = false;
2683
- let appOrigin = null;
2684
- const probeSecret = process.env[PIWI_PROBE_ENV.secret];
2685
- page.on("framenavigated", (frame) => {
2686
- if (frame === page.mainFrame()) {
2687
- const origin = originOf(page.url());
2688
- if (origin) appOrigin = origin;
2689
- }
2690
- });
2691
- const handle = async (route, startedAt) => {
2692
- const request = route.request();
2693
- if (item.level === "server") {
2694
- if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
2695
- await route.fallback();
2696
- return;
2697
- }
2698
- const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
2699
- if (item.dependency) spec.dependency = item.dependency;
2700
- try {
2701
- const response = await route.fetch({
2702
- headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
2703
- });
2704
- const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
2705
- appliedFault = trace.appliedFault;
2706
- serverError = trace.serverError;
2707
- applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
2708
- await route.fulfill({ response });
2709
- } catch {
2710
- applied = false;
2711
- await route.fallback();
2712
- }
2713
- return;
2714
- }
2715
- try {
2716
- const action = faultRouteAction(item.fault);
2717
- if (action) {
2718
- applied = true;
2719
- appliedFault = item.fault;
2720
- await performRouteAction(route, action, startedAt);
2721
- return;
2722
- }
2723
- const response = await route.fetch();
2724
- const body = await response.text();
2725
- const out = computeFault(item.fault, {
2726
- status: response.status(),
2727
- body,
2728
- contentType: response.headers()["content-type"] ?? null
2729
- });
2730
- applied = out.changed;
2731
- if (applied) appliedFault = item.fault;
2732
- const headers = fulfillHeaders(response.headers(), out.contentType);
2733
- await route.fulfill({ status: out.status, body: out.body, headers });
2734
- } catch {
2735
- applied = false;
2736
- await route.fallback();
2737
- }
2738
- };
2739
- await installRouteRules(page, [
2740
- {
2741
- matches: (method, url) => requestMatchesRoute(method, url, item.routeKey),
2742
- match: Math.max(1, item.nth ?? 1),
2743
- handle,
2744
- done: () => applied
2745
- }
2746
- ]);
2747
- return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
2748
- }
2749
-
2750
- // src/internal/flake/mode.ts
2751
- var fs3 = __toESM(require("fs"));
2752
- var path4 = __toESM(require("path"));
2753
-
2754
- // ../core/src/locator-chain.ts
2755
- var LOCATING_METHODS = /* @__PURE__ */ new Set([
2756
- "getByRole",
2757
- "getByTestId",
2758
- "getByText",
2759
- "getByLabel",
2760
- "getByPlaceholder",
2761
- "getByAltText",
2762
- "getByTitle",
2763
- "locator",
2764
- "frameLocator"
2765
- ]);
2766
- var NARROWING_METHODS = /* @__PURE__ */ new Set([
2767
- "filter",
2768
- "first",
2769
- "last",
2770
- "nth",
2771
- "and",
2772
- "or",
2773
- "visible",
2774
- "contentFrame",
2775
- "owner"
2776
- ]);
2777
- var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
2778
- var LocatorParseError = class extends Error {
2779
- constructor(message) {
2780
- super(message);
2781
- this.name = "LocatorParseError";
2782
- }
2783
- };
2784
- var SIMPLE_ESCAPES = {
2785
- n: "\n",
2786
- t: " ",
2787
- r: "\r",
2788
- b: "\b",
2789
- f: "\f",
2790
- v: "\v",
2791
- "0": "\0"
2792
- };
2793
- var Parser = class {
2794
- constructor(src) {
2795
- this.src = src;
2796
- this.i = 0;
2797
- }
2798
- parseTopLevel() {
2799
- const chain = this.parseChain();
2800
- this.skipSpace();
2801
- if (this.i < this.src.length) this.fail("unexpected text");
2802
- return chain;
2803
- }
2804
- fail(message) {
2805
- throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
2806
- }
2807
- skipSpace() {
2808
- while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
2809
- }
2810
- peek() {
2811
- return this.src[this.i];
2812
- }
2813
- expect(ch) {
2814
- this.skipSpace();
2815
- if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
2816
- this.i++;
2817
- }
2818
- identifier() {
2819
- const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
2820
- if (!m) this.fail("expected a name");
2821
- this.i += m[0].length;
2822
- return m[0];
2823
- }
2824
- /**
2825
- * A chain of calls joined by `.`. With `lenient`, parsing stops before the
2826
- * first link that doesn't parse instead of failing, so a chain followed by
2827
- * other text (an error message) is read up to where it ends. `spans` receives
2828
- * where each top-level call starts and ends.
2829
- */
2830
- parseChain(opts = {}) {
2831
- this.skipSpace();
2832
- const calls = [];
2833
- for (; ; ) {
2834
- const before = this.i;
2835
- if (calls.length > 0) this.i++;
2836
- this.skipSpace();
2837
- const start = this.i;
2838
- try {
2839
- calls.push(this.parseCall(calls.length === 0));
2840
- } catch (error) {
2841
- if (!opts.lenient || calls.length === 0) throw error;
2842
- this.i = before;
2843
- break;
2844
- }
2845
- opts.spans?.push({ start, end: this.i });
2846
- const end = this.i;
2847
- this.skipSpace();
2848
- if (this.peek() !== ".") {
2849
- this.i = end;
2850
- break;
2851
- }
2852
- }
2853
- return { calls };
2854
- }
2855
- /** Where the parser stopped. */
2856
- get position() {
2857
- return this.i;
2858
- }
2859
- parseCall(first) {
2860
- this.skipSpace();
2861
- const method = this.identifier();
2862
- if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
2863
- if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
2864
- this.expect("(");
2865
- const args = [];
2866
- this.skipSpace();
2867
- if (this.peek() === ")") {
2868
- this.i++;
2869
- return { method, args };
2870
- }
2871
- for (; ; ) {
2872
- args.push(this.parseValue());
2873
- this.skipSpace();
2874
- if (this.peek() === ",") {
2875
- this.i++;
2876
- this.skipSpace();
2877
- if (this.peek() === ")") {
2878
- this.i++;
2879
- break;
2880
- }
2881
- continue;
2882
- }
2883
- this.expect(")");
2884
- break;
2885
- }
2886
- return { method, args };
2887
- }
2888
- parseValue() {
2889
- this.skipSpace();
2890
- const ch = this.peek();
2891
- if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
2892
- if (ch === "/") return this.parseRegex();
2893
- if (ch === "{") return this.parseObject();
2894
- if (ch !== void 0 && /[-\d]/.test(ch)) {
2895
- const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
2896
- if (!m) this.fail("expected a number");
2897
- this.i += m[0].length;
2898
- return { type: "number", value: Number(m[0]) };
2899
- }
2900
- const rest = this.src.slice(this.i);
2901
- if (/^true\b/.test(rest)) {
2902
- this.i += 4;
2903
- return { type: "boolean", value: true };
2904
- }
2905
- if (/^false\b/.test(rest)) {
2906
- this.i += 5;
2907
- return { type: "boolean", value: false };
2908
- }
2909
- if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
2910
- return this.fail("expected a value");
2911
- }
2912
- /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
2913
- parseString() {
2914
- const quote = this.src[this.i];
2915
- let out = "";
2916
- this.i++;
2917
- while (this.i < this.src.length) {
2918
- const c = this.src[this.i];
2919
- if (c === quote) {
2920
- this.i++;
2921
- return out;
2922
- }
2923
- if (c !== "\\") {
2924
- out += c;
2925
- this.i++;
2926
- continue;
2927
- }
2928
- const next = this.src[this.i + 1];
2929
- if (next === void 0) break;
2930
- const simple = SIMPLE_ESCAPES[next];
2931
- if (simple !== void 0) {
2932
- out += simple;
2933
- this.i += 2;
2934
- continue;
2935
- }
2936
- 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;
2937
- if (hex) {
2938
- out += String.fromCodePoint(parseInt(hex[1], 16));
2939
- this.i += 1 + hex[0].length;
2940
- continue;
2941
- }
2942
- if (next === "u" || next === "x") this.fail("malformed escape");
2943
- out += next;
2944
- this.i += 2;
2945
- }
2946
- return this.fail("unterminated string");
2947
- }
2948
- parseRegex() {
2949
- let j = this.i + 1;
2950
- let inClass = false;
2951
- while (j < this.src.length) {
2952
- const c = this.src[j];
2953
- if (c === "\\") {
2954
- j += 2;
2955
- continue;
2956
- }
2957
- if (c === "[") inClass = true;
2958
- else if (c === "]") inClass = false;
2959
- else if (c === "/" && !inClass) break;
2960
- j++;
2961
- }
2962
- if (j >= this.src.length) this.fail("unterminated regex");
2963
- const source = this.src.slice(this.i + 1, j);
2964
- const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
2965
- this.i = j + 1 + flags.length;
2966
- return { type: "regex", source, flags };
2967
- }
2968
- parseObject() {
2969
- this.expect("{");
2970
- const entries = [];
2971
- for (; ; ) {
2972
- this.skipSpace();
2973
- if (this.peek() === "}") {
2974
- this.i++;
2975
- break;
2976
- }
2977
- const ch = this.peek();
2978
- const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
2979
- this.expect(":");
2980
- entries.push([key, this.parseValue()]);
2981
- this.skipSpace();
2982
- if (this.peek() === ",") {
2983
- this.i++;
2984
- continue;
2854
+ } catch {
2855
+ return body;
2856
+ }
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];
2985
2863
  }
2986
- this.expect("}");
2987
- break;
2988
2864
  }
2989
- return { type: "object", entries };
2865
+ return JSON.stringify(parsed2);
2990
2866
  }
2991
- };
2992
- function parseLocatorChain(expr) {
2993
- const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
2994
- if (!trimmed) throw new LocatorParseError("empty expression");
2995
- return new Parser(trimmed).parseTopLevel();
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;
2996
2874
  }
2997
- function tryParseLocatorChain(expr) {
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 };
2891
+ }
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;
2900
+ }
2901
+ }
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;
2998
2919
  try {
2999
- 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;
3000
2946
  } catch {
3001
2947
  return null;
3002
2948
  }
3003
2949
  }
3004
- function parseLeafLocatorCall(expr) {
3005
- const chain = tryParseLocatorChain(expr);
3006
- if (!chain) return null;
3007
- for (let i = chain.calls.length - 1; i >= 0; i--) {
3008
- 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
+ }
3009
2965
  }
3010
- return null;
3011
2966
  }
3012
- function locatorCallValues(call, opts = {}) {
3013
- const value = (arg) => {
3014
- switch (arg.type) {
3015
- case "string":
3016
- case "number":
3017
- case "boolean":
3018
- return arg.value;
3019
- case "regex":
3020
- return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
3021
- case "object": {
3022
- const out = {};
3023
- for (const [key, v] of arg.entries) {
3024
- const plain = value(v);
3025
- if (plain !== void 0) out[key] = plain;
3026
- }
3027
- 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;
3028
3007
  }
3029
- case "chain":
3030
- 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();
3031
3047
  }
3032
3048
  };
3033
- 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 };
3034
3058
  }
3035
3059
 
3060
+ // src/internal/flake/mode.ts
3061
+ var fs3 = __toESM(require("fs"));
3062
+
3036
3063
  // ../core/src/error-parse.ts
3037
3064
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
3038
3065
  function stripAnsi(text) {
@@ -3055,11 +3082,20 @@ function extractMessageHead(text) {
3055
3082
  let head = text;
3056
3083
  const callLogIdx = head.indexOf("\nCall log:");
3057
3084
  if (callLogIdx !== -1) head = head.slice(0, callLogIdx);
3058
- const stackIdx = head.search(/\n\s+at /);
3085
+ const stackIdx = stackStart(head);
3059
3086
  if (stackIdx !== -1) head = head.slice(0, stackIdx);
3060
3087
  const lines = head.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
3061
3088
  return lines.slice(0, 5).join("\n");
3062
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
+ }
3063
3099
  function extractSelector(text) {
3064
3100
  const match = SELECTOR_FN_RE.exec(text);
3065
3101
  if (!match) return null;
@@ -3080,7 +3116,7 @@ function extractSelector(text) {
3080
3116
  }
3081
3117
  var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
3082
3118
  function extractStackFrames(text) {
3083
- const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
3119
+ const frameRe = /^[ \t]+at (?:.*? \()?([^()\s][^()\n]*?):(\d+):(\d+)\)?[ \t]*$/gm;
3084
3120
  const frames = [];
3085
3121
  let m;
3086
3122
  while ((m = frameRe.exec(text)) !== null) {
@@ -3098,6 +3134,11 @@ function extractTopFrameFile(text) {
3098
3134
  }
3099
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";
3100
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;
3101
3142
 
3102
3143
  // ../core/src/error-signature.ts
3103
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;
@@ -3340,12 +3381,12 @@ function loadFlakePlan() {
3340
3381
  throw new Error(`[Piwi Dashboard] ${errorMessage(error)} (${file})`);
3341
3382
  }
3342
3383
  }
3343
- function relativeTestFile(file) {
3344
- 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;
3345
3386
  }
3346
3387
  function flakeRoleForTest(plan, test) {
3347
3388
  const running = {
3348
- file: relativeTestFile(test.file),
3389
+ file: relativeTestFile(test.file, test.base),
3349
3390
  title: test.title,
3350
3391
  suite: test.titlePath ? test.titlePath.slice(1, -1) : void 0
3351
3392
  };
@@ -3461,7 +3502,7 @@ function flakeResultLine(plan, attempt) {
3461
3502
  experimentId: plan.experimentId,
3462
3503
  armId: plan.arm.id,
3463
3504
  role: attempt.role,
3464
- file: relativeTestFile(attempt.file),
3505
+ file: relativeTestFile(attempt.file, attempt.base),
3465
3506
  title: attempt.title,
3466
3507
  project: attempt.project,
3467
3508
  browserName: attempt.browserName,
@@ -3547,13 +3588,13 @@ function normalizeFunctionRanges(ranges) {
3547
3588
  }
3548
3589
  function normalizeCoveragePath(raw) {
3549
3590
  if (typeof raw !== "string" || raw.length > MAX_PATH_LENGTH || raw.includes("\0")) return null;
3550
- const path14 = raw.replace(/\\/g, "/").replace(/^\.\//, "");
3551
- if (!path14 || path14.startsWith("../") || path14.startsWith("/") || /^[A-Za-z]:\//.test(path14)) return null;
3552
- if (/(^|\/)node_modules\//.test(path14)) return null;
3553
- return path14;
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;
3554
3595
  }
3555
- function fileEntry(path14, runtime, blob, functions = [], lines = []) {
3556
- const file = { path: path14, runtime };
3596
+ function fileEntry(path13, runtime, blob, functions = [], lines = []) {
3597
+ const file = { path: path13, runtime };
3557
3598
  if (blob && (functions.length > 0 || lines.length > 0)) file.blob = blob;
3558
3599
  if (functions.length > 0) file.functions = functions;
3559
3600
  if (lines.length > 0) file.lines = lines;
@@ -3572,11 +3613,11 @@ function sanitizeCoverage(raw) {
3572
3613
  const files = [];
3573
3614
  for (const item of raw.files) {
3574
3615
  if (!isRecord(item) || !isRuntime(item.runtime)) continue;
3575
- const path14 = normalizeCoveragePath(item.path);
3576
- if (path14 === null) continue;
3616
+ const path13 = normalizeCoveragePath(item.path);
3617
+ if (path13 === null) continue;
3577
3618
  const blob = typeof item.blob === "string" && BLOB_ID.test(item.blob) ? item.blob : void 0;
3578
3619
  files.push(
3579
- fileEntry(path14, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3620
+ fileEntry(path13, item.runtime, blob, normalizeFunctionRanges(item.functions), normalizeLineRanges(item.lines))
3580
3621
  );
3581
3622
  }
3582
3623
  const merged = mergeCoverage(captureOf(files, raw.incomplete === true));
@@ -3768,14 +3809,6 @@ function decodeSourceMap(raw) {
3768
3809
  return originalAt(line, column)?.source ?? null;
3769
3810
  },
3770
3811
  originalAt,
3771
- mapsBetween(source, start, end) {
3772
- let found = false;
3773
- eachSegmentBetween(segments, start, end, (i) => {
3774
- found = segments.sources[i] === source;
3775
- return found;
3776
- });
3777
- return found;
3778
- },
3779
3812
  firstOriginalLineBetween(source, start, end) {
3780
3813
  let first = null;
3781
3814
  eachSegmentBetween(segments, start, end, (i) => {
@@ -3935,6 +3968,30 @@ function finalizeCodeReach(files) {
3935
3968
  return [...out].sort().slice(0, MAX_CODE_REACH_FILES);
3936
3969
  }
3937
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;
3993
+ }
3994
+
3938
3995
  // src/internal/capture/inspect-on-failure.ts
3939
3996
  function isCi(ci) {
3940
3997
  return ci !== void 0 && ci !== "" && ci !== "false";
@@ -3971,8 +4028,7 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
3971
4028
  }
3972
4029
 
3973
4030
  // src/internal/capture/pick-on-failure.ts
3974
- var path5 = __toESM(require("path"));
3975
- var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
4031
+ var path4 = __toESM(require("path"));
3976
4032
  function parseLeafLocatorExpression(rawExpr) {
3977
4033
  const leaf = parseLeafLocatorCall(rawExpr);
3978
4034
  return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
@@ -3981,14 +4037,13 @@ function deriveFailedLocator(testInfo) {
3981
4037
  const info = testInfo;
3982
4038
  const errors = info.errors && info.errors.length > 0 ? info.errors : info.error ? [info.error] : [];
3983
4039
  for (const err of errors) {
3984
- const text = `${err.message ?? ""}
3985
- ${err.stack ?? ""}`.replace(ANSI_RE2, "");
3986
- const line = /^\s*Locator:\s*(.+)$/m.exec(text);
3987
- if (!line) continue;
3988
- 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);
3989
4044
  if (!parsed2) continue;
3990
4045
  const loc = err.location;
3991
- 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;
3992
4047
  return { method: parsed2.method, args: parsed2.args, location };
3993
4048
  }
3994
4049
  return null;
@@ -4141,7 +4196,7 @@ function applyPickToSnapshots(snapshots, pick) {
4141
4196
  }
4142
4197
 
4143
4198
  // src/internal/capture/pause-at.ts
4144
- var path6 = __toESM(require("path"));
4199
+ var path5 = __toESM(require("path"));
4145
4200
 
4146
4201
  // ../core/src/editor-send.ts
4147
4202
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
@@ -4185,9 +4240,9 @@ var PauseSet = class {
4185
4240
  var EMPTY = new PauseSet(/* @__PURE__ */ new Set());
4186
4241
  function callSiteFile(file, cwd) {
4187
4242
  const slashed = file.replace(/\\/g, "/");
4188
- const absolute = path6.isAbsolute(file) || path6.isAbsolute(slashed) || /^[A-Za-z]:\//.test(slashed);
4189
- let rel = absolute ? path6.relative(cwd, file) : path6.normalize(slashed);
4190
- 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, "/");
4191
4246
  if (rel.startsWith("./")) rel = rel.slice(2);
4192
4247
  return rel;
4193
4248
  }
@@ -4352,7 +4407,7 @@ function browserFunctionsInWorker() {
4352
4407
  function browserFunctionCoverage(files) {
4353
4408
  return sanitizeCoverage({
4354
4409
  v: 1,
4355
- files: [...files].map(([path14, functions]) => ({ path: path14, runtime: "browser", functions }))
4410
+ files: [...files].map(([path13, functions]) => ({ path: path13, runtime: "browser", functions }))
4356
4411
  });
4357
4412
  }
4358
4413
  function newKnownFunctions(known, sent) {
@@ -4365,19 +4420,19 @@ function newKnownFunctions(known, sent) {
4365
4420
  budget = MAX_COVERAGE_RANGES;
4366
4421
  };
4367
4422
  for (const [raw, ranges] of known) {
4368
- const path14 = normalizeCoveragePath(raw);
4369
- if (path14 === null) continue;
4370
- let named = sent.get(path14);
4423
+ const path13 = normalizeCoveragePath(raw);
4424
+ if (path13 === null) continue;
4425
+ let named = sent.get(path13);
4371
4426
  let fresh = normalizeFunctionRanges(ranges).filter((range) => !named?.has(`${range[0]}:${range[1]}`));
4372
4427
  if (fresh.length === 0) continue;
4373
- if (!named) sent.set(path14, named = /* @__PURE__ */ new Set());
4428
+ if (!named) sent.set(path13, named = /* @__PURE__ */ new Set());
4374
4429
  for (const range of fresh) named.add(`${range[0]}:${range[1]}`);
4375
4430
  while (fresh.length > 0) {
4376
4431
  if (budget === 0 || files.length >= MAX_COVERAGE_FILES) close();
4377
4432
  const part = fresh.slice(0, budget);
4378
4433
  fresh = fresh.slice(part.length);
4379
4434
  budget -= part.length;
4380
- files.push({ path: path14, allFunctions: part });
4435
+ files.push({ path: path13, allFunctions: part });
4381
4436
  }
4382
4437
  }
4383
4438
  close();
@@ -4388,7 +4443,7 @@ function serverCoverageInWorker() {
4388
4443
  }
4389
4444
 
4390
4445
  // src/internal/support/aria-sampling.ts
4391
- var path7 = __toESM(require("path"));
4446
+ var path6 = __toESM(require("path"));
4392
4447
  var os3 = __toESM(require("os"));
4393
4448
  var fs4 = __toESM(require("fs"));
4394
4449
 
@@ -4404,7 +4459,7 @@ function ariaSampleIdentity(filePath, title) {
4404
4459
  return `${filePath}\0${title}`;
4405
4460
  }
4406
4461
  function getAriaSampleFilePath(projectName) {
4407
- return path7.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4462
+ return path6.join(os3.tmpdir(), `piwi-dashboard-aria-sample-${hashForProject(projectName)}.json`);
4408
4463
  }
4409
4464
  var cachedSets = /* @__PURE__ */ new Map();
4410
4465
  function loadAriaSampleSet(projectName) {
@@ -4422,33 +4477,32 @@ function loadAriaSampleSet(projectName) {
4422
4477
  cachedSets.set(projectName, set);
4423
4478
  return set;
4424
4479
  }
4425
- function relativeTestFile2(file) {
4426
- return path7.relative(process.cwd(), file).split(path7.sep).join("/");
4427
- }
4428
4480
  function isDueForAriaSample(testInfo) {
4429
4481
  const projectName = process.env.PIWI_PROJECT_NAME;
4430
4482
  if (!projectName) return false;
4431
4483
  const set = loadAriaSampleSet(projectName);
4432
4484
  if (!set || set.size === 0) return false;
4433
- return set.has(ariaSampleIdentity(relativeTestFile2(testInfo.file), testInfo.title));
4485
+ return set.has(
4486
+ ariaSampleIdentity(relativeTestPath(testInfo.file, configDirFromConfig(testInfo.config)), testInfo.title)
4487
+ );
4434
4488
  }
4435
4489
 
4436
4490
  // src/internal/capture/quiet-capture.ts
4437
4491
  var import_node_module = require("module");
4438
- var path9 = __toESM(require("path"));
4492
+ var path8 = __toESM(require("path"));
4439
4493
 
4440
4494
  // src/internal/support/reporter-version.ts
4441
4495
  var fs5 = __toESM(require("fs"));
4442
- var path8 = __toESM(require("path"));
4496
+ var path7 = __toESM(require("path"));
4443
4497
  var CANDIDATE_OFFSETS = ["..", "../../.."];
4444
4498
  function findOwnPackageJson(fromDir) {
4445
4499
  for (const offset of CANDIDATE_OFFSETS) {
4446
4500
  try {
4447
- const pkgPath = path8.resolve(fromDir, offset, "package.json");
4501
+ const pkgPath = path7.resolve(fromDir, offset, "package.json");
4448
4502
  const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
4449
4503
  const { name, version } = pkg;
4450
4504
  if (name === "@piwitests/reporter" && typeof version === "string")
4451
- return { version, root: path8.dirname(pkgPath) };
4505
+ return { version, root: path7.dirname(pkgPath) };
4452
4506
  } catch {
4453
4507
  }
4454
4508
  }
@@ -4492,7 +4546,7 @@ function resolveBoxing() {
4492
4546
  if (typeof set !== "function") continue;
4493
4547
  return {
4494
4548
  set,
4495
- prefixes: [path9.dirname(playwrightPackageJson), path9.join(root, "dist") + path9.sep]
4549
+ prefixes: [path8.dirname(playwrightPackageJson), path8.join(root, "dist") + path8.sep]
4496
4550
  };
4497
4551
  }
4498
4552
  } catch {
@@ -4523,7 +4577,7 @@ function internalCall(owner, run) {
4523
4577
  // src/internal/capture/code-reach.ts
4524
4578
  var crypto2 = __toESM(require("crypto"));
4525
4579
  var fs6 = __toESM(require("fs"));
4526
- var path10 = __toESM(require("path"));
4580
+ var path9 = __toESM(require("path"));
4527
4581
  var import_node_child_process = require("child_process");
4528
4582
  var MAX_MAP_BYTES = 20 * 1024 * 1024;
4529
4583
  var MAP_TIMEOUT_MS = 5e3;
@@ -4566,12 +4620,12 @@ function repositoryRoot(from) {
4566
4620
  }).trim();
4567
4621
  } catch {
4568
4622
  }
4569
- cachedRepoRoot = { from, root: path10.resolve(root) };
4623
+ cachedRepoRoot = { from, root: path9.resolve(root) };
4570
4624
  return cachedRepoRoot.root;
4571
4625
  }
4572
4626
  function codeReachRoots(configDir, configured) {
4573
4627
  const repoRoot = repositoryRoot(configDir);
4574
- const resolve3 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path10.resolve(configDir, r));
4628
+ const resolve3 = (dirs) => (Array.isArray(dirs) ? dirs : dirs ? [dirs] : []).filter(Boolean).map((r) => path9.resolve(configDir, r));
4575
4629
  const global = Array.isArray(configured) ? resolve3(configured) : resolve3(configured?.["*"]);
4576
4630
  const byOrigin = /* @__PURE__ */ new Map();
4577
4631
  if (configured && !Array.isArray(configured)) {
@@ -4608,10 +4662,10 @@ function isFile(file) {
4608
4662
  function resolveSourcePath(source, roots) {
4609
4663
  const normalized = normalizeSourcePath(source);
4610
4664
  if (!normalized || /(?:^|\/)node_modules\//.test(normalized)) return null;
4611
- if (path10.isAbsolute(normalized) && isFile(normalized)) return path10.resolve(normalized);
4612
- const relative9 = normalized.replace(/^\/+/, "");
4665
+ if (path9.isAbsolute(normalized) && isFile(normalized)) return path9.resolve(normalized);
4666
+ const relative7 = normalized.replace(/^\/+/, "");
4613
4667
  for (const root of roots) {
4614
- const candidate = path10.resolve(root, relative9);
4668
+ const candidate = path9.resolve(root, relative7);
4615
4669
  if (isFile(candidate)) return candidate;
4616
4670
  }
4617
4671
  return null;
@@ -4643,7 +4697,7 @@ function fileForUrl(url, roots, bases) {
4643
4697
  return file;
4644
4698
  }
4645
4699
  function relativeToRepo(file, repoRoot) {
4646
- return path10.relative(repoRoot, file).split(path10.sep).join("/");
4700
+ return path9.relative(repoRoot, file).split(path9.sep).join("/");
4647
4701
  }
4648
4702
  function scriptVersion(entry) {
4649
4703
  return entry.hash ?? crypto2.createHash("sha1").update(entry.source ?? "").digest("hex");
@@ -4706,10 +4760,10 @@ async function resolveFunctions(entries, roots, fetchMap, pageUrl, withKnown) {
4706
4760
  const files = /* @__PURE__ */ new Map();
4707
4761
  const known = /* @__PURE__ */ new Map();
4708
4762
  const addTo = (into, file, functions) => {
4709
- const path14 = relativeToRepo(file, roots.repoRoot);
4710
- const listed = into.get(path14);
4763
+ const path13 = relativeToRepo(file, roots.repoRoot);
4764
+ const listed = into.get(path13);
4711
4765
  if (listed) listed.push(...functions);
4712
- else into.set(path14, [...functions]);
4766
+ else into.set(path13, [...functions]);
4713
4767
  };
4714
4768
  const add = (file, functions) => addTo(files, file, functions);
4715
4769
  const bases = viteBases(entries);
@@ -4973,7 +5027,7 @@ async function stopCodeReach(page) {
4973
5027
 
4974
5028
  // src/internal/capture/resource-ledger.ts
4975
5029
  var fs8 = __toESM(require("fs"));
4976
- var path12 = __toESM(require("path"));
5030
+ var path11 = __toESM(require("path"));
4977
5031
  var import_node_module2 = require("module");
4978
5032
 
4979
5033
  // src/internal/capture/worker-metrics.ts
@@ -4981,7 +5035,7 @@ var import_node_perf_hooks = require("perf_hooks");
4981
5035
 
4982
5036
  // src/internal/support/system-readers.ts
4983
5037
  var fs7 = __toESM(require("fs"));
4984
- var path11 = __toESM(require("path"));
5038
+ var path10 = __toESM(require("path"));
4985
5039
  var CLK_TCK = 100;
4986
5040
  function readText(file) {
4987
5041
  try {
@@ -5067,7 +5121,7 @@ var ProcFs = class {
5067
5121
  }
5068
5122
  /** Whether this looks like a Linux `/proc` at all. */
5069
5123
  available() {
5070
- return fs7.existsSync(path11.join(this.root, "stat"));
5124
+ return fs7.existsSync(path10.join(this.root, "stat"));
5071
5125
  }
5072
5126
  /** Every process, keyed by pid. */
5073
5127
  table() {
@@ -5080,32 +5134,32 @@ var ProcFs = class {
5080
5134
  }
5081
5135
  for (const name of names) {
5082
5136
  if (!/^\d+$/.test(name)) continue;
5083
- const text = readText(path11.join(this.root, name, "stat"));
5137
+ const text = readText(path10.join(this.root, name, "stat"));
5084
5138
  const stat = text ? parseProcStat(Number(name), text) : null;
5085
5139
  if (stat) out.set(stat.pid, stat);
5086
5140
  }
5087
5141
  return out;
5088
5142
  }
5089
5143
  stat(pid) {
5090
- const text = readText(path11.join(this.root, String(pid), "stat"));
5144
+ const text = readText(path10.join(this.root, String(pid), "stat"));
5091
5145
  return text ? parseProcStat(pid, text) : null;
5092
5146
  }
5093
5147
  runWaitMs(pid) {
5094
- const text = readText(path11.join(this.root, String(pid), "schedstat"));
5148
+ const text = readText(path10.join(this.root, String(pid), "schedstat"));
5095
5149
  return text ? parseSchedstat(text) : null;
5096
5150
  }
5097
5151
  cmdline(pid) {
5098
- 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();
5099
5153
  }
5100
5154
  /** A field of `/proc/<pid>/status`, in kB: `VmRSS`, `VmHWM`. */
5101
5155
  statusKb(pid, key) {
5102
- const text = readText(path11.join(this.root, String(pid), "status"));
5156
+ const text = readText(path10.join(this.root, String(pid), "status"));
5103
5157
  return text ? parseKbField(text, key) : null;
5104
5158
  }
5105
5159
  /** Proportional set size: shared pages split between the processes sharing them, so a sum counts them once. */
5106
5160
  async pssKb(pid) {
5107
5161
  try {
5108
- 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");
5109
5163
  } catch {
5110
5164
  return null;
5111
5165
  }
@@ -5113,29 +5167,29 @@ var ProcFs = class {
5113
5167
  /** Reset a process's peak RSS (`VmHWM`) to its current RSS. */
5114
5168
  resetPeakRss(pid) {
5115
5169
  try {
5116
- fs7.writeFileSync(path11.join(this.root, String(pid), "clear_refs"), "5");
5170
+ fs7.writeFileSync(path10.join(this.root, String(pid), "clear_refs"), "5");
5117
5171
  return true;
5118
5172
  } catch {
5119
5173
  return false;
5120
5174
  }
5121
5175
  }
5122
5176
  cpuTicks() {
5123
- const text = readText(path11.join(this.root, "stat"));
5177
+ const text = readText(path10.join(this.root, "stat"));
5124
5178
  return text ? parseCpuTicks(text) : null;
5125
5179
  }
5126
5180
  /** Total stall time of a resource's pressure file, in µs. */
5127
5181
  pressureUs(resource) {
5128
- const text = readText(path11.join(this.root, "pressure", resource));
5182
+ const text = readText(path10.join(this.root, "pressure", resource));
5129
5183
  return text ? parsePsiTotal(text) : null;
5130
5184
  }
5131
5185
  memAvailableKb() {
5132
- const text = readText(path11.join(this.root, "meminfo"));
5186
+ const text = readText(path10.join(this.root, "meminfo"));
5133
5187
  return text ? parseKbField(text, "MemAvailable") : null;
5134
5188
  }
5135
5189
  /** Open file descriptors of a process. */
5136
5190
  fdCount(pid) {
5137
5191
  try {
5138
- return fs7.readdirSync(path11.join(this.root, String(pid), "fd")).length;
5192
+ return fs7.readdirSync(path10.join(this.root, String(pid), "fd")).length;
5139
5193
  } catch {
5140
5194
  return null;
5141
5195
  }
@@ -5297,10 +5351,10 @@ var PAGE_READ_TIMEOUT_MS = 1e3;
5297
5351
  var HOOK_PHASES = /* @__PURE__ */ new Set(["beforeAll", "afterAll", "beforeEach", "afterEach"]);
5298
5352
  var KIND_ORDER = { browser: 0, context: 1, page: 2, request: 3 };
5299
5353
  function relativePath(file) {
5300
- 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("/");
5301
5355
  }
5302
5356
  function isLibraryFile(file) {
5303
- 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;
5304
5358
  }
5305
5359
  function userSite(stack) {
5306
5360
  for (const line of (stack ?? "").split("\n").slice(1)) {
@@ -5348,7 +5402,7 @@ function playwrightTestInfoSource() {
5348
5402
  try {
5349
5403
  const packageJson = resolvePlaywrightPackageJson();
5350
5404
  if (!packageJson) return () => null;
5351
- 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"));
5352
5406
  const read = globals.currentTestInfo;
5353
5407
  if (typeof read !== "function") return () => null;
5354
5408
  return () => {
@@ -6170,7 +6224,7 @@ function configuredCodeReachRoots() {
6170
6224
  }
6171
6225
  } catch {
6172
6226
  }
6173
- return raw.split(path13.delimiter).filter(Boolean);
6227
+ return raw.split(path12.delimiter).filter(Boolean);
6174
6228
  }
6175
6229
  async function stopSinkCodeReach(sink, closing) {
6176
6230
  const stopping = [];
@@ -6186,9 +6240,7 @@ async function stopSinkCodeReach(sink, closing) {
6186
6240
  }
6187
6241
  if (stopping.length > 0) {
6188
6242
  const info = sink.testInfo;
6189
- const configFile = info?.config.configFile;
6190
- const configDir = configFile ? path13.dirname(configFile) : info?.config.rootDir ?? process.cwd();
6191
- const roots = codeReachRoots(configDir, configuredCodeReachRoots());
6243
+ const roots = codeReachRoots(configDirFromConfig(info?.config), configuredCodeReachRoots());
6192
6244
  for (const [page, session] of stopping) {
6193
6245
  session.stopping = stopPageCodeReach(sink, page, session, roots);
6194
6246
  underway.push(session.stopping);
@@ -6330,8 +6382,8 @@ function buildPageState(raw, cookies, interaction) {
6330
6382
  length: typeof e.length === "number" ? e.length : 0
6331
6383
  }));
6332
6384
  return {
6333
- url: raw.url,
6334
- hash: raw.hash || null,
6385
+ url: raw.url.split("#")[0],
6386
+ hash: raw.hash ? maskUrlFragment(raw.hash) : null,
6335
6387
  historyState: historyState || null,
6336
6388
  localStorage: capStorage(raw.localStorage),
6337
6389
  sessionStorage: capStorage(raw.sessionStorage),
@@ -7348,6 +7400,7 @@ var piwiFixtures = {
7348
7400
  const plan = loadFlakePlan();
7349
7401
  const role = flakeRoleForTest(plan, {
7350
7402
  file: testInfo.file,
7403
+ base: configDirFromConfig(testInfo.config),
7351
7404
  title: testInfo.title,
7352
7405
  titlePath: testInfo.titlePath,
7353
7406
  project: testInfo.project.name
@@ -7361,7 +7414,8 @@ var piwiFixtures = {
7361
7414
  sink.probeItem = probeItemForTest({
7362
7415
  title: testInfo.title,
7363
7416
  file: testInfo.file,
7364
- titlePath: testInfo.titlePath
7417
+ titlePath: testInfo.titlePath,
7418
+ base: configDirFromConfig(testInfo.config)
7365
7419
  });
7366
7420
  activeResourceLedger()?.testStarted(testInfo);
7367
7421
  currentSink = sink;
@@ -7405,6 +7459,7 @@ async function recordFlakeAttempt(flake, testInfo) {
7405
7459
  flakeResultLine(flake.plan, {
7406
7460
  role: flake.role,
7407
7461
  file: testInfo.file,
7462
+ base: configDirFromConfig(testInfo.config),
7408
7463
  title: testInfo.title,
7409
7464
  project: testInfo.project.name || null,
7410
7465
  browserName: flake.browserName,