@piwitests/reporter 0.23.0 → 0.25.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
@@ -33,7 +33,9 @@ __export(index_exports, {
33
33
  PiwiDashboardReporter: () => PiwiDashboardReporter,
34
34
  createGlobalSetup: () => createGlobalSetup,
35
35
  default: () => index_default,
36
+ extendPiwiAi: () => extendPiwiAi,
36
37
  extendPiwiFixtures: () => extendPiwiFixtures,
38
+ piwiAiFixtures: () => piwiAiFixtures,
37
39
  piwiFixtures: () => piwiFixtures,
38
40
  wrapConfig: () => wrapConfig
39
41
  });
@@ -103,7 +105,15 @@ var PIWI_ENV_KEYS = {
103
105
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
104
106
  inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
105
107
  pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
106
- outputFile: "PIWI_OUTPUT_FILE"
108
+ outputFile: "PIWI_OUTPUT_FILE",
109
+ aiMode: "PIWI_AI",
110
+ aiDir: "PIWI_AI_DIR",
111
+ aiOnMiss: "PIWI_AI_ON_MISS",
112
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
113
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
114
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
115
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
116
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
107
117
  };
108
118
  var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
109
119
  function readBool(val) {
@@ -186,6 +196,18 @@ function applyOptionsToEnv(options) {
186
196
  if (options.inspectOnFailure !== void 0) env[PIWI_ENV_KEYS.inspectOnFailure] = String(options.inspectOnFailure);
187
197
  if (options.pickLocatorOnFailure !== void 0)
188
198
  env[PIWI_ENV_KEYS.pickLocatorOnFailure] = String(options.pickLocatorOnFailure);
199
+ if (options.ai?.mode !== void 0) env[PIWI_ENV_KEYS.aiMode] = options.ai.mode;
200
+ if (options.ai?.dir !== void 0) env[PIWI_ENV_KEYS.aiDir] = options.ai.dir;
201
+ if (options.ai?.onMiss !== void 0) env[PIWI_ENV_KEYS.aiOnMiss] = options.ai.onMiss;
202
+ if (options.ai?.maxSteps !== void 0) env[PIWI_ENV_KEYS.aiMaxSteps] = String(options.ai.maxSteps);
203
+ if (options.ai?.maxSnapshotChars !== void 0)
204
+ env[PIWI_ENV_KEYS.aiMaxSnapshotChars] = String(options.ai.maxSnapshotChars);
205
+ if (options.ai?.optionalProbeTimeout !== void 0)
206
+ env[PIWI_ENV_KEYS.aiOptionalProbeTimeout] = String(options.ai.optionalProbeTimeout);
207
+ if (options.ai?.responseWaitTimeout !== void 0)
208
+ env[PIWI_ENV_KEYS.aiResponseWaitTimeout] = String(options.ai.responseWaitTimeout);
209
+ if (options.ai?.screenshotFallback !== void 0)
210
+ env[PIWI_ENV_KEYS.aiScreenshotFallback] = String(options.ai.screenshotFallback);
189
211
  }
190
212
 
191
213
  // src/internal/transport/http-client.ts
@@ -437,6 +459,7 @@ function toWireTestCase(tc) {
437
459
  networkRequests: rest.networkRequests || null,
438
460
  webVitals: rest.webVitals || null,
439
461
  pageState: rest.pageState || null,
462
+ aiUsage: rest.aiUsage || null,
440
463
  consoleLogs: rest.consoleLogs || null,
441
464
  ariaSnapshot: rest.ariaSnapshot || null,
442
465
  testSource: rest.testSource || null,
@@ -836,7 +859,9 @@ var ATTACHMENT_NAMES = {
836
859
  webVitals: "piwi-web-vitals",
837
860
  locatorSuggestion: "piwi-locator-suggestion",
838
861
  pageState: "piwi-page-state",
839
- userPick: "piwi-user-pick"
862
+ userPick: "piwi-user-pick",
863
+ aiUsage: "piwi-ai-usage",
864
+ aiMeta: "piwi-ai-meta"
840
865
  };
841
866
  var INTERNAL_ATTACHMENT_NAMES = new Set(Object.values(ATTACHMENT_NAMES));
842
867
  var LOCATOR_SUGGESTION_ANNOTATION = ATTACHMENT_NAMES.locatorSuggestion;
@@ -945,6 +970,13 @@ var FileHandler = class {
945
970
  } catch {
946
971
  }
947
972
  }
973
+ const aiUsage = find(ATTACHMENT_NAMES.aiUsage);
974
+ if (aiUsage?.body) {
975
+ try {
976
+ testCase.aiUsage = JSON.parse(aiUsage.body.toString());
977
+ } catch {
978
+ }
979
+ }
948
980
  }
949
981
  /** Compute SHA-256 hash and size for a single test case's trace file. Returns `null` when the case has no trace on disk. */
950
982
  async computeSingleTraceHash(testCase) {
@@ -1772,10 +1804,10 @@ function readSourceSnippet(file, declLine, context, failingLine) {
1772
1804
  const hasFailingLine = failingLine != null;
1773
1805
  const isFailing = hasFailingLine && lineNum === failingLine;
1774
1806
  const isDecl = lineNum === declLine && !isFailing;
1775
- let marker = " ";
1776
- if (isFailing) marker = "> ";
1777
- else if (isDecl) marker = hasFailingLine ? "* " : "> ";
1778
- return `${marker}${String(lineNum).padStart(4)} | ${l}`;
1807
+ let marker2 = " ";
1808
+ if (isFailing) marker2 = "> ";
1809
+ else if (isDecl) marker2 = hasFailingLine ? "* " : "> ";
1810
+ return `${marker2}${String(lineNum).padStart(4)} | ${l}`;
1779
1811
  }).join("\n");
1780
1812
  } catch {
1781
1813
  return null;
@@ -3060,7 +3092,16 @@ function probeElementAttrs(el, arg) {
3060
3092
  }
3061
3093
  }
3062
3094
  const hasLabel = !!(el.labels && el.labels.length > 0);
