@piwitests/reporter 0.36.0 → 0.38.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;
@@ -1441,6 +1494,9 @@ var MetadataCollector = class {
1441
1494
  testDir: p.testDir,
1442
1495
  use: {
1443
1496
  browserName: p.use?.browserName || p.name,
1497
+ // The origin of `baseURL` is what the graph treats as the run's own
1498
+ // surface, so only requests to it become route nodes.
1499
+ baseURL: p.use?.baseURL,
1444
1500
  viewport: p.use?.viewport,
1445
1501
  deviceScaleFactor: p.use?.deviceScaleFactor
1446
1502
  }
@@ -1757,6 +1813,7 @@ var StreamManager = class {
1757
1813
  startTime,
1758
1814
  environment: this.options.environment || null,
1759
1815
  label: this.options.label || null,
1816
+ keep: this.options.keep === true,
1760
1817
  metadata,
1761
1818
  instanceId,
1762
1819
  playwrightVersion,
@@ -1779,6 +1836,7 @@ var StreamManager = class {
1779
1836
  startTime,
1780
1837
  environment: this.options.environment || null,
1781
1838
  label: this.options.label || null,
1839
+ keep: this.options.keep === true,
1782
1840
  metadata,
1783
1841
  instanceId,
1784
1842
  playwrightVersion,
@@ -2080,6 +2138,9 @@ function maskTokenLike(text) {
2080
2138
  // ../core/src/step-analysis.ts
2081
2139
  var MAX_STEP_PARAM_KEYS = 20;
2082
2140
  var MAX_STEP_PARAM_VALUE_CHARS = 200;
2141
+ function capStepValue(value, max) {
2142
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 1))}\u2026` : value;
2143
+ }
2083
2144
  function categorizeStep(title, pwCategory, params) {
2084
2145
  if (!title) return "other";
2085
2146
  if (pwCategory === "hook" || pwCategory === "fixture") return pwCategory;
@@ -2110,11 +2171,11 @@ function normalizeStepParams(raw) {
2110
2171
  out[key] = value;
2111
2172
  count++;
2112
2173
  } else if (typeof value === "string") {
2113
- out[key] = maskTokenLike(value).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
2174
+ out[key] = capStepValue(maskTokenLike(value), MAX_STEP_PARAM_VALUE_CHARS);
2114
2175
  count++;
2115
2176
  } else if (value != null) {
2116
2177
  try {
2117
- out[key] = maskTokenLike(JSON.stringify(value)).slice(0, MAX_STEP_PARAM_VALUE_CHARS);
2178
+ out[key] = capStepValue(maskTokenLike(JSON.stringify(value)), MAX_STEP_PARAM_VALUE_CHARS);
2118
2179
  count++;
2119
2180
  } catch {
2120
2181
  }
@@ -2437,8 +2498,8 @@ function isListMode(argv = process.argv) {
2437
2498
  }
2438
2499
 
2439
2500
  // src/public/global-setup.ts
2440
- var path11 = __toESM(require("path"));
2441
- var fs12 = __toESM(require("fs"));
2501
+ var path12 = __toESM(require("path"));
2502
+ var fs13 = __toESM(require("fs"));
2442
2503
 
2443
2504
  // src/internal/support/aria-sampling.ts
2444
2505
  var path10 = __toESM(require("path"));
@@ -2489,17 +2550,83 @@ function isDueForAriaSample(testInfo) {
2489
2550
  return set.has(ariaSampleIdentity(relativeTestFile(testInfo.file), testInfo.title));
2490
2551
  }
2491
2552
 
2553
+ // src/internal/manifest/collect.ts
2554
+ var fs12 = __toESM(require("fs"));
2555
+ var path11 = __toESM(require("path"));
2556
+ var COMMITTED_MANIFEST_FILE = "piwi.manifest.json";
2557
+ var INSTRUMENTATION_MANIFEST_PATH = "/__piwi/manifest";
2558
+ var INSTRUMENTATION_HEADERS = ["x-piwi-logs", "x-piwi-trace"];
2559
+ function parseManifestJson(text) {
2560
+ let parsed;
2561
+ try {
2562
+ parsed = JSON.parse(text);
2563
+ } catch {
2564
+ return null;
2565
+ }
2566
+ if (!parsed || typeof parsed !== "object") return null;
2567
+ const obj = parsed;
2568
+ 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) => ({
2569
+ method: r.method,
2570
+ pattern: r.pattern,
2571
+ handler: typeof r.handler === "string" ? r.handler : void 0,
2572
+ responses: Array.isArray(r.responses) ? r.responses.filter((c) => Number.isInteger(c)) : void 0
2573
+ })) : void 0;
2574
+ 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;
2575
+ if (!routes && !pages) return null;
2576
+ return { routes, pages };
2577
+ }
2578
+ function hasInstrumentationHeader(headers) {
2579
+ const get = (name) => typeof headers.get === "function" ? headers.get(name) : headers[name];
2580
+ return INSTRUMENTATION_HEADERS.some((h) => get(h) != null && get(h) !== "");
2581
+ }
2582
+ function readCommittedManifest(configDir) {
2583
+ const file = path11.join(configDir, COMMITTED_MANIFEST_FILE);
2584
+ try {
2585
+ if (!fs12.existsSync(file)) return null;
2586
+ return parseManifestJson(fs12.readFileSync(file, "utf8"));
2587
+ } catch {
2588
+ return null;
2589
+ }
2590
+ }
2591
+ function configDirFromConfig(config) {
2592
+ if (config?.configFile) return path11.dirname(config.configFile);
2593
+ if (config?.rootDir) return config.rootDir;
2594
+ return process.cwd();
2595
+ }
2596
+ function baseUrlFromConfig(config) {
2597
+ const top = config?.use?.baseURL;
2598
+ if (typeof top === "string" && top) return top;
2599
+ for (const project of config?.projects ?? []) {
2600
+ const url = project?.use?.baseURL;
2601
+ if (typeof url === "string" && url) return url;
2602
+ }
2603
+ return null;
2604
+ }
2605
+ var INSTRUMENTATION_FETCH_TIMEOUT_MS = 5e3;
2606
+ async function fetchInstrumentationManifest(baseUrl, fetchImpl = fetch) {
2607
+ try {
2608
+ const probe = await fetchImpl(baseUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
2609
+ if (!hasInstrumentationHeader(probe.headers)) return null;
2610
+ const manifestUrl = new URL(INSTRUMENTATION_MANIFEST_PATH, baseUrl).toString();
2611
+ const res = await fetchImpl(manifestUrl, { signal: AbortSignal.timeout(INSTRUMENTATION_FETCH_TIMEOUT_MS) });
2612
+ if (!res.ok) return null;
2613
+ return parseManifestJson(await res.text());
2614
+ } catch {
2615
+ return null;
2616
+ }
2617
+ }
2618
+
2492
2619
  // src/public/global-setup.ts
2493
2620
  function createGlobalSetup(options, userSetup) {
2494
2621
  return async function globalSetupFn(config) {
2495
- const piwiReporterPath = path11.resolve(__dirname, "./index.js");
2622
+ const piwiReporterPath = path12.resolve(__dirname, "./index.js");
2496
2623
  let inlineReporterOptions = {};
2497
2624
  if (Array.isArray(config?.reporter)) {
2498
2625
  for (const r of config.reporter) {
2499
2626
  if (!Array.isArray(r) || typeof r[0] !== "string") continue;
2500
2627
  const isPiwi = r[0].toLowerCase().includes("piwi") || (() => {
2501
2628
  try {
2502
- return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
2629
+ return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
2503
2630
  } catch {
2504
2631
  return false;
2505
2632
  }
@@ -2531,7 +2658,7 @@ function createGlobalSetup(options, userSetup) {
2531
2658
  if (!Array.isArray(r) || typeof r[0] !== "string") return false;
2532
2659
  if (r[0].toLowerCase().includes("piwi")) return true;
2533
2660
  try {
2534
- return path11.resolve(require.resolve(r[0])) === piwiReporterPath;
2661
+ return path12.resolve(require.resolve(r[0])) === piwiReporterPath;
2535
2662
  } catch {
2536
2663
  return false;
2537
2664
  }
@@ -2555,6 +2682,7 @@ function createGlobalSetup(options, userSetup) {
2555
2682
  projectDescription: opts.projectDescription,
2556
2683
  environment: opts.environment || null,
2557
2684
  label: opts.label || null,
2685
+ keep: opts.keep === true,
2558
2686
  startTime: (/* @__PURE__ */ new Date()).toISOString(),
2559
2687
  instanceId: computeInstanceId(opts.projectName, runLabel),
2560
2688
  shardIndex,
@@ -2563,7 +2691,7 @@ function createGlobalSetup(options, userSetup) {
2563
2691
  auth
2564
2692
  );
2565
2693
  if (response?.runId && response?.setupToken) {
2566
- fs12.writeFileSync(
2694
+ fs13.writeFileSync(
2567
2695
  getSetupFilePath(opts.projectName),
2568
2696
  JSON.stringify({
2569
2697
  runId: response.runId,
@@ -2574,18 +2702,43 @@ function createGlobalSetup(options, userSetup) {
2574
2702
  logger.debug(`Global setup: initializing run #${response.runId}`);
2575
2703
  }
2576
2704
  if (opts.projectName) clearAriaSampleFile(opts.projectName);
2577
- if (opts.sampleAriaOnPass !== false && opts.projectName) {
2705
+ const wantsAria = opts.sampleAriaOnPass !== false && !!opts.projectName;
2706
+ const committedManifest = opts.uploadManifest !== false ? readCommittedManifest(configDirFromConfig(config)) : null;
2707
+ const manifestBaseUrl = opts.uploadManifest !== false ? baseUrlFromConfig(config) : null;
2708
+ const wantsManifest = opts.uploadManifest !== false && !!opts.projectName && (!!committedManifest || !!manifestBaseUrl);
2709
+ let projectId;
2710
+ if ((wantsAria || wantsManifest) && opts.projectName) {
2578
2711
  const menu = await httpClient.getJSON("/api/projects/menu", auth);
2579
- const projectId = menu?.items?.find(
2712
+ projectId = menu?.items?.find(
2580
2713
  (p) => p.name.toLowerCase() === opts.projectName.toLowerCase()
2581
2714
  )?.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.`);
2715
+ }
2716
+ if (wantsAria && projectId != null) {
2717
+ const sampling = await httpClient.getJSON(`/api/projects/${projectId}/aria-sampling`, auth);
2718
+ const tests = Array.isArray(sampling?.tests) ? sampling.tests : null;
2719
+ if (tests) {
2720
+ const identities = tests.filter((t) => typeof t.filePath === "string" && typeof t.title === "string").map((t) => ariaSampleIdentity(t.filePath, t.title));
2721
+ writeAriaSampleFile(opts.projectName, identities);
2722
+ logger.debug(`Green ARIA sampling: ${identities.length} test(s) due a sample.`);
2723
+ }
2724
+ }
2725
+ if (wantsManifest && projectId != null) {
2726
+ const branch = resolveScmBranch(process.env) ?? null;
2727
+ if (committedManifest) {
2728
+ await httpClient.putJSON(
2729
+ `/api/projects/${projectId}/surface/manifest`,
2730
+ { source: "committed", branch, manifest: committedManifest },
2731
+ auth
2732
+ ).catch((e) => logger.debug(`Manifest upload (committed) skipped: ${errorMessage(e)}`));
2733
+ }
2734
+ if (manifestBaseUrl) {
2735
+ const declared = await fetchInstrumentationManifest(manifestBaseUrl);
2736
+ if (declared) {
2737
+ await httpClient.putJSON(
2738
+ `/api/projects/${projectId}/surface/manifest`,
2739
+ { source: "instrumentation", branch, manifest: declared },
2740
+ auth
2741
+ ).catch((e) => logger.debug(`Manifest upload (instrumentation) skipped: ${errorMessage(e)}`));
2589
2742
  }
2590
2743
  }
2591
2744
  }
@@ -2597,8 +2750,110 @@ function createGlobalSetup(options, userSetup) {
2597
2750
  }
2598
2751
 
2599
2752
  // src/public/config-wrapper.ts
2600
- var path12 = __toESM(require("path"));
2601
- var fs13 = __toESM(require("fs"));
2753
+ var path14 = __toESM(require("path"));
2754
+ var fs15 = __toESM(require("fs"));
2755
+
2756
+ // src/internal/probe/mode.ts
2757
+ var fs14 = __toESM(require("fs"));
2758
+ var path13 = __toESM(require("path"));
2759
+
2760
+ // src/internal/probe/plan.ts
2761
+ function probeRunMetadata(base = {}) {
2762
+ return { ...base, piwiProbe: true };
2763
+ }
2764
+ function parseRouteKey(routeKey) {
2765
+ const space = routeKey.indexOf(" ");
2766
+ if (space < 0) return { method: "", pattern: routeKey };
2767
+ return { method: routeKey.slice(0, space).toUpperCase(), pattern: routeKey.slice(space + 1) };
2768
+ }
2769
+ function patternToRegex(pattern) {
2770
+ const path23 = pattern.split("?")[0] ?? pattern;
2771
+ const escaped = path23.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/:[A-Za-z0-9_]+/g, "[^/]+");
2772
+ return new RegExp(`^${escaped}$`);
2773
+ }
2774
+ function suitePathEqual(a, b) {
2775
+ if (!a || !b || a.length !== b.length) return false;
2776
+ return a.every((title, i) => title === b[i]);
2777
+ }
2778
+ function matchProbeItem(plan, test) {
2779
+ const byFileTitle = plan.items.filter(
2780
+ (item) => item.filePath && test.filePath && item.filePath === test.filePath && item.testTitle === test.title
2781
+ );
2782
+ if (byFileTitle.length === 1) return byFileTitle[0];
2783
+ if (byFileTitle.length > 1) {
2784
+ return byFileTitle.find((item) => suitePathEqual(item.suitePath, test.suitePath)) ?? byFileTitle[0];
2785
+ }
2786
+ for (const item of plan.items) {
2787
+ if (!item.filePath && item.testTitle && test.title && item.testTitle === test.title) return item;
2788
+ }
2789
+ return null;
2790
+ }
2791
+ function requestMatchesRoute(method, url, routeKey) {
2792
+ const { method: rm, pattern } = parseRouteKey(routeKey);
2793
+ if (rm && rm !== method.toUpperCase()) return false;
2794
+ let path23;
2795
+ try {
2796
+ path23 = new URL(url).pathname;
2797
+ } catch {
2798
+ path23 = url.split("?")[0] ?? url;
2799
+ }
2800
+ return patternToRegex(pattern).test(path23);
2801
+ }
2802
+ function createProbeState() {
2803
+ return { navigated: false, matches: 0 };
2804
+ }
2805
+ function markNavigated(state) {
2806
+ state.navigated = true;
2807
+ }
2808
+ function shouldMutate(state, item) {
2809
+ if (!state.navigated) return false;
2810
+ state.matches += 1;
2811
+ return state.matches === Math.max(1, item.nth ?? 1);
2812
+ }
2813
+
2814
+ // src/internal/probe/mode.ts
2815
+ var planCache;
2816
+ function isProbeMode() {
2817
+ return process.env[PIWI_PROBE_ENV.flag] === "1" || process.env[PIWI_PROBE_ENV.flag] === "true";
2818
+ }
2819
+ function loadPlan() {
2820
+ if (planCache !== void 0) return planCache;
2821
+ planCache = null;
2822
+ const path23 = process.env[PIWI_PROBE_ENV.plan];
2823
+ if (path23) {
2824
+ try {
2825
+ planCache = JSON.parse(fs14.readFileSync(path23, "utf8"));
2826
+ } catch {
2827
+ planCache = null;
2828
+ }
2829
+ }
2830
+ return planCache;
2831
+ }
2832
+ function probeItemForTest(test) {
2833
+ const plan = loadPlan();
2834
+ if (!plan) return null;
2835
+ const filePath = test.file ? path13.relative(process.cwd(), test.file).split(path13.sep).join("/") : null;
2836
+ const suitePath = test.titlePath ? test.titlePath.slice(1, -1) : void 0;
2837
+ return matchProbeItem(plan, { title: test.title, filePath, suitePath });
2838
+ }
2839
+ function classifyProbeHandled(signals) {
2840
+ return signals.consoleErrors > 0 || signals.dialogs > 0 || signals.backendError ? "degraded" : "graceful";
2841
+ }
2842
+ function outcomeFromStatus(status, applied) {
2843
+ if (!applied) return "inconclusive";
2844
+ return status === "failed" || status === "timedOut" || status === "interrupted" ? "noticed" : "not-noticed";
2845
+ }
2846
+ function recordProbeOutcome(line) {
2847
+ const path23 = process.env[PIWI_PROBE_ENV.results];
2848
+ if (!path23) return;
2849
+ try {
2850
+ fs14.appendFileSync(path23, `${JSON.stringify(line)}
2851
+ `);
2852
+ } catch {
2853
+ }
2854
+ }
2855
+
2856
+ // src/public/config-wrapper.ts
2602
2857
  var PIWI_MODULE = "@piwitests/reporter";
2603
2858
  var CAPTURE_DEFAULTS = {
2604
2859
  screenshot: "only-on-failure",
@@ -2663,11 +2918,11 @@ function injectReporter(reporter, piwiOptions) {
2663
2918
  }
2664
2919
  function resolveSetupModule() {
2665
2920
  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")
2921
+ path14.join(__dirname, "global-setup-module.js"),
2922
+ path14.join(__dirname, "..", "global-setup-module.js"),
2923
+ path14.join(__dirname, "..", "global-setup-module.ts")
2669
2924
  ];
2670
- return candidates.find((candidate) => fs13.existsSync(candidate)) ?? candidates[0];
2925
+ return candidates.find((candidate) => fs15.existsSync(candidate)) ?? candidates[0];
2671
2926
  }
2672
2927
  function wrapConfig(config, piwiOptions) {
2673
2928
  if (piwiOptions) applyOptionsToEnv(piwiOptions);
@@ -2679,6 +2934,7 @@ function wrapConfig(config, piwiOptions) {
2679
2934
  const forwarded = {};
2680
2935
  const failOnFlaky = piwiOptions?.failOnFlakyTests ?? readBool(process.env[PIWI_ENV_KEYS.failOnFlakyTests]);
2681
2936
  if (failOnFlaky === true) forwarded.failOnFlakyTests = true;
2937
+ if (isProbeMode()) forwarded.retries = 0;
2682
2938
  const use = applyCaptureDefaults(config.use, piwiOptions);
2683
2939
  return {
2684
2940
  ...config,
@@ -2867,7 +3123,7 @@ function collectTestMetadata(annotations) {
2867
3123
  }
2868
3124
 
2869
3125
  // src/internal/collect/error-text.ts
2870
- var path13 = __toESM(require("path"));
3126
+ var path15 = __toESM(require("path"));
2871
3127
 
2872
3128
  // ../core/src/error-text.ts
2873
3129
  function joinErrorMessages(errors) {
@@ -2896,13 +3152,13 @@ function buildErrorText(result) {
2896
3152
  if (!text) return null;
2897
3153
  const loc = result.error?.location;
2898
3154
  if (!loc?.file) return text;
2899
- const rel = path13.relative(process.cwd(), loc.file).split(path13.sep).join("/");
3155
+ const rel = path15.relative(process.cwd(), loc.file).split(path15.sep).join("/");
2900
3156
  return appendErrorLocation(text, { file: rel, line: loc.line, column: loc.column });
2901
3157
  }
2902
3158
 
2903
3159
  // src/internal/support/ci-output.ts
2904
- var fs14 = __toESM(require("fs"));
2905
- var path14 = __toESM(require("path"));
3160
+ var fs16 = __toESM(require("fs"));
3161
+ var path16 = __toESM(require("path"));
2906
3162
  var SUMMARY_MAX_FAILURES = 20;
2907
3163
  function emitRunOutputs(output, logger, outputFile, env = process.env) {
2908
3164
  logger.info(`View run: ${output.runUrl}`);
@@ -2912,9 +3168,9 @@ function emitRunOutputs(output, logger, outputFile, env = process.env) {
2912
3168
  }
2913
3169
  function writeOutputFile(file, output, logger) {
2914
3170
  try {
2915
- const dir = path14.dirname(file);
2916
- if (dir && dir !== ".") fs14.mkdirSync(dir, { recursive: true });
2917
- fs14.writeFileSync(
3171
+ const dir = path16.dirname(file);
3172
+ if (dir && dir !== ".") fs16.mkdirSync(dir, { recursive: true });
3173
+ fs16.writeFileSync(
2918
3174
  file,
2919
3175
  JSON.stringify(
2920
3176
  {
@@ -2994,7 +3250,7 @@ function emitGitLabDotenv(output, env, logger) {
2994
3250
  if (output.projectId != null) lines.push(`PIWI_PROJECT_ID=${output.projectId}`);
2995
3251
  if (output.ciBuildUrl) lines.push(`PIWI_CI_BUILD_URL=${output.ciBuildUrl}`);
2996
3252
  try {
2997
- fs14.writeFileSync(file, lines.join("\n") + "\n");
3253
+ fs16.writeFileSync(file, lines.join("\n") + "\n");
2998
3254
  logger.info(`Wrote GitLab dotenv report to ${file} (declare it as artifacts:reports:dotenv)`);
2999
3255
  } catch (error) {
3000
3256
  logger.warn(`Failed to write GitLab dotenv file '${file}': ${errorMessage(error)}`);
@@ -3002,7 +3258,7 @@ function emitGitLabDotenv(output, env, logger) {
3002
3258
  }
3003
3259
  function appendFileLines(file, lines, logger, label) {
3004
3260
  try {
3005
- fs14.appendFileSync(file, lines.join("\n") + "\n");
3261
+ fs16.appendFileSync(file, lines.join("\n") + "\n");
3006
3262
  } catch (error) {
3007
3263
  logger.warn(`Failed to write GitHub Actions ${label}: ${errorMessage(error)}`);
3008
3264
  }
@@ -3121,6 +3377,7 @@ var RunSubmitter = class {
3121
3377
  didNotRunTests: run.didNotRunTests,
3122
3378
  environment: run.options.environment,
3123
3379
  label: run.options.label || null,
3380
+ keep: run.options.keep === true,
3124
3381
  metadata: run.metadata,
3125
3382
  instanceId: run.instanceId,
3126
3383
  playwrightVersion: run.playwrightVersion ?? void 0,
@@ -3151,6 +3408,7 @@ var RunSubmitter = class {
3151
3408
  flakyTests,
3152
3409
  durations,
3153
3410
  label: run.options.label || null,
3411
+ keep: run.options.keep === true,
3154
3412
  metadata: run.metadata,
3155
3413
  hasPendingUploads: this.hasReports(run),
3156
3414
  playwrightVersion: run.playwrightVersion ?? void 0,
@@ -3238,6 +3496,298 @@ var RunSubmitter = class {
3238
3496
  }
3239
3497
  };
3240
3498
 
3499
+ // ../core/src/locator-chain.ts
3500
+ var LOCATING_METHODS = /* @__PURE__ */ new Set([
3501
+ "getByRole",
3502
+ "getByTestId",
3503
+ "getByText",
3504
+ "getByLabel",
3505
+ "getByPlaceholder",
3506
+ "getByAltText",
3507
+ "getByTitle",
3508
+ "locator",
3509
+ "frameLocator"
3510
+ ]);
3511
+ var NARROWING_METHODS = /* @__PURE__ */ new Set([
3512
+ "filter",
3513
+ "first",
3514
+ "last",
3515
+ "nth",
3516
+ "and",
3517
+ "or",
3518
+ "visible",
3519
+ "contentFrame",
3520
+ "owner"
3521
+ ]);
3522
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
3523
+ var LocatorParseError = class extends Error {
3524
+ constructor(message) {
3525
+ super(message);
3526
+ this.name = "LocatorParseError";
3527
+ }
3528
+ };
3529
+ var SIMPLE_ESCAPES = {
3530
+ n: "\n",
3531
+ t: " ",
3532
+ r: "\r",
3533
+ b: "\b",
3534
+ f: "\f",
3535
+ v: "\v",
3536
+ "0": "\0"
3537
+ };
3538
+ var Parser = class {
3539
+ constructor(src) {
3540
+ this.src = src;
3541
+ this.i = 0;
3542
+ }
3543
+ parseTopLevel() {
3544
+ const chain = this.parseChain();
3545
+ this.skipSpace();
3546
+ if (this.i < this.src.length) this.fail("unexpected text");
3547
+ return chain;
3548
+ }
3549
+ fail(message) {
3550
+ throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
3551
+ }
3552
+ skipSpace() {
3553
+ while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
3554
+ }
3555
+ peek() {
3556
+ return this.src[this.i];
3557
+ }
3558
+ expect(ch) {
3559
+ this.skipSpace();
3560
+ if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
3561
+ this.i++;
3562
+ }
3563
+ identifier() {
3564
+ const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
3565
+ if (!m) this.fail("expected a name");
3566
+ this.i += m[0].length;
3567
+ return m[0];
3568
+ }
3569
+ /**
3570
+ * A chain of calls joined by `.`. With `lenient`, parsing stops before the
3571
+ * first link that doesn't parse instead of failing, so a chain followed by
3572
+ * other text (an error message) is read up to where it ends. `spans` receives
3573
+ * where each top-level call starts and ends.
3574
+ */
3575
+ parseChain(opts = {}) {
3576
+ this.skipSpace();
3577
+ const calls = [];
3578
+ for (; ; ) {
3579
+ const before = this.i;
3580
+ if (calls.length > 0) this.i++;
3581
+ this.skipSpace();
3582
+ const start = this.i;
3583
+ try {
3584
+ calls.push(this.parseCall(calls.length === 0));
3585
+ } catch (error) {
3586
+ if (!opts.lenient || calls.length === 0) throw error;
3587
+ this.i = before;
3588
+ break;
3589
+ }
3590
+ opts.spans?.push({ start, end: this.i });
3591
+ const end = this.i;
3592
+ this.skipSpace();
3593
+ if (this.peek() !== ".") {
3594
+ this.i = end;
3595
+ break;
3596
+ }
3597
+ }
3598
+ return { calls };
3599
+ }
3600
+ /** Where the parser stopped. */
3601
+ get position() {
3602
+ return this.i;
3603
+ }
3604
+ parseCall(first) {
3605
+ this.skipSpace();
3606
+ const method = this.identifier();
3607
+ if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
3608
+ if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
3609
+ this.expect("(");
3610
+ const args = [];
3611
+ this.skipSpace();
3612
+ if (this.peek() === ")") {
3613
+ this.i++;
3614
+ return { method, args };
3615
+ }
3616
+ for (; ; ) {
3617
+ args.push(this.parseValue());
3618
+ this.skipSpace();
3619
+ if (this.peek() === ",") {
3620
+ this.i++;
3621
+ this.skipSpace();
3622
+ if (this.peek() === ")") {
3623
+ this.i++;
3624
+ break;
3625
+ }
3626
+ continue;
3627
+ }
3628
+ this.expect(")");
3629
+ break;
3630
+ }
3631
+ return { method, args };
3632
+ }
3633
+ parseValue() {
3634
+ this.skipSpace();
3635
+ const ch = this.peek();
3636
+ if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
3637
+ if (ch === "/") return this.parseRegex();
3638
+ if (ch === "{") return this.parseObject();
3639
+ if (ch !== void 0 && /[-\d]/.test(ch)) {
3640
+ const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
3641
+ if (!m) this.fail("expected a number");
3642
+ this.i += m[0].length;
3643
+ return { type: "number", value: Number(m[0]) };
3644
+ }
3645
+ const rest = this.src.slice(this.i);
3646
+ if (/^true\b/.test(rest)) {
3647
+ this.i += 4;
3648
+ return { type: "boolean", value: true };
3649
+ }
3650
+ if (/^false\b/.test(rest)) {
3651
+ this.i += 5;
3652
+ return { type: "boolean", value: false };
3653
+ }
3654
+ if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
3655
+ return this.fail("expected a value");
3656
+ }
3657
+ /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
3658
+ parseString() {
3659
+ const quote = this.src[this.i];
3660
+ let out = "";
3661
+ this.i++;
3662
+ while (this.i < this.src.length) {
3663
+ const c = this.src[this.i];
3664
+ if (c === quote) {
3665
+ this.i++;
3666
+ return out;
3667
+ }
3668
+ if (c !== "\\") {
3669
+ out += c;
3670
+ this.i++;
3671
+ continue;
3672
+ }
3673
+ const next = this.src[this.i + 1];
3674
+ if (next === void 0) break;
3675
+ const simple = SIMPLE_ESCAPES[next];
3676
+ if (simple !== void 0) {
3677
+ out += simple;
3678
+ this.i += 2;
3679
+ continue;
3680
+ }
3681
+ 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;
3682
+ if (hex) {
3683
+ out += String.fromCodePoint(parseInt(hex[1], 16));
3684
+ this.i += 1 + hex[0].length;
3685
+ continue;
3686
+ }
3687
+ if (next === "u" || next === "x") this.fail("malformed escape");
3688
+ out += next;
3689
+ this.i += 2;
3690
+ }
3691
+ return this.fail("unterminated string");
3692
+ }
3693
+ parseRegex() {
3694
+ let j = this.i + 1;
3695
+ let inClass = false;
3696
+ while (j < this.src.length) {
3697
+ const c = this.src[j];
3698
+ if (c === "\\") {
3699
+ j += 2;
3700
+ continue;
3701
+ }
3702
+ if (c === "[") inClass = true;
3703
+ else if (c === "]") inClass = false;
3704
+ else if (c === "/" && !inClass) break;
3705
+ j++;
3706
+ }
3707
+ if (j >= this.src.length) this.fail("unterminated regex");
3708
+ const source = this.src.slice(this.i + 1, j);
3709
+ const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
3710
+ this.i = j + 1 + flags.length;
3711
+ return { type: "regex", source, flags };
3712
+ }
3713
+ parseObject() {
3714
+ this.expect("{");
3715
+ const entries = [];
3716
+ for (; ; ) {
3717
+ this.skipSpace();
3718
+ if (this.peek() === "}") {
3719
+ this.i++;
3720
+ break;
3721
+ }
3722
+ const ch = this.peek();
3723
+ const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
3724
+ this.expect(":");
3725
+ entries.push([key, this.parseValue()]);
3726
+ this.skipSpace();
3727
+ if (this.peek() === ",") {
3728
+ this.i++;
3729
+ continue;
3730
+ }
3731
+ this.expect("}");
3732
+ break;
3733
+ }
3734
+ return { type: "object", entries };
3735
+ }
3736
+ };
3737
+ function parseLocatorChain(expr) {
3738
+ const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
3739
+ if (!trimmed) throw new LocatorParseError("empty expression");
3740
+ return new Parser(trimmed).parseTopLevel();
3741
+ }
3742
+ function scanLocatorChain(text) {
3743
+ const parser = new Parser(text);
3744
+ const spans = [];
3745
+ try {
3746
+ const chain = parser.parseChain({ lenient: true, spans });
3747
+ return { chain, spans, end: parser.position };
3748
+ } catch {
3749
+ return null;
3750
+ }
3751
+ }
3752
+ function tryParseLocatorChain(expr) {
3753
+ try {
3754
+ return parseLocatorChain(expr);
3755
+ } catch {
3756
+ return null;
3757
+ }
3758
+ }
3759
+ function parseLeafLocatorCall(expr) {
3760
+ const chain = tryParseLocatorChain(expr);
3761
+ if (!chain) return null;
3762
+ for (let i = chain.calls.length - 1; i >= 0; i--) {
3763
+ if (LOCATING_METHODS.has(chain.calls[i].method)) return chain.calls[i];
3764
+ }
3765
+ return null;
3766
+ }
3767
+ function locatorCallValues(call, opts = {}) {
3768
+ const value = (arg) => {
3769
+ switch (arg.type) {
3770
+ case "string":
3771
+ case "number":
3772
+ case "boolean":
3773
+ return arg.value;
3774
+ case "regex":
3775
+ return opts.regexAsText ? `/${arg.source}/${arg.flags}` : void 0;
3776
+ case "object": {
3777
+ const out = {};
3778
+ for (const [key, v] of arg.entries) {
3779
+ const plain = value(v);
3780
+ if (plain !== void 0) out[key] = plain;
3781
+ }
3782
+ return out;
3783
+ }
3784
+ case "chain":
3785
+ return void 0;
3786
+ }
3787
+ };
3788
+ return call.args.map(value).filter((v) => v !== void 0);
3789
+ }
3790
+
3241
3791
  // ../core/src/locator-methods.ts
3242
3792
  var LOCATOR_BUILDER_METHODS = [
3243
3793
  "getByRole",
@@ -3282,10 +3832,35 @@ function extractLeafSelector(text) {
3282
3832
  if (!first) return null;
3283
3833
  const nl = text.indexOf("\n", first.index);
3284
3834
  const region = text.slice(first.index, nl === -1 ? void 0 : nl);
3835
+ const scanned = readChain(region);
3836
+ if (scanned) {
3837
+ for (let k = scanned.chain.calls.length - 1; k >= 0; k--) {
3838
+ if (LEAF_METHODS.has(scanned.chain.calls[k].method)) {
3839
+ return region.slice(scanned.spans[k].start, scanned.spans[k].end);
3840
+ }
3841
+ }
3842
+ return null;
3843
+ }
3844
+ return lenientLeaf(region);
3845
+ }
3846
+ var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
3847
+ function skipQuoted(text, i) {
3848
+ const quote = text[i];
3849
+ for (let j = i + 1; j < text.length; j++) {
3850
+ if (text[j] === "\\") j++;
3851
+ else if (text[j] === quote) return j + 1;
3852
+ }
3853
+ return text.length;
3854
+ }
3855
+ function lenientLeaf(region) {
3285
3856
  let depth = 0;
3286
3857
  let leafStart = -1;
3287
3858
  for (let i = 0; i < region.length; i++) {
3288
3859
  const ch = region[i];
3860
+ if (ch === "'" || ch === '"' || ch === "`") {
3861
+ i = skipQuoted(region, i) - 1;
3862
+ continue;
3863
+ }
3289
3864
  if (ch === "(") {
3290
3865
  depth++;
3291
3866
  continue;
@@ -3308,7 +3883,9 @@ function extractLeafSelector(text) {
3308
3883
  depth = 0;
3309
3884
  for (let i = leafStart; i < region.length; i++) {
3310
3885
  const ch = region[i];
3311
- if (ch === "(") {
3886
+ if (ch === "'" || ch === '"' || ch === "`") {
3887
+ i = skipQuoted(region, i) - 1;
3888
+ } else if (ch === "(") {
3312
3889
  depth++;
3313
3890
  } else if (ch === ")") {
3314
3891
  depth--;
@@ -3322,11 +3899,15 @@ function extractLocatorChain(text) {
3322
3899
  if (!first) return null;
3323
3900
  const nl = text.indexOf("\n", first.index);
3324
3901
  const region = text.slice(first.index, nl === -1 ? void 0 : nl);
3902
+ const scanned = readChain(region);
3903
+ if (scanned) return region.slice(0, scanned.end);
3325
3904
  let depth = 0;
3326
3905
  let end = -1;
3327
3906
  for (let i = 0; i < region.length; i++) {
3328
3907
  const ch = region[i];
3329
- if (ch === "(") {
3908
+ if (ch === "'" || ch === '"' || ch === "`") {
3909
+ i = skipQuoted(region, i) - 1;
3910
+ } else if (ch === "(") {
3330
3911
  depth++;
3331
3912
  } else if (ch === ")") {
3332
3913
  if (depth > 0) depth--;
@@ -3341,6 +3922,12 @@ function extractLocatorChain(text) {
3341
3922
  if (end === -1) return region.slice(0, 80);
3342
3923
  return region.slice(0, end);
3343
3924
  }
3925
+ function readChain(region) {
3926
+ const scanned = scanLocatorChain(region);
3927
+ if (!scanned) return null;
3928
+ const link = /^\s*\.\s*(\w+)\(/.exec(region.slice(scanned.end));
3929
+ return link && CHAIN_LINK_METHODS.has(link[1]) ? null : scanned;
3930
+ }
3344
3931
  function extractTopFrame(text) {
3345
3932
  const frameRe = /^\s+at (?:.*? \()?([^()\s][^()]*?):(\d+):(\d+)\)?\s*$/gm;
3346
3933
  let m;
@@ -4064,7 +4651,7 @@ function testLocation(test) {
4064
4651
  return `${testFile(test)}:${test.location.line}:${test.location.column}`;
4065
4652
  }
4066
4653
  function testFile(test) {
4067
- return path15.relative(process.cwd(), test.location.file).split(path15.sep).join("/");
4654
+ return path17.relative(process.cwd(), test.location.file).split(path17.sep).join("/");
4068
4655
  }
4069
4656
  var PiwiDashboardReporter = class _PiwiDashboardReporter {
4070
4657
  constructor(rawOptions = {}) {
@@ -4181,6 +4768,7 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4181
4768
  );
4182
4769
  }
4183
4770
  this.metadata = this.metadataCollector.collect(config, suite, this.options);
4771
+ if (isProbeMode()) this.metadata = probeRunMetadata(this.metadata);
4184
4772
  this.plannedTests = suite.allTests();
4185
4773
  const pwShard = config.shard;
4186
4774
  if (pwShard?.total && pwShard.total > 1) {
@@ -4219,18 +4807,27 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4219
4807
  }
4220
4808
  }
4221
4809
  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"]);
4810
+ /** Step categories streamed live while the run executes. */
4811
+ this.LIVE_STEP_CATEGORIES = /* @__PURE__ */ new Set(["hook", "fixture", "pw:api", "expect"]);
4812
+ }
4813
+ /**
4814
+ * Whether a step streams live: its category is in `LIVE_STEP_CATEGORIES`
4815
+ * and no ancestor is an `expect` step. The steps inside an assertion are its
4816
+ * polling — every `expect.poll` attempt and `toPass` retry — not a step a
4817
+ * human watches; the meaningful readout is the assertion around them.
4818
+ */
4819
+ static isLiveStep(step) {
4820
+ if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(step.category)) return false;
4821
+ for (let parent = step.parent; parent; parent = parent.parent) {
4822
+ if (parent.category === "expect") return false;
4823
+ }
4824
+ return true;
4228
4825
  }
4229
4826
  /** Playwright reporter hook: called when a step (including hook/fixture) begins */
4230
4827
  onStepBegin(test, _result, step) {
4231
4828
  if (!this.enabled || !this.streamManager) return;
4829
+ if (!_PiwiDashboardReporter.isLiveStep(step)) return;
4232
4830
  const cat = step.category;
4233
- if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
4234
4831
  const event = {
4235
4832
  type: "step-begin",
4236
4833
  title: step.title,
@@ -4246,8 +4843,8 @@ var PiwiDashboardReporter = class _PiwiDashboardReporter {
4246
4843
  /** Playwright reporter hook: called when a step (including hook/fixture) ends */
4247
4844
  onStepEnd(test, _result, step) {
4248
4845
  if (!this.enabled || !this.streamManager) return;
4846
+ if (!_PiwiDashboardReporter.isLiveStep(step)) return;
4249
4847
  const cat = step.category;
4250
- if (!_PiwiDashboardReporter.LIVE_STEP_CATEGORIES.has(cat)) return;
4251
4848
  const workerIndex = workerIndexOf(_result);
4252
4849
  const startedAt = step.startTime instanceof Date ? step.startTime.getTime() : null;
4253
4850
  const event = {
@@ -4537,7 +5134,7 @@ async function resolveSelection(options = {}) {
4537
5134
  }
4538
5135
 
4539
5136
  // src/internal/capture/capture-fixtures.ts
4540
- var import_node_zlib = require("zlib");
5137
+ var import_node_zlib2 = require("zlib");
4541
5138
 
4542
5139
  // ../picker-dom/src/probe.ts
4543
5140
  function probeElementAttrs(el, arg) {
@@ -5895,7 +6492,7 @@ function mergeCandidates(base, extra) {
5895
6492
  }
5896
6493
 
5897
6494
  // src/internal/capture/locator-healing.ts
5898
- var path16 = __toESM(require("path"));
6495
+ var path18 = __toESM(require("path"));
5899
6496
 
5900
6497
  // ../core/src/locator-fingerprint.ts
5901
6498
  var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
@@ -6265,16 +6862,318 @@ function captureCallerLocation(stack = new Error().stack ?? "") {
6265
6862
  }
6266
6863
  let rel = file;
6267
6864
  try {
6268
- rel = path16.relative(process.cwd(), file);
6865
+ rel = path18.relative(process.cwd(), file);
6269
6866
  } catch {
6270
6867
  }
6271
- rel = rel.split(path16.sep).join("/");
6868
+ rel = rel.split(path18.sep).join("/");
6272
6869
  if (rel.startsWith("./")) rel = rel.slice(2);
6273
6870
  return `${rel}:${m[3]}:${m[4]}`;
6274
6871
  }
6275
6872
  return null;
6276
6873
  }
6277
6874
 
6875
+ // src/internal/capture/page-inventory.ts
6876
+ var PAGE_INVENTORY_MAX_ENTRIES = 500;
6877
+ function collectPageInventoryInPage(maxEntries) {
6878
+ const g = globalThis;
6879
+ const doc = g.document;
6880
+ if (!doc || !g.location) return null;
6881
+ const clean = (s) => (s || "").replace(/\s+/g, " ").trim().slice(0, 120);
6882
+ const roleOf = (el) => {
6883
+ const explicit = el.getAttribute("role");
6884
+ if (explicit) return explicit.toLowerCase();
6885
+ const tag = (el.tagName || "").toLowerCase();
6886
+ if (tag === "a") return el.getAttribute("href") != null ? "link" : "generic";
6887
+ if (tag === "button") return "button";
6888
+ if (tag === "select") return el.hasAttribute("multiple") ? "listbox" : "combobox";
6889
+ if (tag === "textarea") return "textbox";
6890
+ if (tag === "input") {
6891
+ const type = (el.getAttribute("type") || "text").toLowerCase();
6892
+ const map = {
6893
+ checkbox: "checkbox",
6894
+ radio: "radio",
6895
+ button: "button",
6896
+ submit: "button",
6897
+ reset: "button",
6898
+ image: "button",
6899
+ range: "slider",
6900
+ search: "searchbox",
6901
+ email: "textbox",
6902
+ tel: "textbox",
6903
+ url: "textbox",
6904
+ number: "spinbutton"
6905
+ };
6906
+ return map[type] || "textbox";
6907
+ }
6908
+ return "generic";
6909
+ };
6910
+ const holdsValue = (el, role) => {
6911
+ const tag = (el.tagName || "").toLowerCase();
6912
+ if (tag === "select" || tag === "textarea" || tag === "input") return true;
6913
+ if (el.isContentEditable === true) return true;
6914
+ const editable = el.getAttribute("contenteditable");
6915
+ if (editable === "" || editable === "true") return true;
6916
+ return role === "textbox" || role === "combobox" || role === "listbox" || role === "searchbox" || role === "spinbutton";
6917
+ };
6918
+ const labelText = (el) => {
6919
+ const labels = el.labels;
6920
+ if (!labels || labels.length === 0) return "";
6921
+ let text = "";
6922
+ for (const label of labels) text += ` ${label.textContent || ""}`;
6923
+ return text;
6924
+ };
6925
+ const labelledByText = (el) => {
6926
+ const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
6927
+ if (ids.length === 0) return "";
6928
+ let text = "";
6929
+ for (const id of ids) {
6930
+ const ref = doc.getElementById(id);
6931
+ if (ref) text += ` ${ref.textContent || ""}`;
6932
+ }
6933
+ return text;
6934
+ };
6935
+ const nameOf = (el) => {
6936
+ const role = roleOf(el);
6937
+ const chain = [el.getAttribute("aria-label"), labelledByText(el), labelText(el)];
6938
+ if (!holdsValue(el, role)) chain.push(el.textContent);
6939
+ chain.push(el.getAttribute("placeholder"), el.getAttribute("title"), el.getAttribute("alt"));
6940
+ return clean(chain.find((s) => s && String(s).trim()) || "");
6941
+ };
6942
+ const cleanHref = (href) => {
6943
+ const cut = href.search(/[?#]/);
6944
+ return cut >= 0 ? href.slice(0, cut) : href;
6945
+ };
6946
+ const controls = [];
6947
+ const links = [];
6948
+ const seenC = /* @__PURE__ */ new Set();
6949
+ const seenL = /* @__PURE__ */ new Set();
6950
+ let budget = maxEntries;
6951
+ 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"]';
6952
+ for (const el of Array.from(doc.querySelectorAll(controlSelector))) {
6953
+ if (budget <= 0) break;
6954
+ const role = roleOf(el);
6955
+ const name = nameOf(el);
6956
+ if (!name) continue;
6957
+ const key = `${role}\0${name}`;
6958
+ if (seenC.has(key)) continue;
6959
+ seenC.add(key);
6960
+ controls.push({ role, name });
6961
+ budget--;
6962
+ }
6963
+ for (const el of Array.from(doc.querySelectorAll("a[href]"))) {
6964
+ if (budget <= 0) break;
6965
+ const name = nameOf(el);
6966
+ const href = cleanHref(el.getAttribute("href") || "");
6967
+ if (!name) continue;
6968
+ const key = `${name}\0${href}`;
6969
+ if (seenL.has(key)) continue;
6970
+ seenL.add(key);
6971
+ links.push({ name, href });
6972
+ budget--;
6973
+ }
6974
+ return { url: g.location.href, controls, links };
6975
+ }
6976
+ function capPageInventory(inventory, max = PAGE_INVENTORY_MAX_ENTRIES) {
6977
+ const controls = inventory.controls.slice(0, Math.max(0, max));
6978
+ const links = inventory.links.slice(0, Math.max(0, max - controls.length));
6979
+ return { url: inventory.url, controls, links, capturedAt: inventory.capturedAt };
6980
+ }
6981
+ function inventoryPageKey(url) {
6982
+ try {
6983
+ const u = new URL(url);
6984
+ return `${u.origin}${u.pathname}`;
6985
+ } catch {
6986
+ return null;
6987
+ }
6988
+ }
6989
+
6990
+ // src/internal/probe/interception.ts
6991
+ var import_node_zlib = require("zlib");
6992
+
6993
+ // src/internal/probe/faults.ts
6994
+ var PROBE_FAULTS = ["status-500", "empty-body", "drop-field", "stale-value", "slow"];
6995
+ var SERVER_PROBE_FAULTS = [
6996
+ "throw",
6997
+ "status",
6998
+ "delay",
6999
+ "dependency",
7000
+ "data",
7001
+ "extreme",
7002
+ "replay",
7003
+ "auth",
7004
+ "slow-first"
7005
+ ];
7006
+ function isFaultAllowed(level, fault) {
7007
+ const list = level === "server" ? SERVER_PROBE_FAULTS : PROBE_FAULTS;
7008
+ return list.includes(fault);
7009
+ }
7010
+ var SLOW_FAULT_DELAY_MS = 5e3;
7011
+ function dropOneField(body) {
7012
+ let parsed;
7013
+ try {
7014
+ parsed = JSON.parse(body);
7015
+ } catch {
7016
+ return body;
7017
+ }
7018
+ if (Array.isArray(parsed)) {
7019
+ let key;
7020
+ for (const el of parsed) {
7021
+ if (el && typeof el === "object" && !Array.isArray(el)) {
7022
+ key ??= Object.keys(el)[0];
7023
+ if (key) delete el[key];
7024
+ }
7025
+ }
7026
+ return JSON.stringify(parsed);
7027
+ }
7028
+ if (parsed && typeof parsed === "object") {
7029
+ const obj = parsed;
7030
+ const key = Object.keys(obj)[0];
7031
+ if (key) delete obj[key];
7032
+ return JSON.stringify(obj);
7033
+ }
7034
+ return body;
7035
+ }
7036
+ function computeFault(fault, input) {
7037
+ const base = {
7038
+ status: input.status,
7039
+ body: input.body,
7040
+ contentType: input.contentType ?? null,
7041
+ delayMs: 0,
7042
+ changed: false
7043
+ };
7044
+ switch (fault) {
7045
+ case "status-500":
7046
+ return { ...base, status: 500, changed: input.status !== 500 };
7047
+ case "empty-body":
7048
+ return { ...base, body: "", changed: input.body.length > 0 };
7049
+ case "drop-field": {
7050
+ const body = dropOneField(input.body);
7051
+ return { ...base, body, changed: body !== input.body };
7052
+ }
7053
+ case "stale-value": {
7054
+ const body = input.priorBody ?? input.body;
7055
+ return { ...base, body, changed: body !== input.body };
7056
+ }
7057
+ case "slow":
7058
+ return { ...base, delayMs: SLOW_FAULT_DELAY_MS, changed: true };
7059
+ default:
7060
+ return base;
7061
+ }
7062
+ }
7063
+
7064
+ // src/internal/probe/sign.ts
7065
+ var import_node_crypto = require("crypto");
7066
+ function signProbeMessage(secret, nonce, ts, specJson) {
7067
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(`${nonce}.${ts}.${specJson}`).digest("hex");
7068
+ }
7069
+ function buildProbeHeader(secret, spec, now = Date.now()) {
7070
+ const nonce = (0, import_node_crypto.randomBytes)(8).toString("hex");
7071
+ const specJson = JSON.stringify(spec);
7072
+ const sig = signProbeMessage(secret, nonce, now, specJson);
7073
+ return Buffer.from(JSON.stringify({ nonce, ts: now, specJson, sig }), "utf8").toString("base64");
7074
+ }
7075
+
7076
+ // src/internal/probe/interception.ts
7077
+ function parseProbeTrace(traceHeader) {
7078
+ const none = { appliedFault: null, serverError: false };
7079
+ if (!traceHeader) return none;
7080
+ try {
7081
+ const spans = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf8"));
7082
+ if (!Array.isArray(spans)) return none;
7083
+ const root = spans.find((s) => s && typeof s === "object" && !s.parentId);
7084
+ if (!root) return none;
7085
+ const attrs = root.attrs ?? {};
7086
+ const appliedRaw = attrs["piwi.probe.applied"];
7087
+ const status = Number(attrs["http.status_code"]) || 0;
7088
+ return {
7089
+ appliedFault: appliedRaw != null ? String(appliedRaw) : null,
7090
+ serverError: root.status === "error" || status >= 500
7091
+ };
7092
+ } catch {
7093
+ return none;
7094
+ }
7095
+ }
7096
+ function fulfillHeaders(responseHeaders, contentType) {
7097
+ const headers = { ...responseHeaders };
7098
+ delete headers["content-length"];
7099
+ delete headers["content-encoding"];
7100
+ if (contentType) headers["content-type"] = contentType;
7101
+ return headers;
7102
+ }
7103
+ function originOf(url) {
7104
+ try {
7105
+ const origin = new URL(url).origin;
7106
+ return origin && origin !== "null" ? origin : null;
7107
+ } catch {
7108
+ return null;
7109
+ }
7110
+ }
7111
+ async function installProbeInterception(page, item) {
7112
+ if (!isFaultAllowed(item.level ?? "client", item.fault)) {
7113
+ return { applied: () => false, appliedFault: () => null, serverError: () => false };
7114
+ }
7115
+ const state = createProbeState();
7116
+ let applied = false;
7117
+ let appliedFault = null;
7118
+ let serverError = false;
7119
+ let appOrigin = null;
7120
+ const probeSecret = process.env[PIWI_PROBE_ENV.secret];
7121
+ page.on("framenavigated", (frame) => {
7122
+ if (frame === page.mainFrame()) {
7123
+ markNavigated(state);
7124
+ const origin = originOf(page.url());
7125
+ if (origin) appOrigin = origin;
7126
+ }
7127
+ });
7128
+ await page.route("**/*", async (route) => {
7129
+ const request = route.request();
7130
+ if (applied || !requestMatchesRoute(request.method(), request.url(), item.routeKey) || !shouldMutate(state, item)) {
7131
+ await route.fallback();
7132
+ return;
7133
+ }
7134
+ if (item.level === "server") {
7135
+ if (!probeSecret || appOrigin !== null && originOf(request.url()) !== appOrigin) {
7136
+ await route.fallback();
7137
+ return;
7138
+ }
7139
+ const spec = { route: item.routeKey, fault: item.fault, nth: item.nth };
7140
+ if (item.dependency) spec.dependency = item.dependency;
7141
+ try {
7142
+ const response = await route.fetch({
7143
+ headers: { ...request.headers(), "x-piwi-probe": buildProbeHeader(probeSecret, spec) }
7144
+ });
7145
+ const trace = parseProbeTrace(response.headers()["x-piwi-trace"]);
7146
+ appliedFault = trace.appliedFault;
7147
+ serverError = trace.serverError;
7148
+ applied = trace.appliedFault != null && trace.appliedFault.split(":", 1)[0] === item.fault;
7149
+ await route.fulfill({ response });
7150
+ } catch {
7151
+ applied = false;
7152
+ await route.fallback();
7153
+ }
7154
+ return;
7155
+ }
7156
+ try {
7157
+ const response = await route.fetch();
7158
+ const body = await response.text();
7159
+ const out = computeFault(item.fault, {
7160
+ status: response.status(),
7161
+ body,
7162
+ contentType: response.headers()["content-type"] ?? null
7163
+ });
7164
+ applied = out.changed;
7165
+ if (applied) appliedFault = item.fault;
7166
+ if (out.delayMs > 0) await new Promise((resolve5) => setTimeout(resolve5, out.delayMs));
7167
+ const headers = fulfillHeaders(response.headers(), out.contentType);
7168
+ await route.fulfill({ status: out.status, body: out.body, headers });
7169
+ } catch {
7170
+ applied = false;
7171
+ await route.fallback();
7172
+ }
7173
+ });
7174
+ return { applied: () => applied, appliedFault: () => appliedFault, serverError: () => serverError };
7175
+ }
7176
+
6278
7177
  // src/internal/capture/inspect-on-failure.ts
6279
7178
  function isCi(ci) {
6280
7179
  return ci !== void 0 && ci !== "" && ci !== "false";
@@ -6311,88 +7210,11 @@ function inspectionGateFromTestInfo(testInfo, enabled = process.env.PIWI_INSPECT
6311
7210
  }
6312
7211
 
6313
7212
  // src/internal/capture/pick-on-failure.ts
6314
- var path17 = __toESM(require("path"));
7213
+ var path19 = __toESM(require("path"));
6315
7214
  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
7215
  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()) };
7216
+ const leaf = parseLeafLocatorCall(rawExpr);
7217
+ return leaf ? { method: leaf.method, args: locatorCallValues(leaf) } : null;
6396
7218
  }
6397
7219
  function deriveFailedLocator(testInfo) {
6398
7220
  const info = testInfo;
@@ -6405,7 +7227,7 @@ ${err.stack ?? ""}`.replace(ANSI_RE2, "");
6405
7227
  const parsed = parseLeafLocatorExpression(line[1].trim());
6406
7228
  if (!parsed) continue;
6407
7229
  const loc = err.location;
6408
- const location = loc ? `${path17.relative(process.cwd(), loc.file).split(path17.sep).join("/")}:${loc.line}:${loc.column}` : null;
7230
+ const location = loc ? `${path19.relative(process.cwd(), loc.file).split(path19.sep).join("/")}:${loc.line}:${loc.column}` : null;
6409
7231
  return { method: parsed.method, args: parsed.args, location };
6410
7232
  }
6411
7233
  return null;
@@ -6599,6 +7421,9 @@ function createSink() {
6599
7421
  stashedPageState: null,
6600
7422
  stashedAria: null,
6601
7423
  stashedAriaJson: null,
7424
+ pageInventories: [],
7425
+ probeItem: null,
7426
+ probeInterception: null,
6602
7427
  pickOffered: false,
6603
7428
  userPick: null
6604
7429
  };
@@ -6775,6 +7600,62 @@ async function readPageState(page) {
6775
7600
  return null;
6776
7601
  }
6777
7602
  }
7603
+ var inventoriedPageKeys = /* @__PURE__ */ new Set();
7604
+ var PAGE_INVENTORY_IN_PAGE_CAP = 2e3;
7605
+ var PAGE_INVENTORY_EVAL_TIMEOUT_MS = 2e3;
7606
+ var MAX_PAGE_INVENTORY_WINDOWS = 64;
7607
+ function withTimeout(promise, ms) {
7608
+ return new Promise((resolve5, reject) => {
7609
+ const timer = setTimeout(() => reject(new Error("page inventory read timed out")), ms);
7610
+ promise.then(
7611
+ (value) => {
7612
+ clearTimeout(timer);
7613
+ resolve5(value);
7614
+ },
7615
+ (error) => {
7616
+ clearTimeout(timer);
7617
+ reject(error);
7618
+ }
7619
+ );
7620
+ });
7621
+ }
7622
+ async function readPageInventory(page) {
7623
+ try {
7624
+ const inventory = await withTimeout(
7625
+ page.evaluate(collectPageInventoryInPage, PAGE_INVENTORY_IN_PAGE_CAP),
7626
+ PAGE_INVENTORY_EVAL_TIMEOUT_MS
7627
+ );
7628
+ return inventory ? { ...inventory, capturedAt: Date.now() } : null;
7629
+ } catch {
7630
+ return null;
7631
+ }
7632
+ }
7633
+ async function recordPageWindow(sink, page) {
7634
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7635
+ let url;
7636
+ try {
7637
+ url = page.url();
7638
+ } catch {
7639
+ return;
7640
+ }
7641
+ if (!url || url.startsWith("about:")) return;
7642
+ const key = inventoryPageKey(url);
7643
+ const capturedAt = Date.now();
7644
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7645
+ if (key && !inventoriedPageKeys.has(key)) {
7646
+ inventoriedPageKeys.add(key);
7647
+ const content = await readPageInventory(page);
7648
+ if (sink.pageInventories.length >= MAX_PAGE_INVENTORY_WINDOWS) return;
7649
+ sink.pageInventories.push({
7650
+ url: content?.url ?? url,
7651
+ controls: content?.controls ?? [],
7652
+ links: content?.links ?? [],
7653
+ capturedAt
7654
+ });
7655
+ return;
7656
+ }
7657
+ sink.pageInventories.push({ url, controls: [], links: [], capturedAt });
7658
+ }
6778
7659
  async function stashPageState(sink, closing) {
6779
7660
  const page = sink.lastActivePage;
6780
7661
  if (!page || isPageClosed(page)) return;
@@ -6799,6 +7680,9 @@ async function stashPageState(sink, closing) {
6799
7680
  } else if (status === "passed" && process.env.PIWI_SAMPLE_ARIA_ON_PASS !== "false" && sink.testInfo && isDueForAriaSample(sink.testInfo)) {
6800
7681
  await sampleAria();
6801
7682
  }
7683
+ if (status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
7684
+ await recordPageWindow(sink, page);
7685
+ }
6802
7686
  }
6803
7687
  async function maybeOpenPicker(sink, closing) {
6804
7688
  if (sink.pickOffered) return;
@@ -7042,6 +7926,13 @@ function wrapFrameLocator(page, frameLocator) {
7042
7926
  function instrumentPage(page) {
7043
7927
  if (!page || INSTRUMENTED_PAGES.has(page)) return;
7044
7928
  INSTRUMENTED_PAGES.add(page);
7929
+ const probeSink = currentSink;
7930
+ if (probeSink?.probeItem && !probeSink.probeInterception) {
7931
+ void installProbeInterception(page, probeSink.probeItem).then((interception) => {
7932
+ probeSink.probeInterception = interception;
7933
+ }).catch(() => {
7934
+ });
7935
+ }
7045
7936
  const ctx = pageContext(page);
7046
7937
  if (ctx) instrumentContext(ctx);
7047
7938
  if (typeof page.close === "function") {
@@ -7077,6 +7968,14 @@ function instrumentPage(page) {
7077
7968
  page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
7078
7969
  }
7079
7970
  }
7971
+ if (typeof page.on === "function" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
7972
+ page.on("framenavigated", (frame) => {
7973
+ const sink = currentSink;
7974
+ if (!sink || frame !== page.mainFrame()) return;
7975
+ void recordPageWindow(sink, page).catch(() => {
7976
+ });
7977
+ });
7978
+ }
7080
7979
  page.on("console", (msg) => {
7081
7980
  const sink = currentSink;
7082
7981
  if (!sink) return;
@@ -7136,14 +8035,14 @@ function instrumentPage(page) {
7136
8035
  const logHeader = headers["x-piwi-logs"];
7137
8036
  if (logHeader) {
7138
8037
  try {
7139
- entry.serverLogs = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
8038
+ entry.serverLogs = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(logHeader, "base64")).toString("utf-8"));
7140
8039
  } catch {
7141
8040
  }
7142
8041
  }
7143
8042
  const traceHeader = headers["x-piwi-trace"];
7144
8043
  if (traceHeader && process.env.PIWI_CAPTURE_SERVER_TRACES !== "false") {
7145
8044
  try {
7146
- entry.serverTraces = JSON.parse((0, import_node_zlib.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
8045
+ entry.serverTraces = JSON.parse((0, import_node_zlib2.gunzipSync)(Buffer.from(traceHeader, "base64")).toString("utf-8"));
7147
8046
  } catch {
7148
8047
  }
7149
8048
  }
@@ -7328,6 +8227,16 @@ async function flushSink(sink, testInfo) {
7328
8227
  });
7329
8228
  }
7330
8229
  }
8230
+ if (testInfo.status === "passed" && process.env.PIWI_CAPTURE_PAGE_INVENTORY === "true") {
8231
+ if (pageReadable) await recordPageWindow(sink, page);
8232
+ const windows = sink.pageInventories.slice(0, MAX_PAGE_INVENTORY_WINDOWS).map((inv) => capPageInventory(inv));
8233
+ if (windows.length > 0) {
8234
+ await testInfo.attach(ATTACHMENT_NAMES.pageInventory, {
8235
+ contentType: "application/json",
8236
+ body: Buffer.from(JSON.stringify(windows))
8237
+ });
8238
+ }
8239
+ }
7331
8240
  }
7332
8241
  var piwiFixtures = {
7333
8242
  // Worker-scoped: patch the shared browser so every page/context created from
@@ -7353,11 +8262,36 @@ var piwiFixtures = {
7353
8262
  async ({}, use, testInfo) => {
7354
8263
  const sink = createSink();
7355
8264
  sink.testInfo = testInfo;
8265
+ if (isProbeMode())
8266
+ sink.probeItem = probeItemForTest({
8267
+ title: testInfo.title,
8268
+ file: testInfo.file,
8269
+ titlePath: testInfo.titlePath
8270
+ });
7356
8271
  currentSink = sink;
7357
8272
  try {
7358
8273
  await use();
7359
8274
  } finally {
7360
8275
  currentSink = null;
8276
+ if (sink.probeItem) {
8277
+ const applied = sink.probeInterception?.applied() ?? false;
8278
+ const level = sink.probeItem.level ?? "client";
8279
+ const handled = level === "server" ? classifyProbeHandled({
8280
+ consoleErrors: sink.consoleEntries.filter((e) => e.type === "error").length,
8281
+ dialogs: sink.dialogs.length,
8282
+ backendError: sink.probeInterception?.serverError() ?? false
8283
+ }) : "n/a";
8284
+ recordProbeOutcome({
8285
+ testCaseId: sink.probeItem.testCaseId,
8286
+ routeKey: sink.probeItem.routeKey,
8287
+ fault: sink.probeItem.fault,
8288
+ applied,
8289
+ outcome: outcomeFromStatus(testInfo.status, applied),
8290
+ level,
8291
+ dependency: sink.probeItem.dependency ?? null,
8292
+ handled
8293
+ });
8294
+ }
7361
8295
  await flushSink(sink, testInfo);
7362
8296
  }
7363
8297
  },
@@ -7369,13 +8303,13 @@ function extendPiwiFixtures(test) {
7369
8303
  }
7370
8304
 
7371
8305
  // src/internal/ai/ai-fixtures.ts
7372
- var fs16 = __toESM(require("fs"));
7373
- var path20 = __toESM(require("path"));
8306
+ var fs18 = __toESM(require("fs"));
8307
+ var path22 = __toESM(require("path"));
7374
8308
  var import_test = require("@playwright/test");
7375
8309
 
7376
8310
  // src/internal/ai/artifact.ts
7377
- var fs15 = __toESM(require("fs"));
7378
- var path18 = __toESM(require("path"));
8311
+ var fs17 = __toESM(require("fs"));
8312
+ var path20 = __toESM(require("path"));
7379
8313
  var ARTIFACT_VERSION = 1;
7380
8314
  var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
7381
8315
  var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
@@ -7468,7 +8402,7 @@ function parseEntry(text) {
7468
8402
  function readEntry(file) {
7469
8403
  let text;
7470
8404
  try {
7471
- text = fs15.readFileSync(file, "utf8");
8405
+ text = fs17.readFileSync(file, "utf8");
7472
8406
  } catch (error) {
7473
8407
  if (error.code === "ENOENT") return null;
7474
8408
  throw error;
@@ -7478,28 +8412,28 @@ function readEntry(file) {
7478
8412
  function writeEntry(file, entry) {
7479
8413
  const canonical = serializeEntry(entry);
7480
8414
  return withEntryLock(file, () => {
7481
- if (fs15.existsSync(file) && fs15.readFileSync(file, "utf8") === canonical) return { written: false };
7482
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
8415
+ if (fs17.existsSync(file) && fs17.readFileSync(file, "utf8") === canonical) return { written: false };
8416
+ fs17.mkdirSync(path20.dirname(file), { recursive: true });
7483
8417
  const tmp = `${file}.${process.pid}.tmp`;
7484
- fs15.writeFileSync(tmp, canonical);
7485
- fs15.renameSync(tmp, file);
8418
+ fs17.writeFileSync(tmp, canonical);
8419
+ fs17.renameSync(tmp, file);
7486
8420
  return { written: true };
7487
8421
  });
7488
8422
  }
7489
8423
  function withEntryLock(file, fn) {
7490
8424
  const lock = `${file}.lock`;
7491
- fs15.mkdirSync(path18.dirname(file), { recursive: true });
8425
+ fs17.mkdirSync(path20.dirname(file), { recursive: true });
7492
8426
  const deadline = Date.now() + 5e3;
7493
8427
  for (; ; ) {
7494
8428
  try {
7495
- const fd = fs15.openSync(lock, "wx");
7496
- fs15.closeSync(fd);
8429
+ const fd = fs17.openSync(lock, "wx");
8430
+ fs17.closeSync(fd);
7497
8431
  break;
7498
8432
  } catch (error) {
7499
8433
  if (error.code !== "EEXIST") throw error;
7500
8434
  if (Date.now() > deadline) {
7501
8435
  try {
7502
- fs15.unlinkSync(lock);
8436
+ fs17.unlinkSync(lock);
7503
8437
  } catch {
7504
8438
  }
7505
8439
  }
@@ -7509,7 +8443,7 @@ function withEntryLock(file, fn) {
7509
8443
  return fn();
7510
8444
  } finally {
7511
8445
  try {
7512
- fs15.unlinkSync(lock);
8446
+ fs17.unlinkSync(lock);
7513
8447
  } catch {
7514
8448
  }
7515
8449
  }
@@ -7517,7 +8451,7 @@ function withEntryLock(file, fn) {
7517
8451
 
7518
8452
  // src/internal/ai/keys.ts
7519
8453
  var crypto3 = __toESM(require("crypto"));
7520
- var path19 = __toESM(require("path"));
8454
+ var path21 = __toESM(require("path"));
7521
8455
  var DEFAULT_AI_DIR = "__piwi__";
7522
8456
  function normalizeTemplate(template) {
7523
8457
  return template.trim().replace(/\s+/g, " ").toLowerCase();
@@ -7531,13 +8465,13 @@ function slug(text, maxLength = 40) {
7531
8465
  return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
7532
8466
  }
7533
8467
  function entryDir(specFile, dir = DEFAULT_AI_DIR) {
7534
- return path19.join(path19.dirname(specFile), dir, path19.basename(specFile));
8468
+ return path21.join(path21.dirname(specFile), dir, path21.basename(specFile));
7535
8469
  }
7536
8470
  function entryPath(params) {
7537
8471
  const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
7538
8472
  const hash = hashTemplate(basis, params.ordinal ?? 0);
7539
8473
  const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
7540
- return path19.join(entryDir(params.specFile, params.dir), name);
8474
+ return path21.join(entryDir(params.specFile, params.dir), name);
7541
8475
  }
7542
8476
  function parseLocation(location) {
7543
8477
  const match = /:(\d+):(\d+)$/.exec(location);
@@ -7991,7 +8925,7 @@ function testIdentity(testInfo) {
7991
8925
  return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
7992
8926
  }
7993
8927
  function relativeToCwd(file) {
7994
- return path20.relative(process.cwd(), file).split(path20.sep).join("/");
8928
+ return path22.relative(process.cwd(), file).split(path22.sep).join("/");
7995
8929
  }
7996
8930
  function recordIntents(intents, entry) {
7997
8931
  const add = (locator, kind) => {
@@ -8018,7 +8952,7 @@ function createAiApi(page, testInfo, config, used, intents) {
8018
8952
  const readSource = () => {
8019
8953
  if (source === null) {
8020
8954
  try {
8021
- source = fs16.readFileSync(testInfo.file, "utf8");
8955
+ source = fs18.readFileSync(testInfo.file, "utf8");
8022
8956
  } catch {
8023
8957
  source = "";
8024
8958
  }