@piwitests/reporter 0.36.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ __export(index_exports, {
43
43
  module.exports = __toCommonJS(index_exports);
44
44
 
45
45
  // src/public/reporter.ts
46
- var path15 = __toESM(require("path"));
46
+ var path17 = __toESM(require("path"));
47
47
 
48
48
  // src/internal/config/desktop.ts
49
49
  var fs = __toESM(require("fs"));
@@ -76,8 +76,12 @@ var DEFAULTS = {
76
76
  collectPerformanceMetrics: true,
77
77
  captureLocators: true,
78
78
  capturePageState: true,
79
+ // Off by default: the page inventory reads control and link names from every
80
+ // visited page, so it stays opt-in until a project turns it on.
81
+ capturePageInventory: false,
79
82
  captureServerTraces: true,
80
83
  sampleAriaOnPass: true,
84
+ uploadManifest: true,
81
85
  defaultCapture: true,
82
86
  streaming: true,
83
87
  streamingBatchSize: 5,
@@ -99,6 +103,7 @@ var PIWI_ENV_KEYS = {
99
103
  environment: "PIWI_ENVIRONMENT",
100
104
  label: "PIWI_LABEL",
101
105
  runLabel: "PIWI_RUN_LABEL",
106
+ keep: "PIWI_KEEP",
102
107
  streaming: "PIWI_STREAMING",
103
108
  streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
104
109
  streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
@@ -109,8 +114,10 @@ var PIWI_ENV_KEYS = {
109
114
  uploadReport: "PIWI_UPLOAD_REPORT",
110
115
  captureLocators: "PIWI_CAPTURE_LOCATORS",
111
116
  capturePageState: "PIWI_CAPTURE_PAGE_STATE",
117
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
112
118
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
113
119
  sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
120
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
114
121
  defaultCapture: "PIWI_DEFAULT_CAPTURE",
115
122
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
116
123
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
@@ -132,6 +139,12 @@ var PIWI_SELECTION_ENV = {
132
139
  hash: "PIWI_SELECTION_HASH",
133
140
  count: "PIWI_SELECTION_COUNT"
134
141
  };
142
+ var PIWI_PROBE_ENV = {
143
+ flag: "PIWI_PROBE",
144
+ plan: "PIWI_PROBE_PLAN",
145
+ results: "PIWI_PROBE_RESULTS",
146
+ secret: "PIWI_PROBE_SECRET"
147
+ };
135
148
  function readBool(val) {
136
149
  if (val === void 0) return void 0;
137
150
  return val === "true";
@@ -145,6 +158,7 @@ var ENV_FALLBACK_SPECS = [
145
158
  { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
146
159
  { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
147
160
  { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
161
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
148
162
  { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
149
163
  { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
150
164
  { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
@@ -155,8 +169,10 @@ var ENV_FALLBACK_SPECS = [
155
169
  { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
156
170
  { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
157
171
  { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
172
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
158
173
  { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
159
174
  { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
175
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
160
176
  { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
161
177
  { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
162
178
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
@@ -204,12 +220,16 @@ function applyOptionsToEnv(options) {
204
220
  if (options.environment) env[PIWI_ENV_KEYS.environment] = options.environment;
205
221
  if (options.label) env[PIWI_ENV_KEYS.label] = options.label;
206
222
  if (options.runLabel) env[PIWI_ENV_KEYS.runLabel] = options.runLabel;
223
+ if (options.keep) env[PIWI_ENV_KEYS.keep] = "true";
207
224
  if (options.captureLocators === false || options.collectPerformanceMetrics === false)
208
225
  env[PIWI_ENV_KEYS.captureLocators] = "false";
209
226
  else if (options.captureLocators === true) env[PIWI_ENV_KEYS.captureLocators] = "true";
210
227
  if (options.capturePageState === false || options.collectPerformanceMetrics === false)
211
228
  env[PIWI_ENV_KEYS.capturePageState] = "false";
212
229
  else if (options.capturePageState === true) env[PIWI_ENV_KEYS.capturePageState] = "true";
230
+ if (options.capturePageInventory === false || options.collectPerformanceMetrics === false)
231
+ env[PIWI_ENV_KEYS.capturePageInventory] = "false";
232
+ else if (options.capturePageInventory === true) env[PIWI_ENV_KEYS.capturePageInventory] = "true";
213
233
  if (options.captureServerTraces === false || options.collectPerformanceMetrics === false)
214
234
  env[PIWI_ENV_KEYS.captureServerTraces] = "false";
215
235
  else if (options.captureServerTraces === true) env[PIWI_ENV_KEYS.captureServerTraces] = "true";
@@ -398,6 +418,27 @@ var HttpClient = class {
398
418
  return {};
399
419
  }
400
420
  }
421
+ /** Send a JSON PUT request. Used for the declared-surface manifest upload. */
422
+ async putJSON(pathname, payload, auth) {
423
+ const body = JSON.stringify(payload);
424
+ const res = await this.request("PUT", pathname, {
425
+ headers: {
426
+ "Content-Type": "application/json",
427
+ "Content-Length": Buffer.byteLength(body)
428
+ },
429
+ body,
430
+ auth
431
+ });
432
+ if (res.status < 200 || res.status >= 300) {
433
+ this.logger.debugError(`Response: ${res.text}`);
434
+ throw new HttpError(res.status);
435
+ }
436
+ try {
437
+ return JSON.parse(res.text);
438
+ } catch {
439
+ return {};
440
+ }
441
+ }
401
442
  /** Send a multipart form-data POST request. Used for report and trace uploads. */
402
443
  async postFormData(pathname, form, auth) {
403
444
  const headers = form.getHeaders();
@@ -511,6 +552,7 @@ function toWireTestCase(tc) {
511
552
  networkRequests: rest.networkRequests || null,
512
553
  webVitals: rest.webVitals || null,
513
554
  pageState: rest.pageState || null,
555
+ pageInventory: rest.pageInventory || null,
514
556
  aiUsage: rest.aiUsage || null,
515
557
  consoleLogs: rest.consoleLogs || null,
516
558
  dialogs: rest.dialogs || null,
@@ -579,6 +621,9 @@ function serializeRun(payload, opts) {
579
621
  didNotRunTests: payload.didNotRunTests ?? 0,
580
622
  environment: payload.environment ?? null,
581
623
  label: payload.label ?? null,
624
+ keep: payload.keep === true,
625
+ // A probe run is stamped in `onBegin` (see the reporter), so the marker
626
+ // already rides in `payload.metadata` here and on every other submit path.
582
627
  metadata: payload.metadata,
583
628
  instanceId: payload.instanceId,
584
629
  playwrightVersion: payload.playwrightVersion,
@@ -950,6 +995,7 @@ var ATTACHMENT_NAMES = {
950
995
  console: "piwi-console",
951
996
  dialogs: "piwi-dialogs",
952
997
  network: "piwi-network",
998
+ pageInventory: "piwi-page-inventory",
953
999
  webVitals: "piwi-web-vitals",
954
1000
  locatorSuggestion: "piwi-locator-suggestion",
955
1001
  pageState: "piwi-page-state",
@@ -1152,6 +1198,13 @@ var FileHandler = class {
1152
1198
  } catch {
1153
1199
  }
1154
1200
  }
1201
+ const pageInventory = find(ATTACHMENT_NAMES.pageInventory);
1202
+ if (pageInventory?.body) {
1203
+ try {
1204
+ testCase.pageInventory = JSON.parse(pageInventory.body.toString());
1205
+ } catch {
1206
+ }
1207
+ }
1155
1208
  const aiUsage = find(ATTACHMENT_NAMES.aiUsage);
1156
1209
  if (aiUsage?.body) {
1157
1210
  try {
@@ -1253,7 +1306,7 @@ var MetadataCollector = class {
1253
1306
  }
1254
1307
  /** Collect all available metadata from the environment, config, and suite */
1255
1308
  collect(config, suite, options) {
1256
- const metadata = {};
1309
+ const metadata = { workingDir: process.cwd() };
1257
1310
  if (options.projectDescription) metadata.projectDescription = options.projectDescription;
1258
1311
  if (options.relatedIssue) metadata.relatedIssue = options.relatedIssue;
1259
1312
  if (options.ciInfo) metadata.ciInfo = options.ciInfo;
@@ -1329,7 +1382,7 @@ var MetadataCollector = class {
1329
1382
  /**
1330
1383
  * Walk the test's parent `describe` suites to extract the suite path (titles)
1331
1384
  * and per-level config (parallel mode + annotations). Reaches into Playwright
1332
- * suite internals (`_parallelMode`, `_annotations`) — kept here next to
1385
+ * suite internals (`_parallelMode`, `_staticAnnotations`) — kept here next to
1333
1386
  * `getBrowserConfig` so all such access is guarded behind one class.
1334
1387
  */
1335
1388
  getSuiteInfo(test) {
@@ -1346,7 +1399,10 @@ var MetadataCollector = class {
1346
1399
  suitePath.push(s.title);
1347
1400
  const rawMode = s._parallelMode;
1348
1401
  const mode = rawMode === "parallel" ? "parallel" : rawMode === "serial" ? "serial" : "default";
1349
- const annotations = s._annotations ?? [];
1402
+ const staticAnnotations = s._staticAnnotations ?? [];
1403
+ const annotations = staticAnnotations.map(
1404
+ (a) => a.description === void 0 ? { type: a.type } : { type: a.type, description: a.description }
1405
+ );
1350
1406
  suiteConfig.push({ mode, annotations });
1351
1407
  }
1352
1408
  return { suitePath, suiteConfig };
@@ -1441,6 +1497,11 @@ var MetadataCollector = class {
1441
1497
  testDir: p.testDir,
1442
1498
  use: {
1443
1499
  browserName: p.use?.browserName || p.name,
1500
+ // The origin of `baseURL` is what the graph treats as the run's own
1501
+ // surface, so only requests to it become route nodes.
1502
+ baseURL: p.use?.baseURL,
1503
+ // The attribute `getByTestId` reads, so the locator index can resolve those locators on a live page.
1504
+ testIdAttribute: p.use?.testIdAttribute,
1444
1505
  viewport: p.use?.viewport,
1445
1506
  deviceScaleFactor: p.use?.deviceScaleFactor
1446
1507
  }
@@ -1757,6 +1818,7 @@ var StreamManager = class {
1757
1818
  startTime,
1758
1819
  environment: this.options.environment || null,
1759
1820
  label: this.options.label || null,
1821
+ keep: this.options.keep === true,
1760
1822
  metadata,
1761
1823
  instanceId,
1762
1824
  playwrightVersion,
@@ -1779,6 +1841,7 @@ var StreamManager = class {
1779
1841
  startTime,
1780
1842
  environment: this.options.environment || null,
1781
1843
  label: this.options.label || null,
1844
+ keep: this.options.keep === true,
1782
1845
  metadata,
1783
1846
  instanceId,
1784
1847
  playwrightVersion,
@@ -2080,6 +2143,9 @@ function maskTokenLike(text) {
2080
2143
  // ../core/src/step-analysis.ts
2081
2144
  var MAX_STEP_PARAM_KEYS = 20;
2082
2145
  var MAX_STEP_PARAM_VALUE_CHARS = 200;
2146
+ function capStepValue(value, max) {
2147
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
2148
+ }
2083
2149
  function categorizeStep(title, pwCategory, params) {
2084
2150
  if (!title) return "other";
2085
2151
  if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
@@ -2110,11 +2176,11 @@ function normalizeStepParams(raw) {
2110
2176
  out[key] = value;
2111
2177
  count++;
2112
2178
  } else if (typeof value === "string") {
2113
- out[key] = maskTokenLike(value).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
2179
+ out[key] = capStepValue(maskTokenLike(value), MAX_STEP_PARAM_VALUE_CHARS);
2114
2180
  count++;
2115
2181
  } else if (value != null) {
2116
2182
  try {
2117
- out[key] = maskTokenLike(JSON.stringify(value)).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
2183
+ out[key] = capStepValue(maskTokenLike(JSON.stringify(value)), MAX_STEP_PARAM_VALUE_CHARS);
2118
2184
  count++;
2119
2185
  } catch {
2120
2186
  }
@@ -2437,8 +2503,8 @@ function isListMode(argv = process.argv) {
2437
2503
  }
2438
2504
 
2439
2505
  // src/public/global-setup.ts
2440
- var path11 = __toESM(require("path"));
2441
- var fs12 = __toESM(require("fs"));
2506
+ var path12 = __toESM(require("path"));
2507
+ var fs13 = __toESM(require("fs"));
2442
2508
 
2443
2509
  // src/internal/support/aria-sampling.ts
2444
2510
  var path10 = __toESM(require("path"));
@@ -2489,17 +2555,83 @@ function isDueForAriaSample(testInfo) {
2489
2555
  return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
2490
2556
  }
2491
2557
 
2558
+ // src/internal/manifest/collect.ts
2559
+ var fs12 = __toESM(require("fs"));
2560
+ var path11 = __toESM(require("path"));
2561
+ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
2562
+ var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
2563
+ var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
2564
+ function parseManifestJson(text) {
2565
+ let parsed;
2566
+ try {
2567
+ parsed = JSON.parse(text);
2568
+ } catch {
2569
+ return null;
2570
+ }
2571
+ if (!parsed || typeof parsed !== "object") return null;
2572
+ const obj = parsed;
2573
+ const routes = Array.isArray(obj.routes) ? obj.routes.filter((r) => !!r && typeof r === "object").filter((r) => typeof r.method === "string" && typeof r.pattern === "string").map((r) => ({
2574
+ method: r.method,
2575
+ pattern: r.pattern,
2576
+ handler: typeof r.handler === "string" ? r.handler : void 0,
2577
+ responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
2578
+ })) : void 0;
2579
+ const pages = Array.isArray(obj.pages) ? obj.pages.filter((p) => !!p && typeof p === "object").filter((p) => typeof p.pattern === "string").map((p) => ({ pattern: p.pattern, name: typeof p.name === "string" ? p.name : void 0 })) : void 0;
2580
+ if (!routes && !pages) return null;
2581
+ return { routes, pages };
2582
+ }
2583
+ function hasInstrumentationHeader(headers) {
2584
+ const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
2585
+ return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
2586
+ }
2587
+ function readCommittedManifest(configDir) {
2588
+ const file = path11.join(configDir, COMMITTED_MANIFEST_FILE);
2589
+ try {
2590
+ if (!fs12.existsSync(file)) return null;
2591
+ return parseManifestJson(fs12.readFileSync(file, "utf8"));
2592
+ } catch {
2593
+ return null;
2594
+ }
2595
+ }
2596
+ function configDirFromConfig(config) {
2597
+ if (config?.configFile) return path11.dirname(config.configFile);
2598
+ if (config?.rootDir) return config.rootDir;
2599
+ return process.cwd();
2600
+ }
2601
+ function baseUrlFromConfig(config) {
2602
+ const top = config?.use?.baseURL;
2603
+ if (typeof top === "string" && top) return top;
2604
+ for (const project of config?.projects ?? []) {
2605
+ const url = project?.use?.baseURL;
2606
+ if (typeof url === "string" && url) return url;
2607
+ }
2608
+ return null;
2609
+ }
2610
+ var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
2611
+ async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
2612
+ try {
2613
+ const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
2614
+ if (!hasInstrumentationHeader(probe.headers)) return null;
2615
+ const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
2616
+ const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
2617
+ if (!res.ok) return null;
2618
+ return parseManifestJson(await res.text());
2619
+ } catch {
2620
+ return null;
2621
+ }
2622
+ }
2623
+
2492
2624
  // src/public/global-setup.ts
2493
2625
  function createGlobalSetup(options, userSetup) {
2494
2626
  return async function globalSetupFn(config) {
2495
- const piwiReporterPath = path11.resolve(__dirname, "./index.js");
2627
+ const piwiReporterPath = path12.resolve(__dirname, "./index.js");
2496
2628
  let inlineReporterOptions = {};
2497
2629
  if (Array.isArray(config?.reporter)) {
2498
2630
  for (const r of config.reporter) {
2499
2631
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
2500
2632
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
2501
2633
  try {
2502
- return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
2634
+ return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
2503
2635
  } catch {
2504
2636
  return false;
2505
2637
  }
@@ -2531,7 +2663,7 @@ function createGlobalSetup(options, userSetup) {
2531
2663
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
2532
2664
  if (r[0].toLowerCase().includes("piwi")) return true;
2533
2665
  try {
2534
- return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
2666
+ return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
2535
2667
  } catch {
2536
2668
  return false;
2537
2669
  }
@@ -2555,6 +2687,7 @@ function createGlobalSetup(options, userSetup) {
2555
2687
  projectDescription: opts.projectDescription,
2556
2688
  environment: opts.environment || null,
2557
2689
  label: opts.label || null,
2690
+ keep: opts.keep === true,
2558
2691
  startTime: (/* @__PURE__ */ new Date()).toISOString(),
2559
2692
  instanceId: computeInstanceId(opts.projectName, runLabel),
2560
2693
  shardIndex,
@@ -2563,7 +2696,7 @@ function createGlobalSetup(options, userSetup) {
2563
2696
  auth
2564
2697
  );
2565
2698
  if (response?.runId && response?.setupToken) {
2566
- fs12.writeFileSync(
2699
+ fs13.writeFileSync(
2567
2700
  getSetupFilePath(opts.projectName),
2568
2701
  JSON.stringify({
2569
2702
  runId: response.runId,
@@ -2574,18 +2707,43 @@ function createGlobalSetup(options, userSetup) {
2574
2707
  logger.debug(`Global setup: initializing run #${response.runId}`);
2575
2708
  }
2576
2709
  if (opts.projectName) clearAriaSampleFile(opts.projectName);
2577
- if (opts.sampleAriaOnPass !== false && opts.projectName) {
2710
+ const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
2711
+ const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
2712
+ const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
2713
+ const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
2714
+ let projectId;
2715
+ if ((wantsAria || wantsManifest) && opts.projectName) {
2578
2716
  const menu = await httpClient.getJSON("/api/projects/menu", auth);
2579
- const projectId = menu?.items?.find(
2717
+ projectId = menu?.items?.find(
2580
2718
  (p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
2581
2719
  )?.id;
2582
- if (projectId != null) {
2583
- const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
2584
- const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
2585
- if (tests) {
2586
- const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
2587
- writeAriaSampleFile(opts.projectName, identities);
2588
- logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
2720
+ }
2721
+ if (wantsAria && projectId != null) {
2722
+ const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
2723
+ const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
2724
+ if (tests) {
2725
+ const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
2726
+ writeAriaSampleFile(opts.projectName, identities);
2727
+ logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
2728
+ }
2729
+ }
2730
+ if (wantsManifest && projectId != null) {
2731
+ const branch = resolveScmBranch(process.env) ?? null;
2732
+ if (committedManifest) {
2733
+ await httpClient.putJSON(
2734
+ `/api/projects/${projectId}/surface/manifest`,
2735
+ { source: "committed", branch, manifest: committedManifest },
2736
+ auth
2737
+ ).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
2738
+ }
2739
+ if (manifestBaseUrl) {
2740
+ const declared = await fetchInstrumentationManifest(manifestBaseUrl);
2741
+ if (declared) {
2742
+ await httpClient.putJSON(
2743
+ `/api/projects/${projectId}/surface/manifest`,
2744
+ { source: "instrumentation", branch, manifest: declared },
2745
+ auth
2746
+ ).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
2589
2747
  }
2590
2748
  }
2591
2749
  }
@@ -2597,8 +2755,110 @@ function createGlobalSetup(options, userSetup) {
2597
2755
  }
2598
2756
 
2599
2757
  // src/public/config-wrapper.ts
2600
- var path12 = __toESM(require("path"));
2601
- var fs13 = __toESM(require("fs"));
2758
+ var path14 = __toESM(require("path"));
2759
+ var fs15 = __toESM(require("fs"));
2760
+
2761
+ // src/internal/probe/mode.ts
2762
+ var fs14 = __toESM(require("fs"));
2763
+ var path13 = __toESM(require("path"));
2764
+
2765
+ // src/internal/probe/plan.ts
2766
+ function probeRunMetadata(base = {}) {
2767
+ return { ...base, piwiProbe: true };
2768
+ }
2769
+ function parseRouteKey(routeKey) {
2770
+ const space = routeKey.indexOf(" ");
2771
+ if (space < 0) return { method: "", pattern: routeKey };
2772
+ return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
2773
+ }
2774
+ function patternToRegex(pattern) {
2775
+ const path23 = pattern.split("?")[0] ?? pattern;
2776
+ const escaped = path23.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2777
+ return new RegExp(`^${escaped}$`);
2778
+ }
2779
+ function suitePathEqual(a, b) {
2780
+ if (!a || !b || a.length !== b.length) return false;
2781
+ return a.every((title, i) => title === b[i]);
2782
+ }
2783
+ function matchProbeItem(plan, test) {
2784
+ const byFileTitle = plan.items.filter(
2785
+ (item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
2786
+ );
2787
+ if (byFileTitle.length === 1) return byFileTitle[0];
2788
+ if (byFileTitle.length > 1) {
2789
+ return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
2790
+ }
2791
+ for (const item of plan.items) {
2792
+ if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
2793
+ }
2794
+ return null;
2795
+ }
2796
+ function requestMatchesRoute(method, url, routeKey) {
2797
+ const { method: rm, pattern } = parseRouteKey(routeKey);
2798
+ if (rm && rm !== method.toUpperCase()) return false;
2799
+ let path23;
2800
+ try {
2801
+ path23 = new URL(url).pathname;
2802
+ } catch {
2803
+ path23 = url.split("?")[0] ?? url;
2804
+ }
2805
+ return patternToRegex(pattern).test(path23);
2806
+ }
2807
+ function createProbeState() {
2808
+ return { navigated: false, matches: 0 };
2809
+ }
2810
+ function markNavigated(state) {
2811
+ state.navigated = true;
2812
+ }
2813
+ function shouldMutate(state, item) {
2814
+ if (!state.navigated) return false;
2815
+ state.matches += 1;
2816
+ return state.matches === Math.max(1, item.nth ?? 1);
2817
+ }
2818
+
2819
+ // src/internal/probe/mode.ts
2820
+ var planCache;
2821
+ function isProbeMode() {
2822
+ return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
2823
+ }
2824
+ function loadPlan() {
2825
+ if (planCache !== void 0) return planCache;
2826
+ planCache = null;
2827
+ const path23 = process.env[PIWI_PROBE_ENV.plan];
2828
+ if (path23) {
2829
+ try {
2830
+ planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
2831
+ } catch {
2832
+ planCache = null;
2833
+ }
2834
+ }
2835
+ return planCache;
2836
+ }
2837
+ function probeItemForTest(test) {
2838
+ const plan = loadPlan();
2839
+ if (!plan) return null;
2840
+ const filePath = test.file ? path13.relative(process.cwd(), test.file).split(path13.sep).join("/") : null;
2841
+ const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2842
+ return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2843
+ }
2844
+ function classifyProbeHandled(signals) {
2845
+ return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
2846
+ }
2847
+ function outcomeFromStatus(status, applied) {
2848
+ if (!applied) return "inconclusive";
2849
+ return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2850
+ }
2851
+ function recordProbeOutcome(line) {
2852
+ const path23 = process.env[PIWI_PROBE_ENV.results];
2853
+ if (!path23) return;
2854
+ try {
2855
+ fs14.appendFileSync(path23, `${JSON.stringify(line)}
2856
+ `);
2857
+ } catch {
2858
+ }
2859
+ }
2860
+
2861
+ // src/public/config-wrapper.ts
2602
2862
  var PIWI_MODULE = "@piwitests/reporter";
2603
2863
  var CAPTURE_DEFAULTS = {
2604
2864
  screenshot: "only-on-failure",
@@ -2663,11 +2923,11 @@ function injectReporter(reporter, piwiOptions) {
2663
2923
  }
2664
2924
  function resolveSetupModule() {
2665
2925
  const candidates = [
2666
- path12.join(__dirname, "global-setup-module.js"),
2667
- path12.join(__dirname, "..", "global-setup-module.js"),
2668
- path12.join(__dirname, "..", "global-setup-module.ts")
2926
+ path14.join(__dirname, "global-setup-module.js"),
2927
+ path14.join(__dirname, "..", "global-setup-module.js"),
2928
+ path14.join(__dirname, "..", "global-setup-module.ts")
2669
2929
  ];
2670
- return candidates.find((candidate) => fs13.existsSync(candidate)) ?? candidates[0];
2930
+ return candidates.find((candidate) => fs15.existsSync(candidate)) ?? candidates[0];
2671
2931
  }
2672
2932
  function wrapConfig(config, piwiOptions) {
2673
2933
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -2679,6 +2939,7 @@ function wrapConfig(config, piwiOptions) {
2679
2939
  const forwarded = {};
2680
2940
  const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
2681
2941
  if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
2942
+ if (isProbeMode()) forwarded.retries = 0;
2682
2943
  const use = applyCaptureDefaults(config.use, piwiOptions);
2683
2944
  return {
2684
2945
  ...config,
@@ -2867,7 +3128,7 @@ function collectTestMetadata(annotations) {
2867
3128
  }
2868
3129
 
2869
3130
  // src/internal/collect/error-text.ts
2870
- var path13 = __toESM(require("path"));
3131
+ var path15 = __toESM(require("path"));
2871
3132
 
2872
3133
  // ../core/src/error-text.ts
2873
3134
  function joinErrorMessages(errors) {
@@ -2896,13 +3157,13 @@ function buildErrorText(result) {
2896
3157
  if (!text) return null;
2897
3158
  const loc = result.error?.location;
2898
3159
  if (!loc?.file) return text;
2899
- const rel = path13.relative(process.cwd(), loc.file).split(path13.sep).join("/");
3160
+ const rel = path15.relative(process.cwd(), loc.file).split(path15.sep).join("/");
2900
3161
  return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
2901
3162
  }
2902
3163
 
2903
3164
  // src/internal/support/ci-output.ts
2904
- var fs14 = __toESM(require("fs"));
2905
- var path14 = __toESM(require("path"));
3165
+ var fs16 = __toESM(require("fs"));
3166
+ var path16 = __toESM(require("path"));
2906
3167
  var SUMMARY_MAX_FAILURES = 20;
2907
3168
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
2908
3169
  logger.info(`View run: ${output.runUrl}`);
@@ -2912,9 +3173,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
2912
3173
  }
2913
3174
  function writeOutputFile(file, output, logger) {
2914
3175
  try {
2915
- const dir = path14.dirname(file);
2916
- if (dir && dir !== ".") fs14.mkdirSync(dir, { recursive: true });
2917
- fs14.writeFileSync(
3176
+ const dir = path16.dirname(file);
3177
+ if (dir && dir !== ".") fs16.mkdirSync(dir, { recursive: true });
3178
+ fs16.writeFileSync(
2918
3179
  file,
2919
3180
  JSON.stringify(
2920
3181
  {
@@ -2994,7 +3255,7 @@ function emitGitLabDotenv(output, env, logger) {
2994
3255
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
2995
3256
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
2996
3257
  try {
2997
- fs14.writeFileSync(file, lines.join("\n") + "\n");
3258
+ fs16.writeFileSync(file, lines.join("\n") + "\n");
2998
3259
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
2999
3260
  } catch (error) {
3000
3261
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -3002,7 +3263,7 @@ function emitGitLabDotenv(output, env, logger) {
3002
3263
  }
3003
3264
  function appendFileLines(file, lines, logger, label) {
3004
3265
  try {
3005
- fs14.appendFileSync(file, lines.join("\n") + "\n");
3266
+ fs16.appendFileSync(file, lines.join("\n") + "\n");
3006
3267
  } catch (error) {
3007
3268
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
3008
3269
  }
@@ -3121,6 +3382,7 @@ var RunSubmitter = class {
3121
3382
  didNotRunTests: run.didNotRunTests,
3122
3383
  environment: run.options.environment,
3123
3384
  label: run.options.label || null,
3385
+ keep: run.options.keep === true,
3124
3386
  metadata: run.metadata,
3125
3387
  instanceId: run.instanceId,
3126
3388
  playwrightVersion: run.playwrightVersion ?? void 0,
@@ -3151,6 +3413,7 @@ var RunSubmitter = class {
3151
3413
  flakyTests,
3152
3414
  durations,
3153
3415
  label: run.options.label || null,
3416
+ keep: run.options.keep === true,
3154
3417
  metadata: run.metadata,
3155
3418
  hasPendingUploads: this.hasReports(run),
3156
3419
  playwrightVersion: run.playwrightVersion ?? void 0,
@@ -3238,6 +3501,298 @@ var RunSubmitter = class {
3238
3501
  }
3239
3502
  };
3240
3503
 
3504
+ // ../core/src/locator-chain.ts
3505
+ var LOCATING_METHODS = /* @__PURE__ */ new Set([
3506
+ "getByRole",
3507
+ "getByTestId",
3508
+ "getByText",
3509
+ "getByLabel",
3510
+ "getByPlaceholder",
3511
+ "getByAltText",
3512
+ "getByTitle",
3513
+ "locator",
3514
+ "frameLocator"
3515
+ ]);
3516
+ var NARROWING_METHODS = /* @__PURE__ */ new Set([
3517
+ "filter",
3518
+ "first",
3519
+ "last",
3520
+ "nth",
3521
+ "and",
3522
+ "or",
3523
+ "visible",
3524
+ "contentFrame",
3525
+ "owner"
3526
+ ]);
3527
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
3528
+ var LocatorParseError = class extends Error {
3529
+ constructor(message) {
3530
+ super(message);
3531
+ this.name = "LocatorParseError";
3532
+ }
3533
+ };
3534
+ var SIMPLE_ESCAPES = {
3535
+ n: "\n",
3536
+ t: " ",
3537
+ r: "\r",
3538
+ b: "\b",
3539
+ f: "\f",
3540
+ v: "\v",
3541
+ "0": "\0"
3542
+ };
3543
+ var Parser = class {
3544
+ constructor(src) {
3545
+ this.src = src;
3546
+ this.i = 0;
3547
+ }
3548
+ parseTopLevel() {
3549
+ const chain = this.parseChain();
3550
+ this.skipSpace();
3551
+ if (this.i < this.src.length) this.fail("unexpected text");
3552
+ return chain;
3553
+ }
3554
+ fail(message) {
3555
+ throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
3556
+ }
3557
+ skipSpace() {
3558
+ while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
3559
+ }
3560
+ peek() {
3561
+ return this.src[this.i];
3562
+ }
3563
+ expect(ch) {
3564
+ this.skipSpace();
3565
+ if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
3566
+ this.i++;
3567
+ }
3568
+ identifier() {
3569
+ const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
3570
+ if (!m) this.fail("expected a name");
3571
+ this.i += m[0].length;
3572
+ return m[0];
3573
+ }
3574
+ /**
3575
+ * A chain of calls joined by `.`. With `lenient`, parsing stops before the
3576
+ * first link that doesn't parse instead of failing, so a chain followed by
3577
+ * other text (an error message) is read up to where it ends. `spans` receives
3578
+ * where each top-level call starts and ends.
3579
+ */
3580
+ parseChain(opts = {}) {
3581
+ this.skipSpace();
3582
+ const calls = [];
3583
+ for (; ; ) {
3584
+ const before = this.i;
3585
+ if (calls.length > 0) this.i++;
3586
+ this.skipSpace();
3587
+ const start = this.i;
3588
+ try {
3589
+ calls.push(this.parseCall(calls.length === 0));
3590
+ } catch (error) {
3591
+ if (!opts.lenient || calls.length === 0) throw error;
3592
+ this.i = before;
3593
+ break;
3594
+ }
3595
+ opts.spans?.push({ start, end: this.i });
3596
+ const end = this.i;
3597
+ this.skipSpace();
3598
+ if (this.peek() !== ".") {
3599
+ this.i = end;
3600
+ break;
3601
+ }
3602
+ }
3603
+ return { calls };
3604
+ }
3605
+ /** Where the parser stopped. */
3606
+ get position() {
3607
+ return this.i;
3608
+ }
3609
+ parseCall(first) {
3610
+ this.skipSpace();
3611
+ const method = this.identifier();
3612
+ if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
3613
+ if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
3614
+ this.expect("(");
3615
+ const args = [];
3616
+ this.skipSpace();
3617
+ if (this.peek() === ")") {
3618
+ this.i++;
3619
+ return { method, args };
3620
+ }
3621
+ for (; ; ) {
3622
+ args.push(this.parseValue());
3623
+ this.skipSpace();
3624
+ if (this.peek() === ",") {
3625
+ this.i++;
3626
+ this.skipSpace();
3627
+ if (this.peek() === ")") {
3628
+ this.i++;
3629
+ break;
3630
+ }
3631
+ continue;
3632
+ }
3633
+ this.expect(")");
3634
+ break;
3635
+ }
3636
+ return { method, args };
3637
+ }
3638
+ parseValue() {
3639
+ this.skipSpace();
3640
+ const ch = this.peek();
3641
+ if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
3642
+ if (ch === "/") return this.parseRegex();
3643
+ if (ch === "{") return this.parseObject();
3644
+ if (ch !== void 0 && /[-\d]/.test(ch)) {
3645
+ const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
3646
+ if (!m) this.fail("expected a number");
3647
+ this.i += m[0].length;
3648
+ return { type: "number", value: Number(m[0]) };
3649
+ }
3650
+ const rest = this.src.slice(this.i);
3651
+ if (/^true\b/.test(rest)) {
3652
+ this.i += 4;
3653
+ return { type: "boolean", value: true };
3654
+ }
3655
+ if (/^false\b/.test(rest)) {
3656
+ this.i += 5;
3657
+ return { type: "boolean", value: false };
3658
+ }
3659
+ if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
3660
+ return this.fail("expected a value");
3661
+ }
3662
+ /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
3663
+ parseString() {
3664
+ const quote = this.src[this.i];
3665
+ let out = "";
3666
+ this.i++;
3667
+ while (this.i < this.src.length) {
3668
+ const c = this.src[this.i];
3669
+ if (c === quote) {
3670
+ this.i++;
3671
+ return out;
3672
+ }
3673
+ if (c !== "\\") {
3674
+ out += c;
3675
+ this.i++;
3676
+ continue;
3677
+ }
3678
+ const next = this.src[this.i + 1];
3679
+ if (next === void 0) break;
3680
+ const simple = SIMPLE_ESCAPES[next];
3681
+ if (simple !== void 0) {
3682
+ out += simple;
3683
+ this.i += 2;
3684
+ continue;
3685
+ }
3686
+ 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;
3687
+ if (hex) {
3688
+ out += String.fromCodePoint(parseInt(hex[1], 16));
3689
+ this.i += 1 + hex[0].length;
3690
+ continue;
3691
+ }
3692
+ if (next === "u" || next === "x") this.fail("malformed escape");
3693
+ out += next;
3694
+ this.i += 2;
3695
+ }
3696
+ return this.fail("unterminated string");
3697
+ }
3698
+ parseRegex() {
3699
+ let j = this.i + 1;
3700
+ let inClass = false;
3701
+ while (j < this.src.length) {
3702
+ const c = this.src[j];
3703
+ if (c === "\\") {
3704
+ j += 2;
3705
+ continue;
3706
+ }
3707
+ if (c === "[") inClass = true;
3708
+ else if (c === "]") inClass = false;
3709
+ else if (c === "/" && !inClass) break;
3710
+ j++;
3711
+ }
3712
+ if (j >= this.src.length) this.fail("unterminated regex");
3713
+ const source = this.src.slice(this.i + 1, j);
3714
+ const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
3715
+ this.i = j + 1 + flags.length;
3716
+ return { type: "regex", source, flags };
3717
+ }
3718
+ parseObject() {
3719
+ this.expect("{");
3720
+ const entries = [];
3721
+ for (; ; ) {
3722
+ this.skipSpace();
3723
+ if (this.peek() === "}") {
3724
+ this.i++;
3725
+ break;
3726
+ }
3727
+ const ch = this.peek();
3728
+ const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
3729
+ this.expect(":");
3730
+ entries.push([key, this.parseValue()]);
3731
+ this.skipSpace();
3732
+ if (this.peek() === ",") {
3733
+ this.i++;
3734
+ continue;
3735
+ }
3736
+ this.expect("}");
3737
+ break;
3738
+ }
3739
+ return { type: "object", entries };
3740
+ }
3741
+ };
3742
+ function parseLocatorChain(expr) {
3743
+ const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
3744
+ if (!trimmed) throw new LocatorParseError("empty expression");
3745
+ return new Parser(trimmed).parseTopLevel();
3746
+ }
3747
+ function scanLocatorChain(text) {
3748
+ const parser = new Parser(text);
3749
+ const spans = [];
3750
+ try {
3751
+ const chain = parser.parseChain({ lenient: true, spans });
3752
+ return { chain, spans, end: parser.position };
3753
+ } catch {
3754
+ return null;
3755
+ }
3756
+ }
3757
+ function tryParseLocatorChain(expr) {
3758
+ try {
3759
+ return parseLocatorChain(expr);
3760
+ } catch {
3761
+ return null;
3762
+ }
3763
+ }
3764
+ function parseLeafLocatorCall(expr) {
3765
+ const chain = tryParseLocatorChain(expr);
3766
+ if (!chain) return null;
3767
+ for (let i = chain.calls.length - 1; i >= 0; i--) {
3768
+ if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
3769
+ }
3770
+ return null;
3771
+ }
3772
+ function locatorCallValues(call, opts = {}) {
3773
+ const value = (arg) => {
3774
+ switch (arg.type) {
3775
+ case "string":
3776
+ case "number":
3777
+ case "boolean":
3778
+ return arg.value;
3779
+ case "regex":
3780
+ return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
3781
+ case "object": {
3782
+ const out = {};
3783
+ for (const [key, v] of arg.entries) {
3784
+ const plain = value(v);
3785
+ if (plain !== void 0) out[key] = plain;
3786
+ }
3787
+ return out;
3788
+ }
3789
+ case "chain":
3790
+ return void 0;
3791
+ }
3792
+ };
3793
+ return call.args.map(value).filter((v) => v !== void 0);
3794
+ }
3795
+
3241
3796
  // ../core/src/locator-methods.ts
3242
3797
  var LOCATOR_BUILDER_METHODS = [
3243
3798
  "getByRole",
@@ -3282,10 +3837,35 @@ function extractLeafSelector(text) {
3282
3837
  if (!first) return null;
3283
3838
  const nl = text.indexOf("\n", first.index);
3284
3839
  const region = text.slice(first.index, nl === -1 ? void 0 : nl);
3840
+ const scanned = readChain(region);
3841
+ if (scanned) {
3842
+ for (let k = scanned.chain.calls.length - 1; k >= 0; k--) {
3843
+ if (LEAF_METHODS.has(scanned.chain.calls[k].method)) {
3844
+ return region.slice(scanned.spans[k].start, scanned.spans[k].end);
3845
+ }
3846
+ }
3847
+ return null;
3848
+ }
3849
+ return lenientLeaf(region);
3850
+ }
3851
+ var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
3852
+ function skipQuoted(text, i) {
3853
+ const quote = text[i];
3854
+ for (let j = i + 1; j < text.length; j++) {
3855
+ if (text[j] === "\\") j++;
3856
+ else if (text[j] === quote) return j + 1;
3857
+ }
3858
+ return text.length;
3859
+ }
3860
+ function lenientLeaf(region) {
3285
3861
  let depth = 0;
3286
3862
  let leafStart = -1;
3287
3863
  for (let i = 0; i < region.length; i++) {
3288
3864
  const ch = region[i];
3865
+ if (ch === "'" || ch === '"' || ch === "`") {
3866
+ i = skipQuoted(region, i) - 1;
3867
+ continue;
3868
+ }
3289
3869
  if (ch === "(") {
3290
3870
  depth++;
3291
3871
  continue;
@@ -3308,7 +3888,9 @@ function extractLeafSelector(text) {
3308
3888
  depth = 0;
3309
3889
  for (let i = leafStart; i < region.length; i++) {
3310
3890
  const ch = region[i];
3311
- if (ch === "(") {
3891
+ if (ch === "'" || ch === '"' || ch === "`") {
3892
+ i = skipQuoted(region, i) - 1;
3893
+ } else if (ch === "(") {
3312
3894
  depth++;
3313
3895
  } else if (ch === ")") {
3314
3896
  depth--;
@@ -3322,11 +3904,15 @@ function extractLocatorChain(text) {
3322
3904
  if (!first) return null;
3323
3905
  const nl = text.indexOf("\n", first.index);
3324
3906
  const region = text.slice(first.index, nl === -1 ? void 0 : nl);
3907
+ const scanned = readChain(region);
3908
+ if (scanned) return region.slice(0, scanned.end);
3325
3909
  let depth = 0;
3326
3910
  let end = -1;
3327
3911
  for (let i = 0; i < region.length; i++) {
3328
3912
  const ch = region[i];
3329
- if (ch === "(") {
3913
+ if (ch === "'" || ch === '"' || ch === "`") {
3914
+ i = skipQuoted(region, i) - 1;
3915
+ } else if (ch === "(") {
3330
3916
  depth++;
3331
3917
  } else if (ch === ")") {
3332
3918
  if (depth > 0) depth--;
@@ -3341,6 +3927,12 @@ function extractLocatorChain(text) {
3341
3927
  if (end === -1) return region.slice(0, 80);
3342
3928
  return region.slice(0, end);
3343
3929
  }
3930
+ function readChain(region) {
3931
+ const scanned = scanLocatorChain(region);
3932
+ if (!scanned) return null;
3933
+ const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
3934
+ return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
3935
+ }
3344
3936
  function extractTopFrame(text) {
3345
3937
  const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
3346
3938
  let m;
@@ -4064,7 +4656,7 @@ function testLocation(test) {
4064
4656
  return `${testFile(test)}:${test.location.line}:${test.location.column}`;
4065
4657
  }
4066
4658
  function testFile(test) {
4067
- return path15.relative(process.cwd(), test.location.file).split(path15.sep).join("/");
4659
+ return path17.relative(process.cwd(), test.location.file).split(path17.sep).join("/");
4068
4660
  }
4069
4661
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
4070
4662
  constructor(rawOptions = {}) {
@@ -4181,6 +4773,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4181
4773
  );
4182
4774
  }
4183
4775
  this.metadata = this.metadataCollector.collect(config, suite, this.options);
4776
+ if (isProbeMode()) this.metadata = probeRunMetadata(this.metadata);
4184
4777
  this.plannedTests = suite.allTests();
4185
4778
  const pwShard = config.shard;
4186
4779
  if (pwShard?.total && pwShard.total > 1) {
@@ -4219,18 +4812,27 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4219
4812
  }
4220
4813
  }
4221
4814
  static {
4222
- /**
4223
- * Step categories streamed live while the run executes. `pw:assert` is
4224
- * excluded: it is the polling noise of `expect()`, not a step a human
4225
- * watches; the meaningful readout is the `pw:expect` wrapper around it.
4226
- */
4227
- this.LIVE_STEP_CATEGORIES = /* @__PURE__ */ new Set(["hook", "fixture", "pw:api", "pw:expect"]);
4815
+ /** Step categories streamed live while the run executes. */
4816
+ this.LIVE_STEP_CATEGORIES = /* @__PURE__ */ new Set(["hook", "fixture", "pw:api", "expect"]);
4817
+ }
4818
+ /**
4819
+ * Whether a step streams live: its category is in `LIVE_STEP_CATEGORIES`
4820
+ * and no ancestor is an `expect` step. The steps inside an assertion are its
4821
+ * polling — every `expect.poll` attempt and `toPass` retry — not a step a
4822
+ * human watches; the meaningful readout is the assertion around them.
4823
+ */
4824
+ static isLiveStep(step) {
4825
+ if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(step.category)) return false;
4826
+ for (let parent = step.parent; parent; parent = parent.parent) {
4827
+ if (parent.category === "expect") return false;
4828
+ }
4829
+ return true;
4228
4830
  }
4229
4831
  /** Playwright reporter hook: called when a step (including hook/fixture) begins */
4230
4832
  onStepBegin(test, _result, step) {
4231
4833
  if (!this.enabled || !this.streamManager) return;
4834
+ if (!_PiwiDashboardReporter.isLiveStep(step)) return;
4232
4835
  const cat = step.category;
4233
- if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
4234
4836
  const event = {
4235
4837
  type: "step-begin",
4236
4838
  title: step.title,
@@ -4246,8 +4848,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4246
4848
  /** Playwright reporter hook: called when a step (including hook/fixture) ends */
4247
4849
  onStepEnd(test, _result, step) {
4248
4850
  if (!this.enabled || !this.streamManager) return;
4851
+ if (!_PiwiDashboardReporter.isLiveStep(step)) return;
4249
4852
  const cat = step.category;
4250
- if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
4251
4853
  const workerIndex = workerIndexOf(_result);
4252
4854
  const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
4253
4855
  const event = {
@@ -4537,7 +5139,7 @@ async function resolveSelection(options = {}) {
4537
5139
  }
4538
5140
 
4539
5141
  // src/internal/capture/capture-fixtures.ts
4540
- var import_node_zlib = require("zlib");
5142
+ var import_node_zlib2 = require("zlib");
4541
5143
 
4542
5144
  // ../picker-dom/src/probe.ts
4543
5145
  function probeElementAttrs(el, arg) {
@@ -5895,7 +6497,7 @@ function mergeCandidates(base, extra) {
5895
6497
  }
5896
6498
 
5897
6499
  // src/internal/capture/locator-healing.ts
5898
- var path16 = __toESM(require("path"));
6500
+ var path18 = __toESM(require("path"));
5899
6501
 
5900
6502
  // ../core/src/locator-fingerprint.ts
5901
6503
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -6265,16 +6867,318 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
6265
6867
  }
6266
6868
  let rel = file;
6267
6869
  try {
6268
- rel = path16.relative(process.cwd(), file);
6870
+ rel = path18.relative(process.cwd(), file);
6269
6871
  } catch {
6270
6872
  }
6271
- rel = rel.split(path16.sep).join("/");
6873
+ rel = rel.split(path18.sep).join("/");
6272
6874
  if (rel.startsWith("./")) rel = rel.slice(2);
6273
6875
  return `${rel}:${m[3]}:${m[4]}`;
6274
6876
  }
6275
6877
  return null;
6276
6878
  }
6277
6879
 
6880
+ // src/internal/capture/page-inventory.ts
6881
+ var PAGE_INVENTORY_MAX_ENTRIES = 500;
6882
+ function collectPageInventoryInPage(maxEntries) {
6883
+ const g = globalThis;
6884
+ const doc = g.document;
6885
+ if (!doc || !g.location) return null;
6886
+ const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
6887
+ const roleOf = (el) => {
6888
+ const explicit = el.getAttribute("role");
6889
+ if (explicit) return explicit.toLowerCase();
6890
+ const tag = (el.tagName || "").toLowerCase();
6891
+ if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
6892
+ if (tag === "button") return "button";
6893
+ if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
6894
+ if (tag === "textarea") return "textbox";
6895
+ if (tag === "input") {
6896
+ const type = (el.getAttribute("type") || "text").toLowerCase();
6897
+ const map = {
6898
+ checkbox: "checkbox",
6899
+ radio: "radio",
6900
+ button: "button",
6901
+ submit: "button",
6902
+ reset: "button",
6903
+ image: "button",
6904
+ range: "slider",
6905
+ search: "searchbox",
6906
+ email: "textbox",
6907
+ tel: "textbox",
6908
+ url: "textbox",
6909
+ number: "spinbutton"
6910
+ };
6911
+ return map[type] || "textbox";
6912
+ }
6913
+ return "generic";
6914
+ };
6915
+ const holdsValue = (el, role) => {
6916
+ const tag = (el.tagName || "").toLowerCase();
6917
+ if (tag === "select" || tag === "textarea" || tag === "input") return true;
6918
+ if (el.isContentEditable === true) return true;
6919
+ const editable = el.getAttribute("contenteditable");
6920
+ if (editable === "" || editable === "true") return true;
6921
+ return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
6922
+ };
6923
+ const labelText = (el) => {
6924
+ const labels = el.labels;
6925
+ if (!labels || labels.length === 0) return "";
6926
+ let text = "";
6927
+ for (const label of labels) text += ` ${label.textContent || ""}`;
6928
+ return text;
6929
+ };
6930
+ const labelledByText = (el) => {
6931
+ const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
6932
+ if (ids.length === 0) return "";
6933
+ let text = "";
6934
+ for (const id of ids) {
6935
+ const ref = doc.getElementById(id);
6936
+ if (ref) text += ` ${ref.textContent || ""}`;
6937
+ }
6938
+ return text;
6939
+ };
6940
+ const nameOf = (el) => {
6941
+ const role = roleOf(el);
6942
+ const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
6943
+ if (!holdsValue(el, role)) chain.push(el.textContent);
6944
+ chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
6945
+ return clean(chain.find((s) => s && String(s).trim()) || "");
6946
+ };
6947
+ const cleanHref = (href) => {
6948
+ const cut = href.search(/[?#]/);
6949
+ return cut >= 0 ? href.slice(0, cut) : href;
6950
+ };
6951
+ const controls = [];
6952
+ const links = [];
6953
+ const seenC = /* @__PURE__ */ new Set();
6954
+ const seenL = /* @__PURE__ */ new Set();
6955
+ let budget = maxEntries;
6956
+ const controlSelector = 'button, [role="button"], input, select, textarea, [role="checkbox"], [role="radio"], [role="tab"], [role="menuitem"], [role="combobox"], [role="switch"], [role="slider"], [role="searchbox"], [role="spinbutton"], [role="textbox"], [role="listbox"]';
6957
+ for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
6958
+ if (budget <= 0) break;
6959
+ const role = roleOf(el);
6960
+ const name = nameOf(el);
6961
+ if (!name) continue;
6962
+ const key = `${role}\0${name}`;
6963
+ if (seenC.has(key)) continue;
6964
+ seenC.add(key);
6965
+ controls.push({ role, name });
6966
+ budget--;
6967
+ }
6968
+ for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
6969
+ if (budget <= 0) break;
6970
+ const name = nameOf(el);
6971
+ const href = cleanHref(el.getAttribute("href") || "");
6972
+ if (!name) continue;
6973
+ const key = `${name}\0${href}`;
6974
+ if (seenL.has(key)) continue;
6975
+ seenL.add(key);
6976
+ links.push({ name, href });
6977
+ budget--;
6978
+ }
6979
+ return { url: g.location.href, controls, links };
6980
+ }
6981
+ function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
6982
+ const controls = inventory.controls.slice(0, Math.max(0, max));
6983
+ const links = inventory.links.slice(0, Math.max(0, max - controls.length));
6984
+ return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
6985
+ }
6986
+ function inventoryPageKey(url) {
6987
+ try {
6988
+ const u = new URL(url);
6989
+ return `${u.origin}${u.pathname}`;
6990
+ } catch {
6991
+ return null;
6992
+ }
6993
+ }
6994
+
6995
+ // src/internal/probe/interception.ts
6996
+ var import_node_zlib = require("zlib");
6997
+
6998
+ // src/internal/probe/faults.ts
6999
+ var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
7000
+ var SERVER_PROBE_FAULTS = [
7001
+ "throw",
7002
+ "status",
7003
+ "delay",
7004
+ "dependency",
7005
+ "data",
7006
+ "extreme",
7007
+ "replay",
7008
+ "auth",
7009
+ "slow-first"
7010
+ ];
7011
+ function isFaultAllowed(level, fault) {
7012
+ const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
7013
+ return list.includes(fault);
7014
+ }
7015
+ var SLOW_FAULT_DELAY_MS = 5e3;
7016
+ function dropOneField(body) {
7017
+ let parsed;
7018
+ try {
7019
+ parsed = JSON.parse(body);
7020
+ } catch {
7021
+ return body;
7022
+ }
7023
+ if (Array.isArray(parsed)) {
7024
+ let key;
7025
+ for (const el of parsed) {
7026
+ if (el && typeof el === "object" && !Array.isArray(el)) {
7027
+ key ??= Object.keys(el)[0];
7028
+ if (key) delete el[key];
7029
+ }
7030
+ }
7031
+ return JSON.stringify(parsed);
7032
+ }
7033
+ if (parsed && typeof parsed === "object") {
7034
+ const obj = parsed;
7035
+ const key = Object.keys(obj)[0];
7036
+ if (key) delete obj[key];
7037
+ return JSON.stringify(obj);
7038
+ }
7039
+ return body;
7040
+ }
7041
+ function computeFault(fault, input) {
7042
+ const base = {
7043
+ status: input.status,
7044
+ body: input.body,
7045
+ contentType: input.contentType ?? null,
7046
+ delayMs: 0,
7047
+ changed: false
7048
+ };
7049
+ switch (fault) {
7050
+ case "status-500":
7051
+ return { ...base, status: 500, changed: input.status !== 500 };
7052
+ case "empty-body":
7053
+ return { ...base, body: "", changed: input.body.length > 0 };
7054
+ case "drop-field": {
7055
+ const body = dropOneField(input.body);
7056
+ return { ...base, body, changed: body !== input.body };
7057
+ }
7058
+ case "stale-value": {
7059
+ const body = input.priorBody ?? input.body;
7060
+ return { ...base, body, changed: body !== input.body };
7061
+ }
7062
+ case "slow":
7063
+ return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
7064
+ default:
7065
+ return base;
7066
+ }
7067
+ }
7068
+
7069
+ // src/internal/probe/sign.ts
7070
+ var import_node_crypto = require("crypto");
7071
+ function signProbeMessage(secret, nonce, ts, specJson) {
7072
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
7073
+ }
7074
+ function buildProbeHeader(secret, spec, now = Date.now()) {
7075
+ const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
7076
+ const specJson = JSON.stringify(spec);
7077
+ const sig = signProbeMessage(secret, nonce, now, specJson);
7078
+ return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
7079
+ }
7080
+
7081
+ // src/internal/probe/interception.ts
7082
+ function parseProbeTrace(traceHeader) {
7083
+ const none = { appliedFault: null, serverError: false };
7084
+ if (!traceHeader) return none;
7085
+ try {
7086
+ const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
7087
+ if (!Array.isArray(spans)) return none;
7088
+ const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
7089
+ if (!root) return none;
7090
+ const attrs = root.attrs ?? {};
7091
+ const appliedRaw = attrs["piwi.probe.applied"];
7092
+ const status = Number(attrs["http.status_code"]) || 0;
7093
+ return {
7094
+ appliedFault: appliedRaw != null ? String(appliedRaw) : null,
7095
+ serverError: root.status === "error" || status >= 500
7096
+ };
7097
+ } catch {
7098
+ return none;
7099
+ }
7100
+ }
7101
+ function fulfillHeaders(responseHeaders, contentType) {
7102
+ const headers = { ...responseHeaders };
7103
+ delete headers["content-length"];
7104
+ delete headers["content-encoding"];
7105
+ if (contentType) headers["content-type"] = contentType;
7106
+ return headers;
7107
+ }
7108
+ function originOf(url) {
7109
+ try {
7110
+ const origin = new URL(url).origin;
7111
+ return origin && origin !== "null" ? origin : null;
7112
+ } catch {
7113
+ return null;
7114
+ }
7115
+ }
7116
+ async function installProbeInterception(page, item) {
7117
+ if (!isFaultAllowed(item.level ?? "client", item.fault)) {
7118
+ return { applied: () => false, appliedFault: () => null, serverError: () => false };
7119
+ }
7120
+ const state = createProbeState();
7121
+ let applied = false;
7122
+ let appliedFault = null;
7123
+ let serverError = false;
7124
+ let appOrigin = null;
7125
+ const probeSecret = process.env[PIWI_PROBE_ENV.secret];
7126
+ page.on("framenavigated", (frame) => {
7127
+ if (frame === page.mainFrame()) {
7128
+ markNavigated(state);
7129
+ const origin = originOf(page.url());
7130
+ if (origin) appOrigin = origin;
7131
+ }
7132
+ });
7133
+ await page.route("**/*", async (route) => {
7134
+ const request = route.request();
7135
+ if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
7136
+ await route.fallback();
7137
+ return;
7138
+ }
7139
+ if (item.level === "server") {
7140
+ if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
7141
+ await route.fallback();
7142
+ return;
7143
+ }
7144
+ const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
7145
+ if (item.dependency) spec.dependency = item.dependency;
7146
+ try {
7147
+ const response = await route.fetch({
7148
+ headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
7149
+ });
7150
+ const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
7151
+ appliedFault = trace.appliedFault;
7152
+ serverError = trace.serverError;
7153
+ applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
7154
+ await route.fulfill({ response });
7155
+ } catch {
7156
+ applied = false;
7157
+ await route.fallback();
7158
+ }
7159
+ return;
7160
+ }
7161
+ try {
7162
+ const response = await route.fetch();
7163
+ const body = await response.text();
7164
+ const out = computeFault(item.fault, {
7165
+ status: response.status(),
7166
+ body,
7167
+ contentType: response.headers()["content-type"] ?? null
7168
+ });
7169
+ applied = out.changed;
7170
+ if (applied) appliedFault = item.fault;
7171
+ if (out.delayMs > 0) await new Promise((resolve5) => setTimeout(resolve5, out.delayMs));
7172
+ const headers = fulfillHeaders(response.headers(), out.contentType);
7173
+ await route.fulfill({ status: out.status, body: out.body, headers });
7174
+ } catch {
7175
+ applied = false;
7176
+ await route.fallback();
7177
+ }
7178
+ });
7179
+ return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
7180
+ }
7181
+
6278
7182
  // src/internal/capture/inspect-on-failure.ts
6279
7183
  function isCi(ci) {
6280
7184
  return ci !== void 0 && ci !== "" && ci !== "false";
@@ -6311,88 +7215,11 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
6311
7215
  }
6312
7216
 
6313
7217
  // src/internal/capture/pick-on-failure.ts
6314
- var path17 = __toESM(require("path"));
7218
+ var path19 = __toESM(require("path"));
6315
7219
  var ANSI_RE2 = /\[[0-9;]*m/g;
6316
- function endOfString(s, start) {
6317
- const q = s[start];
6318
- for (let i = start + 1; i < s.length; i++) {
6319
- if (s[i] === "\\") {
6320
- i++;
6321
- continue;
6322
- }
6323
- if (s[i] === q) return i;
6324
- }
6325
- return s.length - 1;
6326
- }
6327
- function matchBrace(s, start) {
6328
- let depth = 0;
6329
- for (let i = start; i < s.length; i++) {
6330
- if (s[i] === "{") depth++;
6331
- else if (s[i] === "}" && --depth === 0) return i;
6332
- }
6333
- return s.length - 1;
6334
- }
6335
- function parseOptions(src) {
6336
- const obj = {};
6337
- const re = /(\w+)\s*:\s*('(?:\\.|[^'])*'|"(?:\\.|[^"])*"|true|false|-?\d+)/g;
6338
- let m;
6339
- while ((m = re.exec(src)) !== null) {
6340
- const key = m[1];
6341
- const raw = m[2];
6342
- if (raw === "true") obj[key] = true;
6343
- else if (raw === "false") obj[key] = false;
6344
- else if (/^-?\d+$/.test(raw)) obj[key] = Number(raw);
6345
- else obj[key] = raw.slice(1, -1).replace(/\\(.)/g, "$1");
6346
- }
6347
- return obj;
6348
- }
6349
- function parseArgs(inner) {
6350
- const args = [];
6351
- let i = 0;
6352
- while (i < inner.length) {
6353
- const c = inner[i];
6354
- if (c === " " || c === ",") {
6355
- i++;
6356
- continue;
6357
- }
6358
- if (c === "'" || c === '"') {
6359
- const end = endOfString(inner, i);
6360
- args.push(inner.slice(i + 1, end).replace(/\\(.)/g, "$1"));
6361
- i = end + 1;
6362
- continue;
6363
- }
6364
- if (c === "{") {
6365
- const end = matchBrace(inner, i);
6366
- args.push(parseOptions(inner.slice(i, end + 1)));
6367
- i = end + 1;
6368
- continue;
6369
- }
6370
- i++;
6371
- }
6372
- return args;
6373
- }
6374
- function leafExpression(expr) {
6375
- let depth = 0;
6376
- let leafStart = 0;
6377
- for (let i = 0; i < expr.length - 1; i++) {
6378
- const c = expr[i];
6379
- if (c === "'" || c === '"') {
6380
- i = endOfString(expr, i);
6381
- continue;
6382
- }
6383
- if (c === "(") depth++;
6384
- else if (c === ")") {
6385
- depth--;
6386
- if (depth === 0 && expr[i + 1] === ".") leafStart = i + 2;
6387
- }
6388
- }
6389
- return expr.slice(leafStart);
6390
- }
6391
7220
  function parseLeafLocatorExpression(rawExpr) {
6392
- const expr = leafExpression(rawExpr.trim());
6393
- const m = /^([A-Za-z]+)\((.*)\)$/s.exec(expr);
6394
- if (!m) return null;
6395
- return { method: m[1], args: parseArgs(m[2].trim()) };
7221
+ const leaf = parseLeafLocatorCall(rawExpr);
7222
+ return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
6396
7223
  }
6397
7224
  function deriveFailedLocator(testInfo) {
6398
7225
  const info = testInfo;
@@ -6405,7 +7232,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
6405
7232
  const parsed = parseLeafLocatorExpression(line[1].trim());
6406
7233
  if (!parsed) continue;
6407
7234
  const loc = err.location;
6408
- const location = loc ? `${path17.relative(process.cwd(), loc.file).split(path17.sep).join("/")}:${loc.line}:${loc.column}` : null;
7235
+ const location = loc ? `${path19.relative(process.cwd(), loc.file).split(path19.sep).join("/")}:${loc.line}:${loc.column}` : null;
6409
7236
  return { method: parsed.method, args: parsed.args, location };
6410
7237
  }
6411
7238
  return null;
@@ -6599,6 +7426,9 @@ function createSink() {
6599
7426
  stashedPageState: null,
6600
7427
  stashedAria: null,
6601
7428
  stashedAriaJson: null,
7429
+ pageInventories: [],
7430
+ probeItem: null,
7431
+ probeInterception: null,
6602
7432
  pickOffered: false,
6603
7433
  userPick: null
6604
7434
  };
@@ -6775,6 +7605,62 @@ async function readPageState(page) {
6775
7605
  return null;
6776
7606
  }
6777
7607
  }
7608
+ var inventoriedPageKeys = /* @__PURE__ */ new Set();
7609
+ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
7610
+ var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
7611
+ var MAX_PAGE_INVENTORY_WINDOWS = 64;
7612
+ function withTimeout(promise, ms) {
7613
+ return new Promise((resolve5, reject) => {
7614
+ const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
7615
+ promise.then(
7616
+ (value) => {
7617
+ clearTimeout(timer);
7618
+ resolve5(value);
7619
+ },
7620
+ (error) => {
7621
+ clearTimeout(timer);
7622
+ reject(error);
7623
+ }
7624
+ );
7625
+ });
7626
+ }
7627
+ async function readPageInventory(page) {
7628
+ try {
7629
+ const inventory = await withTimeout(
7630
+ page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
7631
+ PAGE_INVENTORY_EVAL_TIMEOUT_MS
7632
+ );
7633
+ return inventory ? { ...inventory, capturedAt: Date.now() } : null;
7634
+ } catch {
7635
+ return null;
7636
+ }
7637
+ }
7638
+ async function recordPageWindow(sink, page) {
7639
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7640
+ let url;
7641
+ try {
7642
+ url = page.url();
7643
+ } catch {
7644
+ return;
7645
+ }
7646
+ if (!url || url.startsWith("about:")) return;
7647
+ const key = inventoryPageKey(url);
7648
+ const capturedAt = Date.now();
7649
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7650
+ if (key && !inventoriedPageKeys.has(key)) {
7651
+ inventoriedPageKeys.add(key);
7652
+ const content = await readPageInventory(page);
7653
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7654
+ sink.pageInventories.push({
7655
+ url: content?.url ?? url,
7656
+ controls: content?.controls ?? [],
7657
+ links: content?.links ?? [],
7658
+ capturedAt
7659
+ });
7660
+ return;
7661
+ }
7662
+ sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
7663
+ }
6778
7664
  async function stashPageState(sink, closing) {
6779
7665
  const page = sink.lastActivePage;
6780
7666
  if (!page || isPageClosed(page)) return;
@@ -6799,6 +7685,9 @@ async function stashPageState(sink, closing) {
6799
7685
  } else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
6800
7686
  await sampleAria();
6801
7687
  }
7688
+ if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
7689
+ await recordPageWindow(sink, page);
7690
+ }
6802
7691
  }
6803
7692
  async function maybeOpenPicker(sink, closing) {
6804
7693
  if (sink.pickOffered) return;
@@ -7042,6 +7931,13 @@ function wrapFrameLocator(page, frameLocator) {
7042
7931
  function instrumentPage(page) {
7043
7932
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
7044
7933
  INSTRUMENTED_PAGES.add(page);
7934
+ const probeSink = currentSink;
7935
+ if (probeSink?.probeItem && !probeSink.probeInterception) {
7936
+ void installProbeInterception(page, probeSink.probeItem).then((interception) => {
7937
+ probeSink.probeInterception = interception;
7938
+ }).catch(() => {
7939
+ });
7940
+ }
7045
7941
  const ctx = pageContext(page);
7046
7942
  if (ctx) instrumentContext(ctx);
7047
7943
  if (typeof page.close === "function") {
@@ -7077,6 +7973,14 @@ function instrumentPage(page) {
7077
7973
  page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
7078
7974
  }
7079
7975
  }
7976
+ if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
7977
+ page.on("framenavigated", (frame) => {
7978
+ const sink = currentSink;
7979
+ if (!sink || frame !== page.mainFrame()) return;
7980
+ void recordPageWindow(sink, page).catch(() => {
7981
+ });
7982
+ });
7983
+ }
7080
7984
  page.on("console", (msg) => {
7081
7985
  const sink = currentSink;
7082
7986
  if (!sink) return;
@@ -7136,14 +8040,14 @@ function instrumentPage(page) {
7136
8040
  const logHeader = headers["x-piwi-logs"];
7137
8041
  if (logHeader) {
7138
8042
  try {
7139
- entry.serverLogs = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
8043
+ entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
7140
8044
  } catch {
7141
8045
  }
7142
8046
  }
7143
8047
  const traceHeader = headers["x-piwi-trace"];
7144
8048
  if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
7145
8049
  try {
7146
- entry.serverTraces = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
8050
+ entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
7147
8051
  } catch {
7148
8052
  }
7149
8053
  }
@@ -7328,6 +8232,16 @@ async function flushSink(sink, testInfo) {
7328
8232
  });
7329
8233
  }
7330
8234
  }
8235
+ if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
8236
+ if (pageReadable) await recordPageWindow(sink, page);
8237
+ const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
8238
+ if (windows.length > 0) {
8239
+ await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
8240
+ contentType: "application/json",
8241
+ body: Buffer.from(JSON.stringify(windows))
8242
+ });
8243
+ }
8244
+ }
7331
8245
  }
7332
8246
  var piwiFixtures = {
7333
8247
  // Worker-scoped: patch the shared browser so every page/context created from
@@ -7353,11 +8267,36 @@ var piwiFixtures = {
7353
8267
  async ({}, use, testInfo) => {
7354
8268
  const sink = createSink();
7355
8269
  sink.testInfo = testInfo;
8270
+ if (isProbeMode())
8271
+ sink.probeItem = probeItemForTest({
8272
+ title: testInfo.title,
8273
+ file: testInfo.file,
8274
+ titlePath: testInfo.titlePath
8275
+ });
7356
8276
  currentSink = sink;
7357
8277
  try {
7358
8278
  await use();
7359
8279
  } finally {
7360
8280
  currentSink = null;
8281
+ if (sink.probeItem) {
8282
+ const applied = sink.probeInterception?.applied() ?? false;
8283
+ const level = sink.probeItem.level ?? "client";
8284
+ const handled = level === "server" ? classifyProbeHandled({
8285
+ consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
8286
+ dialogs: sink.dialogs.length,
8287
+ backendError: sink.probeInterception?.serverError() ?? false
8288
+ }) : "n/a";
8289
+ recordProbeOutcome({
8290
+ testCaseId: sink.probeItem.testCaseId,
8291
+ routeKey: sink.probeItem.routeKey,
8292
+ fault: sink.probeItem.fault,
8293
+ applied,
8294
+ outcome: outcomeFromStatus(testInfo.status, applied),
8295
+ level,
8296
+ dependency: sink.probeItem.dependency ?? null,
8297
+ handled
8298
+ });
8299
+ }
7361
8300
  await flushSink(sink, testInfo);
7362
8301
  }
7363
8302
  },
@@ -7369,13 +8308,13 @@ function extendPiwiFixtures(test) {
7369
8308
  }
7370
8309
 
7371
8310
  // src/internal/ai/ai-fixtures.ts
7372
- var fs16 = __toESM(require("fs"));
7373
- var path20 = __toESM(require("path"));
8311
+ var fs18 = __toESM(require("fs"));
8312
+ var path22 = __toESM(require("path"));
7374
8313
  var import_test = require("@playwright/test");
7375
8314
 
7376
8315
  // src/internal/ai/artifact.ts
7377
- var fs15 = __toESM(require("fs"));
7378
- var path18 = __toESM(require("path"));
8316
+ var fs17 = __toESM(require("fs"));
8317
+ var path20 = __toESM(require("path"));
7379
8318
  var ARTIFACT_VERSION = 1;
7380
8319
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
7381
8320
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -7468,7 +8407,7 @@ function parseEntry(text) {
7468
8407
  function readEntry(file) {
7469
8408
  let text;
7470
8409
  try {
7471
- text = fs15.readFileSync(file, "utf8");
8410
+ text = fs17.readFileSync(file, "utf8");
7472
8411
  } catch (error) {
7473
8412
  if (error.code === "ENOENT") return null;
7474
8413
  throw error;
@@ -7478,28 +8417,28 @@ function readEntry(file) {
7478
8417
  function writeEntry(file, entry) {
7479
8418
  const canonical = serializeEntry(entry);
7480
8419
  return withEntryLock(file, () => {
7481
- if (fs15.existsSync(file) && fs15.readFileSync(file, "utf8") === canonical) return { written: false };
7482
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
8420
+ if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
8421
+ fs17.mkdirSync(path20.dirname(file), { recursive: true });
7483
8422
  const tmp = `${file}.${process.pid}.tmp`;
7484
- fs15.writeFileSync(tmp, canonical);
7485
- fs15.renameSync(tmp, file);
8423
+ fs17.writeFileSync(tmp, canonical);
8424
+ fs17.renameSync(tmp, file);
7486
8425
  return { written: true };
7487
8426
  });
7488
8427
  }
7489
8428
  function withEntryLock(file, fn) {
7490
8429
  const lock = `${file}.lock`;
7491
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
8430
+ fs17.mkdirSync(path20.dirname(file), { recursive: true });
7492
8431
  const deadline = Date.now() + 5e3;
7493
8432
  for (; ; ) {
7494
8433
  try {
7495
- const fd = fs15.openSync(lock, "wx");
7496
- fs15.closeSync(fd);
8434
+ const fd = fs17.openSync(lock, "wx");
8435
+ fs17.closeSync(fd);
7497
8436
  break;
7498
8437
  } catch (error) {
7499
8438
  if (error.code !== "EEXIST") throw error;
7500
8439
  if (Date.now() > deadline) {
7501
8440
  try {
7502
- fs15.unlinkSync(lock);
8441
+ fs17.unlinkSync(lock);
7503
8442
  } catch {
7504
8443
  }
7505
8444
  }
@@ -7509,7 +8448,7 @@ function withEntryLock(file, fn) {
7509
8448
  return fn();
7510
8449
  } finally {
7511
8450
  try {
7512
- fs15.unlinkSync(lock);
8451
+ fs17.unlinkSync(lock);
7513
8452
  } catch {
7514
8453
  }
7515
8454
  }
@@ -7517,7 +8456,7 @@ function withEntryLock(file, fn) {
7517
8456
 
7518
8457
  // src/internal/ai/keys.ts
7519
8458
  var crypto3 = __toESM(require("crypto"));
7520
- var path19 = __toESM(require("path"));
8459
+ var path21 = __toESM(require("path"));
7521
8460
  var DEFAULT_AI_DIR = "__piwi__";
7522
8461
  function normalizeTemplate(template) {
7523
8462
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -7531,13 +8470,13 @@ function slug(text, maxLength = 40) {
7531
8470
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
7532
8471
  }
7533
8472
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
7534
- return path19.join(path19.dirname(specFile), dir, path19.basename(specFile));
8473
+ return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
7535
8474
  }
7536
8475
  function entryPath(params) {
7537
8476
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
7538
8477
  const hash = hashTemplate(basis, params.ordinal ?? 0);
7539
8478
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
7540
- return path19.join(entryDir(params.specFile, params.dir), name);
8479
+ return path21.join(entryDir(params.specFile, params.dir), name);
7541
8480
  }
7542
8481
  function parseLocation(location) {
7543
8482
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -7991,7 +8930,7 @@ function testIdentity(testInfo) {
7991
8930
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
7992
8931
  }
7993
8932
  function relativeToCwd(file) {
7994
- return path20.relative(process.cwd(), file).split(path20.sep).join("/");
8933
+ return path22.relative(process.cwd(), file).split(path22.sep).join("/");
7995
8934
  }
7996
8935
  function recordIntents(intents, entry) {
7997
8936
  const add = (locator, kind) => {
@@ -8018,7 +8957,7 @@ function createAiApi(page, testInfo, config, used, intents) {
8018
8957
  const readSource = () => {
8019
8958
  if (source === null) {
8020
8959
  try {
8021
- source = fs16.readFileSync(testInfo.file, "utf8");
8960
+ source = fs18.readFileSync(testInfo.file, "utf8");
8022
8961
  } catch {
8023
8962
  source = "";
8024
8963
  }