3063
- const labelText = includeLabelText ? hasLabel ? (el.labels[0].textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null : null : void 0;
3095
+ let labelText;
3096
+ if (includeLabelText) {
3097
+ labelText = null;
3098
+ try {
3099
+ const label = hasLabel ? el.labels[0] : null;
3100
+ labelText = (label && label.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120) || null;
3101
+ } catch {
3102
+ labelText = null;
3103
+ }
3104
+ }
3064
3105
  return {
3065
3106
  tagName: el.tagName?.toLowerCase?.() ?? "unknown",
3066
3107
  attributes: attrMap,
@@ -3764,6 +3805,7 @@ var CAPTURED_ATTRIBUTES = [
3764
3805
  "alt",
3765
3806
  "title",
3766
3807
  "aria-label",
3808
+ "aria-labelledby",
3767
3809
  "aria-level",
3768
3810
  "role",
3769
3811
  "type",
@@ -4132,8 +4174,30 @@ function mergeCandidates(base, extra) {
4132
4174
  var path15 = __toESM(require("path"));
4133
4175
 
4134
4176
  // ../core/src/locator-fingerprint.ts
4177
+ var ELEMENT_MATCH_SCORES = { role: 60, text: 55, label: 50 };
4135
4178
  var PRESENT_SIMILARITY = 0.8;
4136
4179
  var MATCH_SIMILARITY = 0.2;
4180
+ var TEXT_CONTENT_ROLES = /* @__PURE__ */ new Set([
4181
+ "button",
4182
+ "link",
4183
+ "heading",
4184
+ "menuitem",
4185
+ "menuitemcheckbox",
4186
+ "menuitemradio",
4187
+ "tab",
4188
+ "option",
4189
+ "cell",
4190
+ "columnheader",
4191
+ "rowheader",
4192
+ "gridcell",
4193
+ "treeitem",
4194
+ "listitem",
4195
+ "checkbox",
4196
+ "radio",
4197
+ "switch"
4198
+ ]);
4199
+ var FORM_FIELD_ROLES = /* @__PURE__ */ new Set(["textbox", "combobox", "searchbox", "spinbutton", "slider"]);
4200
+ var escapeQuote = (s) => s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
4137
4201
  function parseAriaCandidates(ariaSnapshot) {
4138
4202
  if (!ariaSnapshot) return [];
4139
4203
  const out = [];
@@ -4196,6 +4260,40 @@ function matchRenamedElement(fp, candidates) {
4196
4260
  }
4197
4261
  return null;
4198
4262
  }
4263
+ function freshLocatorsFromCandidate(c) {
4264
+ const out = [];
4265
+ const name = c.name;
4266
+ if (!name) return out;
4267
+ out.push(
4268
+ c.level != null ? {
4269
+ locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}', level: ${c.level} })`,
4270
+ method: "getByRole",
4271
+ args: { role: c.role, name, level: c.level },
4272
+ score: ELEMENT_MATCH_SCORES.role
4273
+ } : {
4274
+ locator: `getByRole('${escapeQuote(c.role)}', { name: '${escapeQuote(name)}' })`,
4275
+ method: "getByRole",
4276
+ args: { role: c.role, name },
4277
+ score: ELEMENT_MATCH_SCORES.role
4278
+ }
4279
+ );
4280
+ if (TEXT_CONTENT_ROLES.has(c.role)) {
4281
+ out.push({
4282
+ locator: `getByText('${escapeQuote(name)}')`,
4283
+ method: "getByText",
4284
+ args: { text: name },
4285
+ score: ELEMENT_MATCH_SCORES.text
4286
+ });
4287
+ } else if (FORM_FIELD_ROLES.has(c.role)) {
4288
+ out.push({
4289
+ locator: `getByLabel('${escapeQuote(name)}')`,
4290
+ method: "getByLabel",
4291
+ args: { label: name },
4292
+ score: ELEMENT_MATCH_SCORES.label
4293
+ });
4294
+ }
4295
+ return out;
4296
+ }
4199
4297
 
4200
4298
  // ../core/src/locator-methods.ts
4201
4299
  var LOCATOR_BUILDER_METHODS = [
@@ -4299,6 +4397,43 @@ function extractAccessibleName(ariaSnapshot) {
4299
4397
  if (match) return match[1];
4300
4398
  return null;
4301
4399
  }
4400
+ var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
4401
+ var NAME_FROM_CONTENT_ROLES = /* @__PURE__ */ new Set([
4402
+ "button",
4403
+ "cell",
4404
+ "checkbox",
4405
+ "columnheader",
4406
+ "gridcell",
4407
+ "heading",
4408
+ "link",
4409
+ "menuitem",
4410
+ "menuitemcheckbox",
4411
+ "menuitemradio",
4412
+ "option",
4413
+ "radio",
4414
+ "row",
4415
+ "rowheader",
4416
+ "switch",
4417
+ "tab",
4418
+ "tooltip",
4419
+ "treeitem"
4420
+ ]);
4421
+ var PROBE_TEXT_CAP = 80;
4422
+ var PROBE_LABEL_TEXT_CAP = 120;
4423
+ function exactAccessibleName(attrs, role) {
4424
+ if (attrs.attributes["aria-labelledby"]) return null;
4425
+ const ariaLabel = attrs.attributes["aria-label"];
4426
+ if (ariaLabel) return ariaLabel;
4427
+ if (FORM_FIELD_TAGS.has(attrs.tagName)) {
4428
+ const labelText = attrs.labelText;
4429
+ return labelText && labelText.length < PROBE_LABEL_TEXT_CAP ? labelText : null;
4430
+ }
4431
+ if (role && NAME_FROM_CONTENT_ROLES.has(role)) {
4432
+ const text = attrs.textContent;
4433
+ return text && text.length < PROBE_TEXT_CAP ? text : null;
4434
+ }
4435
+ return null;
4436
+ }
4302
4437
  var NAME_BASED_METHODS = /* @__PURE__ */ new Set([
4303
4438
  "getByText",
4304
4439
  "getByRole",
@@ -4743,7 +4878,7 @@ function createSink() {
4743
4878
  capturedLocators: [],
4744
4879
  capturePromises: [],
4745
4880
  failedLocators: [],
4746
- expectCapturedLocations: /* @__PURE__ */ new Set(),
4881
+ probedLocations: /* @__PURE__ */ new Set(),
4747
4882
  lastActivePage: null,
4748
4883
  testInfo: null,
4749
4884
  stashedWebVitals: null,
@@ -4970,7 +5105,6 @@ var INSTRUMENTED_CONTEXTS = /* @__PURE__ */ new WeakSet();
4970
5105
  var PATCHED_BROWSERS = /* @__PURE__ */ new WeakSet();
4971
5106
  var CHAIN_METHOD_SET = new Set(CHAIN_METHODS);
4972
5107
  var ACTION_METHOD_SET = new Set(ACTION_METHODS);
4973
- var FORM_FIELD_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
4974
5108
  var CAPTURED_ATTRS_ARG = {
4975
5109
  keep: [...CAPTURED_ATTRIBUTES],
4976
5110
  tagRoles: TAG_TO_ROLE,
@@ -4979,11 +5113,32 @@ var CAPTURED_ATTRS_ARG = {
4979
5113
  // special-cased logic in the probe, so add them explicitly).
4980
5114
  roleSources: [.../* @__PURE__ */ new Set(["[role]", "input", "select", ...Object.keys(TAG_TO_ROLE)])].join(","),
4981
5115
  // The reporter always wants ancestor-anchored alternatives (the picker's
4982
- // anchors step and generateAnchoredAlternatives both need them); it derives
4983
- // the accessible name itself, so the probe's own labelText is unneeded.
5116
+ // anchors step and generateAnchoredAlternatives both need them). `labelText`
5117
+ // is what names a form field, and the probe reads it from `el.labels` in the
5118
+ // same pass — so asking for it here is free and settles the accessible name
5119
+ // of a labeled field without a second round trip (`exactAccessibleName`).
4984
5120
  includeStructural: true,
4985
- includeLabelText: false
5121
+ includeLabelText: true
4986
5122
  };
5123
+ var PROBE_GLOBAL = "__piwiProbeElement";
5124
+ var PROBE_INIT_SCRIPT = `(() => {
5125
+ const probe = ${probeElementAttrs.toString()};
5126
+ const arg = ${JSON.stringify(CAPTURED_ATTRS_ARG)};
5127
+ globalThis[${JSON.stringify(PROBE_GLOBAL)}] = (el) => probe(el, arg);
5128
+ })();`;
5129
+ var PROBE_UNSEEDED_PAGES = /* @__PURE__ */ new WeakSet();
5130
+ function probeElement(page, target) {
5131
+ const shipSource = () => target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
5132
+ if (!page || PROBE_UNSEEDED_PAGES.has(page)) return shipSource();
5133
+ return target.evaluate((el, name) => {
5134
+ const seeded = globalThis[name];
5135
+ return typeof seeded === "function" ? seeded(el) : null;
5136
+ }, PROBE_GLOBAL).then((attrs) => {
5137
+ if (attrs) return attrs;
5138
+ PROBE_UNSEEDED_PAGES.add(page);
5139
+ return shipSource();
5140
+ });
5141
+ }
4987
5142
  async function ariaSnapshotBestEffort(target, timeout) {
4988
5143
  if (typeof target.ariaSnapshot !== "function") return null;
4989
5144
  try {
@@ -5002,8 +5157,8 @@ async function ariaSnapshotBestEffort(target, timeout) {
5002
5157
  }
5003
5158
  }
5004
5159
  }
5005
- function startElementCapture(sink, target, seq, callerLocation, used) {
5006
- const probe = target.evaluate(probeElementAttrs, CAPTURED_ATTRS_ARG);
5160
+ function startElementCapture(sink, page, target, seq, callerLocation, used) {
5161
+ const probe = probeElement(page, target);
5007
5162
  const settledProbe = probe.then(
5008
5163
  () => void 0,
5009
5164
  () => void 0
@@ -5022,8 +5177,9 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
5022
5177
  ]);
5023
5178
  const role = resolveAriaRole({ ...attrs, accessibleName: null });
5024
5179
  const isFormField = FORM_FIELD_TAGS.has(attrs.tagName);
5025
- const aria = role || isFormField ? await ariaSnapshotBestEffort(target, 500) : null;
5026
- const accessibleName = extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null });
5180
+ const exactName = exactAccessibleName(attrs, role);
5181
+ const aria = exactName === null && (role || isFormField) ? await ariaSnapshotBestEffort(target, 500) : null;
5182
+ const accessibleName = exactName ?? (extractAccessibleName(aria) || approximateAccessibleName({ ...attrs, accessibleName: null }));
5027
5183
  sink.capturedLocators[seq] = {
5028
5184
  location: callerLocation,
5029
5185
  used,
@@ -5043,6 +5199,7 @@ function startElementCapture(sink, target, seq, callerLocation, used) {
5043
5199
  alternatives: generateAlternatives({ ...attrs, accessibleName })
5044
5200
  };
5045
5201
  } catch {
5202
+ if (callerLocation) sink.probedLocations.delete(callerLocation);
5046
5203
  } finally {
5047
5204
  clearTimeout(deadline);
5048
5205
  }
@@ -5079,17 +5236,17 @@ function wrapLocator(page, locator, originMethod, originArgs) {
5079
5236
  args: originArgs,
5080
5237
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
5081
5238
  };
5082
- const alreadyCaptured = callerLocation !== null && sink.expectCapturedLocations.has(callerLocation);
5239
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
5083
5240
  let seq = -1;
5084
- if (!alreadyCaptured) {
5241
+ if (!alreadyProbed) {
5085
5242
  seq = sink.capturedLocators.length;
5086
5243
  sink.capturedLocators.push({ location: callerLocation, used, element: null, alternatives: [] });
5087
5244
  }
5088
5245
  const result = await fn.apply(target, callArgs);
5089
5246
  const matches = result?.matches;
5090
- if (matches === true && !alreadyCaptured) {
5091
- if (callerLocation) sink.expectCapturedLocations.add(callerLocation);
5092
- startElementCapture(sink, target, seq, callerLocation, used);
5247
+ if (matches === true && !alreadyProbed) {
5248
+ if (callerLocation) sink.probedLocations.add(callerLocation);
5249
+ startElementCapture(sink, page, target, seq, callerLocation, used);
5093
5250
  } else if (matches === false) {
5094
5251
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
5095
5252
  }
@@ -5101,19 +5258,23 @@ function wrapLocator(page, locator, originMethod, originArgs) {
5101
5258
  const sink = currentSink;
5102
5259
  if (!sink) return fn.apply(target, callArgs);
5103
5260
  sink.lastActivePage = page;
5104
- const seq = sink.capturedLocators.length;
5105
5261
  const callerLocation = captureCallerLocation();
5106
5262
  const used = {
5107
5263
  method: originMethod,
5108
5264
  args: originArgs,
5109
5265
  raw: `${originMethod}(${JSON.stringify(originArgs)})`
5110
5266
  };
5111
- sink.capturedLocators.push({
5112
- location: callerLocation,
5113
- used,
5114
- element: null,
5115
- alternatives: []
5116
- });
5267
+ const alreadyProbed = callerLocation !== null && sink.probedLocations.has(callerLocation);
5268
+ let seq = -1;
5269
+ if (!alreadyProbed) {
5270
+ seq = sink.capturedLocators.length;
5271
+ sink.capturedLocators.push({
5272
+ location: callerLocation,
5273
+ used,
5274
+ element: null,
5275
+ alternatives: []
5276
+ });
5277
+ }
5117
5278
  let result;
5118
5279
  try {
5119
5280
  result = await fn.apply(target, callArgs);
@@ -5121,7 +5282,10 @@ function wrapLocator(page, locator, originMethod, originArgs) {
5121
5282
  sink.failedLocators.push({ method: originMethod, args: originArgs, location: callerLocation });
5122
5283
  throw error;
5123
5284
  }
5124
- startElementCapture(sink, target, seq, callerLocation, used);
5285
+ if (!alreadyProbed) {
5286
+ if (callerLocation) sink.probedLocations.add(callerLocation);
5287
+ startElementCapture(sink, page, target, seq, callerLocation, used);
5288
+ }
5125
5289
  return result;
5126
5290
  };
5127
5291
  }
@@ -5154,6 +5318,9 @@ function instrumentPage(page) {
5154
5318
  return wrapLocator(page, original(...args), method, args);
5155
5319
  };
5156
5320
  }
5321
+ if (typeof page.on === "function") {
5322
+ page.on("framenavigated", () => PROBE_UNSEEDED_PAGES.delete(page));
5323
+ }
5157
5324
  }
5158
5325
  page.on("console", (msg) => {
5159
5326
  const sink = currentSink;
@@ -5218,6 +5385,10 @@ function instrumentPage(page) {
5218
5385
  function instrumentContext(context) {
5219
5386
  if (!context || INSTRUMENTED_CONTEXTS.has(context)) return;
5220
5387
  INSTRUMENTED_CONTEXTS.add(context);
5388
+ if (process.env.PIWI_CAPTURE_LOCATORS !== "false" && typeof context.addInitScript === "function") {
5389
+ void Promise.resolve(context.addInitScript({ content: PROBE_INIT_SCRIPT })).catch(() => {
5390
+ });
5391
+ }
5221
5392
  const originalNewPage = context.newPage.bind(context);
5222
5393
  context.newPage = async (...args) => {
5223
5394
  const page = await originalNewPage(...args);
@@ -5392,13 +5563,804 @@ function extendPiwiFixtures(test) {
5392
5563
  return test.extend(piwiFixtures);
5393
5564
  }
5394
5565
 
5566
+ // src/internal/ai/ai-fixtures.ts
5567
+ var fs15 = __toESM(require("fs"));
5568
+ var path19 = __toESM(require("path"));
5569
+ var import_test = require("@playwright/test");
5570
+
5571
+ // src/internal/ai/artifact.ts
5572
+ var fs14 = __toESM(require("fs"));
5573
+ var path17 = __toESM(require("path"));
5574
+ var ARTIFACT_VERSION = 1;
5575
+ var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
5576
+ var ACTION_METHOD_SET2 = new Set(ACTION_METHODS);
5577
+ var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
5578
+ "visible",
5579
+ "hidden",
5580
+ "attached",
5581
+ "url"
5582
+ ]);
5583
+ function sortDeep(value) {
5584
+ if (Array.isArray(value)) return value.map(sortDeep);
5585
+ if (value !== null && typeof value === "object") {
5586
+ const out = {};
5587
+ for (const key of Object.keys(value).sort()) {
5588
+ const v = value[key];
5589
+ if (v !== void 0) out[key] = sortDeep(v);
5590
+ }
5591
+ return out;
5592
+ }
5593
+ return value;
5594
+ }
5595
+ function serializeEntry(entry) {
5596
+ return `${JSON.stringify(sortDeep(entry), null, 2)}
5597
+ `;
5598
+ }
5599
+ var ArtifactError = class extends Error {
5600
+ };
5601
+ function assert(condition, message) {
5602
+ if (!condition) throw new ArtifactError(message);
5603
+ }
5604
+ function validateStructuredLocator(value, where) {
5605
+ assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
5606
+ const loc = value;
5607
+ assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
5608
+ assert(LOCATOR_METHOD_SET.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
5609
+ assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
5610
+ if (loc.chain !== void 0) {
5611
+ assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
5612
+ loc.chain.forEach((child, i) => validateStructuredLocator(child, `${where}.chain[${i}]`));
5613
+ }
5614
+ }
5615
+ function validateRunStep(value, where) {
5616
+ assert(value !== null && typeof value === "object", `${where}: step must be an object`);
5617
+ const step = value;
5618
+ validateStructuredLocator(step.locator, `${where}.locator`);
5619
+ assert(typeof step.action === "string", `${where}: action must be a string`);
5620
+ assert(ACTION_METHOD_SET2.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
5621
+ if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
5622
+ if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
5623
+ if (step.waitForResponse !== void 0) {
5624
+ assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
5625
+ }
5626
+ }
5627
+ function validatePostcondition(value) {
5628
+ assert(value !== null && typeof value === "object", "postcondition must be an object");
5629
+ const post = value;
5630
+ assert(
5631
+ POSTCONDITION_ASSERTS.has(post.assert),
5632
+ `postcondition.assert "${String(post.assert)}" is not supported`
5633
+ );
5634
+ if (post.assert === "url") {
5635
+ assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
5636
+ } else {
5637
+ validateStructuredLocator(post.locator, "postcondition.locator");
5638
+ }
5639
+ }
5640
+ function parseEntry(text) {
5641
+ let raw;
5642
+ try {
5643
+ raw = JSON.parse(text);
5644
+ } catch (error) {
5645
+ throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
5646
+ }
5647
+ assert(raw !== null && typeof raw === "object", "entry must be an object");
5648
+ const obj = raw;
5649
+ assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
5650
+ assert(typeof obj.template === "string", "entry.template must be a string");
5651
+ if (obj.kind === "locator") {
5652
+ validateStructuredLocator(obj.locator, "entry.locator");
5653
+ return obj;
5654
+ }
5655
+ if (obj.kind === "run") {
5656
+ assert(Array.isArray(obj.steps), "entry.steps must be an array");
5657
+ obj.steps.forEach((step, i) => validateRunStep(step, `entry.steps[${i}]`));
5658
+ validatePostcondition(obj.postcondition);
5659
+ return obj;
5660
+ }
5661
+ throw new ArtifactError(`unsupported entry kind "${String(obj.kind)}"`);
5662
+ }
5663
+ function readEntry(file) {
5664
+ let text;
5665
+ try {
5666
+ text = fs14.readFileSync(file, "utf8");
5667
+ } catch (error) {
5668
+ if (error.code === "ENOENT") return null;
5669
+ throw error;
5670
+ }
5671
+ return parseEntry(text);
5672
+ }
5673
+ function writeEntry(file, entry) {
5674
+ const canonical = serializeEntry(entry);
5675
+ return withEntryLock(file, () => {
5676
+ if (fs14.existsSync(file) && fs14.readFileSync(file, "utf8") === canonical) return { written: false };
5677
+ fs14.mkdirSync(path17.dirname(file), { recursive: true });
5678
+ const tmp = `${file}.${process.pid}.tmp`;
5679
+ fs14.writeFileSync(tmp, canonical);
5680
+ fs14.renameSync(tmp, file);
5681
+ return { written: true };
5682
+ });
5683
+ }
5684
+ function withEntryLock(file, fn) {
5685
+ const lock = `${file}.lock`;
5686
+ fs14.mkdirSync(path17.dirname(file), { recursive: true });
5687
+ const deadline = Date.now() + 5e3;
5688
+ for (; ; ) {
5689
+ try {
5690
+ const fd = fs14.openSync(lock, "wx");
5691
+ fs14.closeSync(fd);
5692
+ break;
5693
+ } catch (error) {
5694
+ if (error.code !== "EEXIST") throw error;
5695
+ if (Date.now() > deadline) {
5696
+ try {
5697
+ fs14.unlinkSync(lock);
5698
+ } catch {
5699
+ }
5700
+ }
5701
+ }
5702
+ }
5703
+ try {
5704
+ return fn();
5705
+ } finally {
5706
+ try {
5707
+ fs14.unlinkSync(lock);
5708
+ } catch {
5709
+ }
5710
+ }
5711
+ }
5712
+
5713
+ // src/internal/ai/keys.ts
5714
+ var crypto3 = __toESM(require("crypto"));
5715
+ var path18 = __toESM(require("path"));
5716
+ var DEFAULT_AI_DIR = "__piwi__";
5717
+ function normalizeTemplate(template) {
5718
+ return template.trim().replace(/\s+/g, " ").toLowerCase();
5719
+ }
5720
+ function hashTemplate(normalized, ordinal = 0) {
5721
+ const input = ordinal > 0 ? `${normalized}#${ordinal}` : normalized;
5722
+ return crypto3.createHash("sha256").update(input).digest("hex").slice(0, 8);
5723
+ }
5724
+ function slug(text, maxLength = 40) {
5725
+ const base = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
5726
+ return (base || "x").slice(0, maxLength).replace(/-+$/g, "") || "x";
5727
+ }
5728
+ function entryDir(specFile, dir = DEFAULT_AI_DIR) {
5729
+ return path18.join(path18.dirname(specFile), dir, path18.basename(specFile));
5730
+ }
5731
+ function entryPath(params) {
5732
+ const basis = `${normalizeTemplate(params.testTitle)}::${normalizeTemplate(params.template)}`;
5733
+ const hash = hashTemplate(basis, params.ordinal ?? 0);
5734
+ const name = `${slug(params.testTitle)}.${slug(params.template)}.${hash}.json`;
5735
+ return path18.join(entryDir(params.specFile, params.dir), name);
5736
+ }
5737
+ function parseLocation(location) {
5738
+ const match = /:(\d+):(\d+)$/.exec(location);
5739
+ if (!match) return null;
5740
+ return { line: Number(match[1]), col: Number(match[2]) };
5741
+ }
5742
+ function orderByPosition(locations) {
5743
+ return [...new Set(locations)].sort((a, b) => {
5744
+ const pa = parseLocation(a);
5745
+ const pb = parseLocation(b);
5746
+ if (!pa || !pb) return a < b ? -1 : a > b ? 1 : 0;
5747
+ return pa.line - pb.line || pa.col - pb.col;
5748
+ });
5749
+ }
5750
+ function ordinalForLocation(sites, location) {
5751
+ const ordered = orderByPosition(sites);
5752
+ const index = ordered.indexOf(location);
5753
+ return index < 0 ? 0 : index;
5754
+ }
5755
+ function findTemplateSites(source, template) {
5756
+ const needle = template.trim();
5757
+ if (!needle) return [];
5758
+ const sites = [];
5759
+ const lines = source.split("\n");
5760
+ for (let i = 0; i < lines.length; i++) {
5761
+ let from = 0;
5762
+ for (; ; ) {
5763
+ const col = lines[i].indexOf(needle, from);
5764
+ if (col < 0) break;
5765
+ sites.push(`${i + 1}:${col + 1}`);
5766
+ from = col + needle.length;
5767
+ }
5768
+ }
5769
+ return sites;
5770
+ }
5771
+
5772
+ // src/internal/ai/params.ts
5773
+ var PLACEHOLDER = /\{([^{}]+)\}/g;
5774
+ var MARKER = /\{\{([^{}]+)\}\}/g;
5775
+ function extractPlaceholders(template) {
5776
+ const names = [];
5777
+ for (const match of template.matchAll(PLACEHOLDER)) {
5778
+ const name = match[1].trim();
5779
+ if (name && !names.includes(name)) names.push(name);
5780
+ }
5781
+ return names;
5782
+ }
5783
+ function marker(name) {
5784
+ return `{{${name}}}`;
5785
+ }
5786
+ function validateParams(template, params) {
5787
+ const placeholders = extractPlaceholders(template);
5788
+ if (placeholders.length === 0) return;
5789
+ const missing = placeholders.filter((name) => params?.[name] === void 0);
5790
+ if (missing.length > 0) {
5791
+ throw new Error(`piwi AI: template "${template}" is missing parameter(s): ${missing.join(", ")}`);
5792
+ }
5793
+ }
5794
+ function substituteMarkers(text, params) {
5795
+ return text.replace(MARKER, (whole, name) => {
5796
+ const value = params[name.trim()];
5797
+ return value === void 0 ? whole : value;
5798
+ });
5799
+ }
5800
+ function substituteArgs(args, params) {
5801
+ return args.map((arg) => substituteValue(arg, params));
5802
+ }
5803
+ function substituteValue(value, params) {
5804
+ if (typeof value === "string") return substituteMarkers(value, params);
5805
+ if (Array.isArray(value)) return value.map((v) => substituteValue(v, params));
5806
+ if (value !== null && typeof value === "object") {
5807
+ const out = {};
5808
+ for (const [key, v] of Object.entries(value)) out[key] = substituteValue(v, params);
5809
+ return out;
5810
+ }
5811
+ return value;
5812
+ }
5813
+ function maskValues(text, params) {
5814
+ const entries = Object.entries(params).filter(([, value]) => value.length > 0).sort((a, b) => b[1].length - a[1].length);
5815
+ let masked = text;
5816
+ for (const [name, value] of entries) {
5817
+ masked = masked.split(value).join(marker(name));
5818
+ }
5819
+ return masked;
5820
+ }
5821
+ function isParametric(template, compiledText) {
5822
+ return extractPlaceholders(template).every((name) => compiledText.includes(marker(name)));
5823
+ }
5824
+
5825
+ // src/internal/ai/interpreter.ts
5826
+ var LOCATOR_METHOD_SET2 = new Set(LOCATOR_METHODS);
5827
+ var ACTION_METHOD_SET3 = new Set(ACTION_METHODS);
5828
+ var StepDriftError = class extends Error {
5829
+ constructor(step, fingerprint) {
5830
+ super(
5831
+ `piwi AI: element drifted from its recorded identity (role="${fingerprint.role ?? ""}" name="${fingerprint.name ?? ""}") before ${describeLocator(step.locator)}.${step.action}() \u2014 the flow was not run to avoid corrupting page state`
5832
+ );
5833
+ this.step = step;
5834
+ this.fingerprint = fingerprint;
5835
+ this.name = "StepDriftError";
5836
+ }
5837
+ };
5838
+ var PostconditionError = class extends Error {
5839
+ constructor(post, cause) {
5840
+ super(`piwi AI: postcondition (${describePostcondition(post)}) not satisfied \u2014 ${cause}`);
5841
+ this.name = "PostconditionError";
5842
+ }
5843
+ };
5844
+ function describeLocator(loc) {
5845
+ const head = `${loc.method}(${loc.args.map((a) => JSON.stringify(a)).join(", ")})`;
5846
+ const tail = (loc.chain ?? []).map(describeLocator).join(".");
5847
+ return tail ? `${head}.${tail}` : head;
5848
+ }
5849
+ function renderSourceArg(arg) {
5850
+ if (typeof arg === "string") return `'${arg.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
5851
+ if (arg === null || typeof arg === "number" || typeof arg === "boolean") return String(arg);
5852
+ if (Array.isArray(arg)) return `[${arg.map(renderSourceArg).join(", ")}]`;
5853
+ return `{ ${Object.entries(arg).map(([key, value]) => `${key}: ${renderSourceArg(value)}`).join(", ")} }`;
5854
+ }
5855
+ function locatorSource(loc) {
5856
+ const head = `${loc.method}(${loc.args.map(renderSourceArg).join(", ")})`;
5857
+ const tail = (loc.chain ?? []).map(locatorSource).join(".");
5858
+ return tail ? `${head}.${tail}` : head;
5859
+ }
5860
+ function describePostcondition(post) {
5861
+ return post.assert === "url" ? `url \u2192 ${post.url ?? ""}` : `${describeLocator(post.locator)} ${post.assert}`;
5862
+ }
5863
+ function buildLocator(root, structured, params = {}) {
5864
+ if (!LOCATOR_METHOD_SET2.has(structured.method)) {
5865
+ throw new Error(`piwi AI: locator method "${structured.method}" is not allowlisted`);
5866
+ }
5867
+ const args = substituteArgs(structured.args, params);
5868
+ let current = root[structured.method](...args);
5869
+ for (const child of structured.chain ?? []) {
5870
+ current = buildLocator(current, child, params);
5871
+ }
5872
+ return current;
5873
+ }
5874
+ async function probePresent(locator, timeout) {
5875
+ try {
5876
+ await locator.first().waitFor({ state: "visible", timeout });
5877
+ return true;
5878
+ } catch {
5879
+ return false;
5880
+ }
5881
+ }
5882
+ function checkFingerprintDrift(ariaSnapshot, fingerprint) {
5883
+ if (!ariaSnapshot) return "unknown";
5884
+ const candidates = parseAriaCandidates(ariaSnapshot);
5885
+ if (candidates.length === 0) return "unknown";
5886
+ return fingerprintPresent(fingerprint, candidates) ? "present" : "drifted";
5887
+ }
5888
+ async function performAction(page, locator, step, ctx) {
5889
+ if (!ACTION_METHOD_SET3.has(step.action)) throw new Error(`piwi AI: action "${step.action}" is not allowlisted`);
5890
+ const args = step.value === void 0 ? [] : [substituteMarkers(step.value, ctx.params)];
5891
+ const act = () => locator[step.action](...args);
5892
+ if (!step.waitForResponse) {
5893
+ await act();
5894
+ return;
5895
+ }
5896
+ const pattern = substituteMarkers(step.waitForResponse, ctx.params);
5897
+ const waitForResponse = ctx.responseWaitTimeout === void 0 ? page.waitForResponse(pattern) : page.waitForResponse(pattern, { timeout: ctx.responseWaitTimeout });
5898
+ await Promise.all([waitForResponse, act()]);
5899
+ }
5900
+ async function executeStep(step, ctx) {
5901
+ const body = async () => {
5902
+ const locator = buildLocator(ctx.page, step.locator, ctx.params);
5903
+ if (step.optional) {
5904
+ const present = await probePresent(locator, ctx.optionalProbeTimeout ?? 2e3);
5905
+ if (!present) return "skipped";
5906
+ }
5907
+ if (step.fingerprint && ctx.readAria) {
5908
+ const aria = await ctx.readAria(ctx.page);
5909
+ if (checkFingerprintDrift(aria, step.fingerprint) === "drifted") {
5910
+ throw new StepDriftError(step, step.fingerprint);
5911
+ }
5912
+ }
5913
+ await performAction(ctx.page, locator, step, ctx);
5914
+ return "ran";
5915
+ };
5916
+ const title = `${step.action}: ${describeLocator(step.locator)}${step.optional ? " (optional)" : ""}`;
5917
+ return ctx.step ? ctx.step(title, body) : body();
5918
+ }
5919
+ async function assertPostcondition(post, ctx) {
5920
+ const body = async () => {
5921
+ try {
5922
+ if (post.assert === "url") {
5923
+ await ctx.page.waitForURL(substituteMarkers(post.url ?? "", ctx.params));
5924
+ return;
5925
+ }
5926
+ const locator = buildLocator(ctx.page, post.locator, ctx.params);
5927
+ const state = post.assert === "hidden" ? "hidden" : post.assert === "attached" ? "attached" : "visible";
5928
+ await locator.first().waitFor({ state });
5929
+ } catch (error) {
5930
+ throw new PostconditionError(post, error.message);
5931
+ }
5932
+ };
5933
+ await (ctx.step ? ctx.step(`postcondition: ${describePostcondition(post)}`, body) : body());
5934
+ }
5935
+ async function executeRun(entry, ctx) {
5936
+ for (const step of entry.steps) {
5937
+ await executeStep(step, ctx);
5938
+ }
5939
+ await assertPostcondition(entry.postcondition, ctx);
5940
+ }
5941
+
5942
+ // src/internal/ai/compile.ts
5943
+ function hasAnchor(args) {
5944
+ return Object.keys(args).some((key) => key.startsWith("anchor"));
5945
+ }
5946
+ function positionalArgs(method, args) {
5947
+ switch (method) {
5948
+ case "getByTestId":
5949
+ return typeof args.testId === "string" ? [args.testId] : null;
5950
+ case "getByRole": {
5951
+ if (typeof args.role !== "string") return null;
5952
+ const options = {};
5953
+ if (typeof args.name === "string") options.name = args.name;
5954
+ if (typeof args.level === "number") options.level = args.level;
5955
+ return Object.keys(options).length > 0 ? [args.role, options] : [args.role];
5956
+ }
5957
+ case "getByLabel":
5958
+ return typeof args.label === "string" ? [args.label] : null;
5959
+ case "getByPlaceholder":
5960
+ return typeof args.placeholder === "string" ? [args.placeholder] : null;
5961
+ case "getByText":
5962
+ case "getByAltText":
5963
+ return typeof args.text === "string" ? [args.text] : null;
5964
+ case "getByTitle":
5965
+ return typeof args.title === "string" ? [args.title] : null;
5966
+ case "locator":
5967
+ return typeof args.selector === "string" ? [args.selector] : null;
5968
+ default:
5969
+ return null;
5970
+ }
5971
+ }
5972
+ function rankedToStructured(ranked) {
5973
+ if (hasAnchor(ranked.args)) return null;
5974
+ const args = positionalArgs(ranked.method, ranked.args);
5975
+ if (!args) return null;
5976
+ return { method: ranked.method, args };
5977
+ }
5978
+ function compileFromCandidate(candidate) {
5979
+ const fingerprint = { role: candidate.role, name: candidate.name };
5980
+ if (candidate.level != null) fingerprint.level = candidate.level;
5981
+ for (const ranked of freshLocatorsFromCandidate(candidate)) {
5982
+ const structured = rankedToStructured(ranked);
5983
+ if (structured) return { locator: structured, fingerprint, score: ranked.score };
5984
+ }
5985
+ return null;
5986
+ }
5987
+
5988
+ // src/internal/ai/resolution.ts
5989
+ var MAX_SNAPSHOT_CHARS = 24e3;
5990
+ var DEFAULT_MAX_STEPS = 20;
5991
+ var ServerStepResolver = class {
5992
+ constructor(serverUrl, apiKey) {
5993
+ this.serverUrl = serverUrl;
5994
+ this.apiKey = apiKey;
5995
+ }
5996
+ async resolveStep(request) {
5997
+ const headers = { "Content-Type": "application/json" };
5998
+ if (this.apiKey) headers["X-API-Key"] = this.apiKey;
5999
+ const url = `${this.serverUrl.replace(/\/+$/, "")}/api/ai/step-resolution`;
6000
+ const res = await fetch(url, { method: "POST", headers, body: JSON.stringify(request) });
6001
+ if (!res.ok) {
6002
+ const body = await res.json().catch(() => ({}));
6003
+ throw new Error(body.message || `piwi AI: step resolution failed (${res.status})`);
6004
+ }
6005
+ return await res.json();
6006
+ }
6007
+ };
6008
+ function candidateOf(element) {
6009
+ return { role: element.role, name: element.name ?? null, level: element.level ?? null };
6010
+ }
6011
+ function locatorFromElement(element) {
6012
+ return compileFromCandidate(candidateOf(element));
6013
+ }
6014
+ function lazyLocator(resolveOnce) {
6015
+ let pending = null;
6016
+ const ensure = () => pending ??= resolveOnce();
6017
+ const handler = {
6018
+ get(_t, prop) {
6019
+ if (typeof prop !== "string" || prop === "then" || prop === "catch" || prop === "finally") return void 0;
6020
+ return (...args) => ensure().then((loc) => loc[prop](...args));
6021
+ }
6022
+ };
6023
+ return new Proxy({}, handler);
6024
+ }
6025
+ function buildPostcondition(resolved, template) {
6026
+ if (!resolved)
6027
+ throw new Error(`piwi AI: flow "${template}" resolved without a postcondition (the oracle is mandatory)`);
6028
+ if (resolved.assert === "url") {
6029
+ if (!resolved.url) throw new Error(`piwi AI: flow "${template}" postcondition is a url assert without a url`);
6030
+ return { assert: "url", url: resolved.url };
6031
+ }
6032
+ const compiled = resolved.element ? locatorFromElement(resolved.element) : null;
6033
+ if (!compiled) throw new Error(`piwi AI: flow "${template}" postcondition element could not be compiled`);
6034
+ return { assert: resolved.assert, locator: compiled.locator };
6035
+ }
6036
+ var readMaskedSnapshot = async (page, params) => {
6037
+ const raw = await ariaSnapshotBestEffort(page.locator("body"));
6038
+ return raw ? maskValues(raw, params) : "";
6039
+ };
6040
+ var observeXhrResponses = async (page, action, settleMs) => {
6041
+ const urls = [];
6042
+ const onResponse = (res) => {
6043
+ const type = res.request().resourceType();
6044
+ if (type === "xhr" || type === "fetch") urls.push(res.url());
6045
+ };
6046
+ page.on("response", onResponse);
6047
+ try {
6048
+ await action();
6049
+ await page.waitForLoadState("networkidle", settleMs === void 0 ? void 0 : { timeout: settleMs }).catch(() => {
6050
+ });
6051
+ } finally {
6052
+ page.off("response", onResponse);
6053
+ }
6054
+ return urls;
6055
+ };
6056
+ var captureViewportScreenshot = async (page) => {
6057
+ try {
6058
+ const buffer = await page.screenshot({ type: "jpeg", quality: 60 });
6059
+ return { mediaType: "image/jpeg", data: buffer.toString("base64") };
6060
+ } catch {
6061
+ return null;
6062
+ }
6063
+ };
6064
+ async function maybeScreenshot(ctx, ariaSnapshot) {
6065
+ if (!ctx.screenshotFallback || ariaSnapshot.length > 0) return void 0;
6066
+ const capture = ctx.captureScreenshot ?? captureViewportScreenshot;
6067
+ return await capture(ctx.page) ?? void 0;
6068
+ }
6069
+ function assertParametric(template, locatorJson) {
6070
+ if (extractPlaceholders(template).length > 0 && !isParametric(template, locatorJson)) {
6071
+ throw new Error(
6072
+ `piwi AI: resolved locator for "${template}" is not parametric \u2014 the grounding pinned a concrete value instead of a placeholder`
6073
+ );
6074
+ }
6075
+ }
6076
+ async function resolveLocator(template, ctx) {
6077
+ const readSnapshot = ctx.readSnapshot ?? readMaskedSnapshot;
6078
+ const cap = ctx.maxSnapshotChars ?? MAX_SNAPSHOT_CHARS;
6079
+ const ariaSnapshot = (await readSnapshot(ctx.page, ctx.params)).slice(0, cap);
6080
+ const screenshot = await maybeScreenshot(ctx, ariaSnapshot);
6081
+ const decision = await ctx.resolver.resolveStep({
6082
+ kind: "locator",
6083
+ template,
6084
+ paramNames: Object.keys(ctx.params),
6085
+ ariaSnapshot,
6086
+ history: [],
6087
+ ...screenshot ? { screenshot } : {}
6088
+ });
6089
+ if (!decision.element) throw new Error(`piwi AI: resolver returned no element for "${template}"`);
6090
+ const compiled = locatorFromElement(decision.element);
6091
+ if (!compiled) throw new Error(`piwi AI: element for "${template}" could not be compiled to a stable locator`);
6092
+ const entry = {
6093
+ version: ARTIFACT_VERSION,
6094
+ kind: "locator",
6095
+ template,
6096
+ locator: compiled.locator,
6097
+ fingerprint: compiled.fingerprint
6098
+ };
6099
+ assertParametric(template, JSON.stringify(compiled.locator));
6100
+ const built = buildLocator(ctx.page, compiled.locator, ctx.params);
6101
+ await built.first().waitFor({ state: "visible" });
6102
+ return entry;
6103
+ }
6104
+ async function resolveRun(template, ctx) {
6105
+ const readSnapshot = ctx.readSnapshot ?? readMaskedSnapshot;
6106
+ const maxSteps = ctx.maxSteps ?? DEFAULT_MAX_STEPS;
6107
+ const cap = ctx.maxSnapshotChars ?? MAX_SNAPSHOT_CHARS;
6108
+ const paramNames = Object.keys(ctx.params);
6109
+ const steps = [];
6110
+ const history = [];
6111
+ for (let i = 0; i < maxSteps; i++) {
6112
+ const ariaSnapshot = (await readSnapshot(ctx.page, ctx.params)).slice(0, cap);
6113
+ const screenshot = await maybeScreenshot(ctx, ariaSnapshot);
6114
+ const decision = await ctx.resolver.resolveStep({
6115
+ kind: "run",
6116
+ template,
6117
+ paramNames,
6118
+ ariaSnapshot,
6119
+ history,
6120
+ ...screenshot ? { screenshot } : {}
6121
+ });
6122
+ if (decision.done) {
6123
+ const postcondition = buildPostcondition(decision.postcondition, template);
6124
+ await assertPostcondition(postcondition, { page: ctx.page, params: ctx.params });
6125
+ return { version: ARTIFACT_VERSION, kind: "run", template, steps, postcondition };
6126
+ }
6127
+ if (!decision.element || !decision.action) {
6128
+ throw new Error(`piwi AI: resolver returned neither a step nor done for "${template}"`);
6129
+ }
6130
+ const compiled = locatorFromElement(decision.element);
6131
+ if (!compiled) throw new Error(`piwi AI: a step element for "${template}" could not be compiled`);
6132
+ const step = { locator: compiled.locator, action: decision.action, fingerprint: compiled.fingerprint };
6133
+ if (decision.value !== void 0) step.value = decision.value;
6134
+ if (decision.optional) step.optional = true;
6135
+ steps.push(step);
6136
+ const observe = ctx.observeResponses ?? observeXhrResponses;
6137
+ const observed = await observe(
6138
+ ctx.page,
6139
+ () => executeStep(step, {
6140
+ page: ctx.page,
6141
+ params: ctx.params,
6142
+ optionalProbeTimeout: ctx.optionalProbeTimeout
6143
+ }).then(() => void 0),
6144
+ ctx.responseWaitTimeout
6145
+ );
6146
+ history.push({ action: decision.action, element: decision.element, value: decision.value });
6147
+ if (observed.length > 0) {
6148
+ const waitDecision = await ctx.resolver.resolveStep({
6149
+ kind: "wait",
6150
+ template,
6151
+ paramNames,
6152
+ ariaSnapshot: "",
6153
+ history,
6154
+ observedResponses: observed.map((url) => maskValues(url, ctx.params))
6155
+ });
6156
+ if (waitDecision.waitForResponse) step.waitForResponse = waitDecision.waitForResponse;
6157
+ }
6158
+ }
6159
+ throw new Error(`piwi AI: exceeded the ${maxSteps}-step budget resolving "${template}"`);
6160
+ }
6161
+
6162
+ // src/internal/ai/ai-fixtures.ts
6163
+ function parseAiMode(value) {
6164
+ return value === "resolve" || value === "heal" ? value : "replay";
6165
+ }
6166
+ function readPositiveInt(value) {
6167
+ if (value === void 0 || value.trim() === "") return void 0;
6168
+ const n = Number(value);
6169
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : void 0;
6170
+ }
6171
+ function readAiConfig(env) {
6172
+ return {
6173
+ mode: parseAiMode(env.PIWI_AI),
6174
+ dir: env.PIWI_AI_DIR || DEFAULT_AI_DIR,
6175
+ onMiss: env.PIWI_AI_ON_MISS === "fixme" ? "fixme" : "fail",
6176
+ update: env.PIWI_AI_UPDATE === "true",
6177
+ maxSteps: readPositiveInt(env.PIWI_AI_MAX_FLOW_STEPS),
6178
+ maxSnapshotChars: readPositiveInt(env.PIWI_AI_MAX_SNAPSHOT_CHARS),
6179
+ optionalProbeTimeout: readPositiveInt(env.PIWI_AI_OPTIONAL_PROBE_TIMEOUT),
6180
+ responseWaitTimeout: readPositiveInt(env.PIWI_AI_RESPONSE_WAIT_TIMEOUT),
6181
+ screenshotFallback: env.PIWI_AI_SCREENSHOT_FALLBACK === "true"
6182
+ };
6183
+ }
6184
+ function testIdentity(testInfo) {
6185
+ const titles = testInfo.titlePath.slice(1);
6186
+ return titles.length > 0 ? titles.join(" \u203A ") : testInfo.title;
6187
+ }
6188
+ function relativeToCwd(file) {
6189
+ return path19.relative(process.cwd(), file).split(path19.sep).join("/");
6190
+ }
6191
+ function recordIntents(intents, entry) {
6192
+ const add = (locator, kind) => {
6193
+ intents.set(`${entry.template}\0${locator}`, { template: entry.template, locator, kind });
6194
+ };
6195
+ if (entry.kind === "locator") {
6196
+ add(locatorSource(entry.locator), "locator");
6197
+ return;
6198
+ }
6199
+ for (const step of entry.steps) add(locatorSource(step.locator), "run");
6200
+ if (entry.postcondition.locator) add(locatorSource(entry.postcondition.locator), "run");
6201
+ }
6202
+ function missMessage(template, testTitle, relPath) {
6203
+ return [
6204
+ `piwi AI: no committed entry for "${template}" in test "${testTitle}".`,
6205
+ ` expected file: ${relPath}`,
6206
+ ` run: piwi ai resolve --grep ${JSON.stringify(testTitle)}`,
6207
+ ` (requires a configured authoring model; commit the generated file afterwards)`
6208
+ ].join("\n");
6209
+ }
6210
+ function createAiApi(page, testInfo, config, used, intents) {
6211
+ const testTitle = testIdentity(testInfo);
6212
+ let source = null;
6213
+ const readSource = () => {
6214
+ if (source === null) {
6215
+ try {
6216
+ source = fs15.readFileSync(testInfo.file, "utf8");
6217
+ } catch {
6218
+ source = "";
6219
+ }
6220
+ }
6221
+ return source;
6222
+ };
6223
+ const ordinalFor = (template, location) => {
6224
+ if (!location) return 0;
6225
+ const sites = findTemplateSites(readSource(), template);
6226
+ if (sites.length <= 1) return 0;
6227
+ const callLine = /:(\d+):\d+$/.exec(location)?.[1];
6228
+ const perFileSites = sites.map((s) => `${testInfo.file}:${s}`);
6229
+ const match = perFileSites.find((s) => /:(\d+):\d+$/.exec(s)?.[1] === callLine);
6230
+ return match ? ordinalForLocation(perFileSites, match) : 0;
6231
+ };
6232
+ const resolveFile = (template) => {
6233
+ const location = captureCallerLocation();
6234
+ const ordinal = ordinalFor(template, location);
6235
+ return entryPath({ specFile: testInfo.file, testTitle, template, ordinal, dir: config.dir });
6236
+ };
6237
+ const missInReplay = (template, file) => {
6238
+ const message = missMessage(template, testTitle, relativeToCwd(file));
6239
+ if (config.onMiss === "fixme") {
6240
+ testInfo.annotations.push({ type: "fixme", description: message });
6241
+ testInfo.fixme();
6242
+ }
6243
+ throw new Error(message);
6244
+ };
6245
+ const requireResolver = () => {
6246
+ const serverUrl = process.env.PIWI_DASHBOARD_URL;
6247
+ if (!serverUrl) {
6248
+ throw new Error(`piwi AI: ${config.mode} mode needs PIWI_DASHBOARD_URL (the authoring server) to be set`);
6249
+ }
6250
+ return new ServerStepResolver(serverUrl, process.env.PIWI_API_KEY ?? null);
6251
+ };
6252
+ const readAria = (p) => ariaSnapshotBestEffort(p.locator("body"));
6253
+ const step = (title, body) => import_test.test.step(title, body);
6254
+ const canReuse = (entry, kind) => entry?.kind === kind && !(config.mode !== "replay" && config.update);
6255
+ return {
6256
+ piwiLocator(template, ...params) {
6257
+ const values = params[0] ?? {};
6258
+ validateParams(template, params[0]);
6259
+ const file = resolveFile(template);
6260
+ const existing = readEntry(file);
6261
+ if (canReuse(existing, "locator")) {
6262
+ used.add(file);
6263
+ recordIntents(intents, existing);
6264
+ return buildLocator(page, existing.locator, values);
6265
+ }
6266
+ if (config.mode === "replay") return missInReplay(template, file);
6267
+ const resolver = requireResolver();
6268
+ return lazyLocator(async () => {
6269
+ const entry = await resolveLocator(template, {
6270
+ page,
6271
+ params: values,
6272
+ resolver,
6273
+ maxSnapshotChars: config.maxSnapshotChars,
6274
+ screenshotFallback: config.screenshotFallback
6275
+ });
6276
+ writeEntry(file, entry);
6277
+ used.add(file);
6278
+ recordIntents(intents, entry);
6279
+ return buildLocator(page, entry.locator, values);
6280
+ });
6281
+ },
6282
+ async piwiRun(template, ...params) {
6283
+ const values = params[0] ?? {};
6284
+ validateParams(template, params[0]);
6285
+ const file = resolveFile(template);
6286
+ const existing = readEntry(file);
6287
+ if (canReuse(existing, "run")) {
6288
+ used.add(file);
6289
+ recordIntents(intents, existing);
6290
+ await step(
6291
+ `piwiRun: ${template}`,
6292
+ () => executeRun(existing, {
6293
+ page,
6294
+ params: values,
6295
+ readAria,
6296
+ step,
6297
+ optionalProbeTimeout: config.optionalProbeTimeout,
6298
+ responseWaitTimeout: config.responseWaitTimeout
6299
+ })
6300
+ );
6301
+ return;
6302
+ }
6303
+ if (config.mode === "replay") {
6304
+ missInReplay(template, file);
6305
+ return;
6306
+ }
6307
+ const resolver = requireResolver();
6308
+ await step(`piwiRun (resolve): ${template}`, async () => {
6309
+ const entry = await resolveRun(template, {
6310
+ page,
6311
+ params: values,
6312
+ resolver,
6313
+ maxSteps: config.maxSteps,
6314
+ maxSnapshotChars: config.maxSnapshotChars,
6315
+ optionalProbeTimeout: config.optionalProbeTimeout,
6316
+ responseWaitTimeout: config.responseWaitTimeout,
6317
+ screenshotFallback: config.screenshotFallback
6318
+ });
6319
+ writeEntry(file, entry);
6320
+ used.add(file);
6321
+ recordIntents(intents, entry);
6322
+ });
6323
+ }
6324
+ };
6325
+ }
6326
+ async function attachUsage(testInfo, used, intents) {
6327
+ if (used.size === 0) return;
6328
+ const entries = [...used].map(relativeToCwd).sort();
6329
+ const intentList = [...intents.values()].sort(
6330
+ (a, b) => a.template.localeCompare(b.template) || a.locator.localeCompare(b.locator)
6331
+ );
6332
+ await testInfo.attach(ATTACHMENT_NAMES.aiUsage, {
6333
+ contentType: "application/json",
6334
+ body: Buffer.from(JSON.stringify({ entries, ...intentList.length > 0 ? { intents: intentList } : {} }))
6335
+ });
6336
+ }
6337
+ var piwiAiFixtures = {
6338
+ page: async ({ page }, use, testInfo) => {
6339
+ const config = readAiConfig(process.env);
6340
+ const used = /* @__PURE__ */ new Set();
6341
+ const intents = /* @__PURE__ */ new Map();
6342
+ const ai = createAiApi(page, testInfo, config, used, intents);
6343
+ Object.assign(page, ai);
6344
+ try {
6345
+ await use(page);
6346
+ } finally {
6347
+ await attachUsage(testInfo, used, intents);
6348
+ }
6349
+ }
6350
+ };
6351
+ function extendPiwiAi(test) {
6352
+ return test.extend(piwiAiFixtures);
6353
+ }
6354
+
5395
6355
  // src/index.ts
5396
6356
  var index_default = PiwiDashboardReporter;
5397
6357
  // Annotate the CommonJS export names for ESM import in node:
5398
6358
  0 && (module.exports = {
5399
6359
  PiwiDashboardReporter,
5400
6360
  createGlobalSetup,
6361
+ extendPiwiAi,
5401
6362
  extendPiwiFixtures,
6363
+ piwiAiFixtures,
5402
6364
  piwiFixtures,
5403
6365
  wrapConfig
5404
6366
  });