browser-pilot 0.0.11 → 0.0.13

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/actions.cjs CHANGED
@@ -26,6 +26,263 @@ __export(actions_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(actions_exports);
28
28
 
29
+ // src/browser/actionability.ts
30
+ var ActionabilityError = class extends Error {
31
+ failureType;
32
+ coveringElement;
33
+ constructor(message, failureType, coveringElement) {
34
+ super(message);
35
+ this.name = "ActionabilityError";
36
+ this.failureType = failureType;
37
+ this.coveringElement = coveringElement;
38
+ }
39
+ };
40
+
41
+ // src/browser/fuzzy-match.ts
42
+ function jaroWinkler(a, b) {
43
+ if (a.length === 0 && b.length === 0) return 0;
44
+ if (a.length === 0 || b.length === 0) return 0;
45
+ if (a === b) return 1;
46
+ const s1 = a.toLowerCase();
47
+ const s2 = b.toLowerCase();
48
+ const matchWindow = Math.max(0, Math.floor(Math.max(s1.length, s2.length) / 2) - 1);
49
+ const s1Matches = Array.from({ length: s1.length }, () => false);
50
+ const s2Matches = Array.from({ length: s2.length }, () => false);
51
+ let matches = 0;
52
+ let transpositions = 0;
53
+ for (let i = 0; i < s1.length; i++) {
54
+ const start = Math.max(0, i - matchWindow);
55
+ const end = Math.min(i + matchWindow + 1, s2.length);
56
+ for (let j = start; j < end; j++) {
57
+ if (s2Matches[j] || s1[i] !== s2[j]) continue;
58
+ s1Matches[i] = true;
59
+ s2Matches[j] = true;
60
+ matches++;
61
+ break;
62
+ }
63
+ }
64
+ if (matches === 0) return 0;
65
+ let k = 0;
66
+ for (let i = 0; i < s1.length; i++) {
67
+ if (!s1Matches[i]) continue;
68
+ while (!s2Matches[k]) k++;
69
+ if (s1[i] !== s2[k]) transpositions++;
70
+ k++;
71
+ }
72
+ const jaro = (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
73
+ let prefix = 0;
74
+ for (let i = 0; i < Math.min(4, Math.min(s1.length, s2.length)); i++) {
75
+ if (s1[i] === s2[i]) {
76
+ prefix++;
77
+ } else {
78
+ break;
79
+ }
80
+ }
81
+ const WINKLER_SCALING = 0.1;
82
+ return jaro + prefix * WINKLER_SCALING * (1 - jaro);
83
+ }
84
+ function stringSimilarity(a, b) {
85
+ if (a.length === 0 || b.length === 0) return 0;
86
+ const lowerA = a.toLowerCase();
87
+ const lowerB = b.toLowerCase();
88
+ if (lowerA === lowerB) return 1;
89
+ const jw = jaroWinkler(a, b);
90
+ let containsBonus = 0;
91
+ if (lowerB.includes(lowerA)) {
92
+ containsBonus = 0.2;
93
+ } else if (lowerA.includes(lowerB)) {
94
+ containsBonus = 0.1;
95
+ }
96
+ return Math.min(1, jw + containsBonus);
97
+ }
98
+ function scoreElement(query, element) {
99
+ const lowerQuery = query.toLowerCase();
100
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
101
+ let nameScore = 0;
102
+ if (element.name) {
103
+ const lowerName = element.name.toLowerCase();
104
+ if (lowerName === lowerQuery) {
105
+ nameScore = 1;
106
+ } else if (lowerName.includes(lowerQuery)) {
107
+ nameScore = 0.8;
108
+ } else if (words.length > 0) {
109
+ const matchedWords = words.filter((w) => lowerName.includes(w));
110
+ nameScore = matchedWords.length / words.length * 0.7;
111
+ } else {
112
+ nameScore = stringSimilarity(query, element.name) * 0.6;
113
+ }
114
+ }
115
+ let roleScore = 0;
116
+ const lowerRole = element.role.toLowerCase();
117
+ if (lowerRole === lowerQuery || lowerQuery.includes(lowerRole)) {
118
+ roleScore = 0.3;
119
+ } else if (words.some((w) => lowerRole.includes(w))) {
120
+ roleScore = 0.2;
121
+ }
122
+ let selectorScore = 0;
123
+ const lowerSelector = element.selector.toLowerCase();
124
+ if (words.some((w) => lowerSelector.includes(w))) {
125
+ selectorScore = 0.2;
126
+ }
127
+ const totalScore = nameScore * 0.6 + roleScore * 0.25 + selectorScore * 0.15;
128
+ return totalScore;
129
+ }
130
+ function explainMatch(query, element, score) {
131
+ const reasons = [];
132
+ const lowerQuery = query.toLowerCase();
133
+ const words = lowerQuery.split(/\s+/).filter((w) => w.length > 0);
134
+ if (element.name) {
135
+ const lowerName = element.name.toLowerCase();
136
+ if (lowerName === lowerQuery) {
137
+ reasons.push("exact name match");
138
+ } else if (lowerName.includes(lowerQuery)) {
139
+ reasons.push("name contains query");
140
+ } else if (words.some((w) => lowerName.includes(w))) {
141
+ const matchedWords = words.filter((w) => lowerName.includes(w));
142
+ reasons.push(`name contains: ${matchedWords.join(", ")}`);
143
+ } else if (stringSimilarity(query, element.name) > 0.5) {
144
+ reasons.push("similar name");
145
+ }
146
+ }
147
+ const lowerRole = element.role.toLowerCase();
148
+ if (lowerRole === lowerQuery || words.some((w) => w === lowerRole)) {
149
+ reasons.push(`role: ${element.role}`);
150
+ }
151
+ if (words.some((w) => element.selector.toLowerCase().includes(w))) {
152
+ reasons.push("selector match");
153
+ }
154
+ if (reasons.length === 0) {
155
+ reasons.push(`fuzzy match (score: ${score.toFixed(2)})`);
156
+ }
157
+ return reasons.join(", ");
158
+ }
159
+ function fuzzyMatchElements(query, elements, maxResults = 5) {
160
+ if (!query || query.length === 0) {
161
+ return [];
162
+ }
163
+ const THRESHOLD = 0.3;
164
+ const scored = elements.map((element) => ({
165
+ element,
166
+ score: scoreElement(query, element)
167
+ }));
168
+ return scored.filter((s) => s.score >= THRESHOLD).sort((a, b) => b.score - a.score).slice(0, maxResults).map((s) => ({
169
+ element: s.element,
170
+ score: s.score,
171
+ matchReason: explainMatch(query, s.element, s.score)
172
+ }));
173
+ }
174
+
175
+ // src/browser/hint-generator.ts
176
+ var ACTION_ROLE_MAP = {
177
+ click: ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"],
178
+ fill: ["textbox", "searchbox", "textarea"],
179
+ type: ["textbox", "searchbox", "textarea"],
180
+ submit: ["button", "form"],
181
+ select: ["combobox", "listbox", "option"],
182
+ check: ["checkbox", "radio", "switch"],
183
+ uncheck: ["checkbox", "switch"],
184
+ focus: [],
185
+ // Any focusable element
186
+ hover: [],
187
+ // Any element
188
+ clear: ["textbox", "searchbox", "textarea"]
189
+ };
190
+ function extractIntent(selectors) {
191
+ const patterns = [];
192
+ let text = "";
193
+ for (const selector of selectors) {
194
+ if (selector.startsWith("ref:")) {
195
+ continue;
196
+ }
197
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
198
+ if (idMatch) {
199
+ patterns.push(idMatch[1]);
200
+ }
201
+ const ariaMatch = selector.match(/\[aria-label=["']([^"']+)["']\]/);
202
+ if (ariaMatch) {
203
+ patterns.push(ariaMatch[1]);
204
+ }
205
+ const testidMatch = selector.match(/\[data-testid=["']([^"']+)["']\]/);
206
+ if (testidMatch) {
207
+ patterns.push(testidMatch[1]);
208
+ }
209
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
210
+ if (classMatch) {
211
+ patterns.push(classMatch[1]);
212
+ }
213
+ }
214
+ patterns.sort((a, b) => b.length - a.length);
215
+ text = patterns[0] ?? selectors[0] ?? "";
216
+ return { text, patterns };
217
+ }
218
+ function getHintType(selector) {
219
+ if (selector.startsWith("ref:")) return "ref";
220
+ if (selector.includes("data-testid")) return "testid";
221
+ if (selector.includes("aria-label")) return "aria";
222
+ if (selector.startsWith("#")) return "id";
223
+ return "css";
224
+ }
225
+ function getConfidence(score) {
226
+ if (score >= 0.8) return "high";
227
+ if (score >= 0.5) return "medium";
228
+ return "low";
229
+ }
230
+ function diversifyHints(candidates, maxHints) {
231
+ const hints = [];
232
+ const usedTypes = /* @__PURE__ */ new Set();
233
+ for (const candidate of candidates) {
234
+ if (hints.length >= maxHints) break;
235
+ const refSelector = `ref:${candidate.element.ref}`;
236
+ const hintType = getHintType(refSelector);
237
+ if (!usedTypes.has(hintType)) {
238
+ hints.push({
239
+ selector: refSelector,
240
+ reason: candidate.matchReason,
241
+ confidence: getConfidence(candidate.score),
242
+ element: {
243
+ ref: candidate.element.ref,
244
+ role: candidate.element.role,
245
+ name: candidate.element.name,
246
+ disabled: candidate.element.disabled
247
+ }
248
+ });
249
+ usedTypes.add(hintType);
250
+ } else if (hints.length < maxHints) {
251
+ hints.push({
252
+ selector: refSelector,
253
+ reason: candidate.matchReason,
254
+ confidence: getConfidence(candidate.score),
255
+ element: {
256
+ ref: candidate.element.ref,
257
+ role: candidate.element.role,
258
+ name: candidate.element.name,
259
+ disabled: candidate.element.disabled
260
+ }
261
+ });
262
+ }
263
+ }
264
+ return hints;
265
+ }
266
+ async function generateHints(page, failedSelectors, actionType, maxHints = 3) {
267
+ let snapshot;
268
+ try {
269
+ snapshot = await page.snapshot();
270
+ } catch {
271
+ return [];
272
+ }
273
+ const intent = extractIntent(failedSelectors);
274
+ const roleFilter = ACTION_ROLE_MAP[actionType] ?? [];
275
+ let candidates = snapshot.interactiveElements;
276
+ if (roleFilter.length > 0) {
277
+ candidates = candidates.filter((el) => roleFilter.includes(el.role));
278
+ }
279
+ const matches = fuzzyMatchElements(intent.text, candidates, maxHints * 2);
280
+ if (matches.length === 0) {
281
+ return [];
282
+ }
283
+ return diversifyHints(matches, maxHints);
284
+ }
285
+
29
286
  // src/browser/types.ts
30
287
  var ElementNotFoundError = class extends Error {
31
288
  selectors;
@@ -43,9 +300,101 @@ var ElementNotFoundError = class extends Error {
43
300
  this.hints = hints;
44
301
  }
45
302
  };
303
+ var TimeoutError = class extends Error {
304
+ constructor(message = "Operation timed out") {
305
+ const msg = message.includes("bp snapshot") ? message : `${message}. Run 'bp snapshot' to check current page state.`;
306
+ super(msg);
307
+ this.name = "TimeoutError";
308
+ }
309
+ };
310
+ var NavigationError = class extends Error {
311
+ constructor(message) {
312
+ super(message);
313
+ this.name = "NavigationError";
314
+ }
315
+ };
316
+
317
+ // src/cdp/protocol.ts
318
+ var CDPError = class extends Error {
319
+ code;
320
+ data;
321
+ constructor(error) {
322
+ super(error.message);
323
+ this.name = "CDPError";
324
+ this.code = error.code;
325
+ this.data = error.data;
326
+ }
327
+ };
46
328
 
47
329
  // src/actions/executor.ts
48
330
  var DEFAULT_TIMEOUT = 3e4;
331
+ function classifyFailure(error) {
332
+ if (error instanceof ElementNotFoundError) {
333
+ return { reason: "missing" };
334
+ }
335
+ if (error instanceof ActionabilityError) {
336
+ switch (error.failureType) {
337
+ case "visible":
338
+ return { reason: "hidden" };
339
+ case "hitTarget":
340
+ return { reason: "covered", coveringElement: error.coveringElement };
341
+ case "enabled":
342
+ return { reason: "disabled" };
343
+ case "editable":
344
+ return { reason: error.message?.includes("readonly") ? "readonly" : "notEditable" };
345
+ case "stable":
346
+ return { reason: "replaced" };
347
+ default:
348
+ return { reason: "unknown" };
349
+ }
350
+ }
351
+ if (error instanceof TimeoutError) {
352
+ return { reason: "timeout" };
353
+ }
354
+ if (error instanceof NavigationError) {
355
+ return { reason: "navigation" };
356
+ }
357
+ if (error instanceof CDPError) {
358
+ return { reason: "cdpError" };
359
+ }
360
+ const msg = String(error?.message ?? error);
361
+ if (msg.includes("Could not find node") || msg.includes("does not belong to the document")) {
362
+ return { reason: "detached" };
363
+ }
364
+ return { reason: "unknown" };
365
+ }
366
+ function getSuggestion(reason) {
367
+ switch (reason) {
368
+ case "missing":
369
+ return "Element not found. Run 'snapshot' to see available elements, or try alternative selectors.";
370
+ case "hidden":
371
+ return "Element exists but is not visible. Try 'scroll' or wait for it to appear.";
372
+ case "covered":
373
+ return "Element is blocked by another element. Dismiss the covering element first.";
374
+ case "disabled":
375
+ return "Element is disabled. Complete prerequisite steps to enable it.";
376
+ case "readonly":
377
+ return "Element is readonly and cannot be edited directly.";
378
+ case "detached":
379
+ return "Element was removed from the DOM. Run 'snapshot' for fresh element refs.";
380
+ case "replaced":
381
+ return "Element was replaced in the DOM. Run 'snapshot' to get updated refs.";
382
+ case "notEditable":
383
+ return "Element is not an editable field. Try a different selector targeting an input or textarea.";
384
+ case "timeout":
385
+ return "Timed out waiting. The page may still be loading. Try increasing timeout.";
386
+ case "navigation":
387
+ return "Navigation failed. Check the URL and network connectivity.";
388
+ case "cdpError":
389
+ return "Browser connection error. Try 'bp connect' again.";
390
+ case "unknown":
391
+ return "Unexpected error. Run 'snapshot' to check page state.";
392
+ default: {
393
+ const _exhaustive = reason;
394
+ return `Unknown failure: ${_exhaustive}`;
395
+ }
396
+ }
397
+ }
49
398
  var BatchExecutor = class {
50
399
  page;
51
400
  constructor(page) {
@@ -61,21 +410,46 @@ var BatchExecutor = class {
61
410
  for (let i = 0; i < steps.length; i++) {
62
411
  const step = steps[i];
63
412
  const stepStart = Date.now();
64
- try {
65
- const result = await this.executeStep(step, timeout);
66
- results.push({
67
- index: i,
68
- action: step.action,
69
- selector: step.selector,
70
- selectorUsed: result.selectorUsed,
71
- success: true,
72
- durationMs: Date.now() - stepStart,
73
- result: result.value,
74
- text: result.text
75
- });
76
- } catch (error) {
77
- const errorMessage = error instanceof Error ? error.message : String(error);
78
- const hints = error instanceof ElementNotFoundError ? error.hints : void 0;
413
+ const maxAttempts = (step.retry ?? 0) + 1;
414
+ const retryDelay = step.retryDelay ?? 500;
415
+ let lastError;
416
+ let succeeded = false;
417
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
418
+ if (attempt > 0) {
419
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
420
+ }
421
+ try {
422
+ const result = await this.executeStep(step, timeout);
423
+ results.push({
424
+ index: i,
425
+ action: step.action,
426
+ selector: step.selector,
427
+ selectorUsed: result.selectorUsed,
428
+ success: true,
429
+ durationMs: Date.now() - stepStart,
430
+ result: result.value,
431
+ text: result.text
432
+ });
433
+ succeeded = true;
434
+ break;
435
+ } catch (error) {
436
+ lastError = error instanceof Error ? error : new Error(String(error));
437
+ }
438
+ }
439
+ if (!succeeded) {
440
+ const errorMessage = lastError?.message ?? "Unknown error";
441
+ let hints = lastError instanceof ElementNotFoundError ? lastError.hints : void 0;
442
+ const { reason, coveringElement } = classifyFailure(lastError);
443
+ if (step.selector && !step.optional && ["missing", "hidden", "covered", "disabled", "detached", "replaced"].includes(reason)) {
444
+ try {
445
+ const selectors = Array.isArray(step.selector) ? step.selector : [step.selector];
446
+ const autoHints = await generateHints(this.page, selectors, step.action, 3);
447
+ if (autoHints.length > 0) {
448
+ hints = autoHints;
449
+ }
450
+ } catch {
451
+ }
452
+ }
79
453
  results.push({
80
454
  index: i,
81
455
  action: step.action,
@@ -83,7 +457,10 @@ var BatchExecutor = class {
83
457
  success: false,
84
458
  durationMs: Date.now() - stepStart,
85
459
  error: errorMessage,
86
- hints
460
+ hints,
461
+ failureReason: reason,
462
+ coveringElement,
463
+ suggestion: getSuggestion(reason)
87
464
  });
88
465
  if (onFail === "stop" && !step.optional) {
89
466
  return {
@@ -185,7 +562,24 @@ var BatchExecutor = class {
185
562
  }
186
563
  case "press": {
187
564
  if (!step.key) throw new Error("press requires key");
188
- await this.page.press(step.key);
565
+ try {
566
+ await this.page.press(step.key, {
567
+ modifiers: step.modifiers
568
+ });
569
+ } catch (e) {
570
+ if (optional) return {};
571
+ throw e;
572
+ }
573
+ return {};
574
+ }
575
+ case "shortcut": {
576
+ if (!step.combo) throw new Error("shortcut requires combo");
577
+ try {
578
+ await this.page.shortcut(step.combo);
579
+ } catch (e) {
580
+ if (optional) return {};
581
+ throw e;
582
+ }
189
583
  return {};
190
584
  }
191
585
  case "focus": {
@@ -244,6 +638,9 @@ var BatchExecutor = class {
244
638
  const snapshot = await this.page.snapshot();
245
639
  return { value: snapshot };
246
640
  }
641
+ case "forms": {
642
+ return { value: await this.page.forms() };
643
+ }
247
644
  case "screenshot": {
248
645
  const data = await this.page.screenshot({
249
646
  format: step.format,
@@ -263,6 +660,21 @@ var BatchExecutor = class {
263
660
  const text = await this.page.text(selector);
264
661
  return { text, selectorUsed: selector };
265
662
  }
663
+ case "newTab": {
664
+ const { targetId } = await this.page.cdpClient.send(
665
+ "Target.createTarget",
666
+ {
667
+ url: step.url ?? "about:blank"
668
+ },
669
+ null
670
+ );
671
+ return { value: { targetId } };
672
+ }
673
+ case "closeTab": {
674
+ const targetId = step.targetId ?? this.page.targetId;
675
+ await this.page.cdpClient.send("Target.closeTarget", { targetId }, null);
676
+ return { value: { targetId, closedCurrent: targetId === this.page.targetId } };
677
+ }
266
678
  case "switchFrame": {
267
679
  if (!step.selector) throw new Error("switchFrame requires selector");
268
680
  await this.page.switchToFrame(step.selector, { timeout, optional });
@@ -272,6 +684,80 @@ var BatchExecutor = class {
272
684
  await this.page.switchToMain();
273
685
  return {};
274
686
  }
687
+ case "assertVisible": {
688
+ if (!step.selector) throw new Error("assertVisible requires selector");
689
+ const el = await this.page.waitFor(step.selector, {
690
+ timeout,
691
+ optional: true,
692
+ state: "visible"
693
+ });
694
+ if (!el) {
695
+ throw new Error(
696
+ `Assertion failed: selector ${JSON.stringify(step.selector)} is not visible`
697
+ );
698
+ }
699
+ return { selectorUsed: this.getUsedSelector(step.selector) };
700
+ }
701
+ case "assertExists": {
702
+ if (!step.selector) throw new Error("assertExists requires selector");
703
+ const el = await this.page.waitFor(step.selector, {
704
+ timeout,
705
+ optional: true,
706
+ state: "attached"
707
+ });
708
+ if (!el) {
709
+ throw new Error(
710
+ `Assertion failed: selector ${JSON.stringify(step.selector)} does not exist`
711
+ );
712
+ }
713
+ return { selectorUsed: this.getUsedSelector(step.selector) };
714
+ }
715
+ case "assertText": {
716
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
717
+ const text = await this.page.text(selector);
718
+ const expected = step.expect ?? step.value;
719
+ if (typeof expected !== "string") throw new Error("assertText requires expect or value");
720
+ if (!text.includes(expected)) {
721
+ throw new Error(
722
+ `Assertion failed: text does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(text.slice(0, 200))}`
723
+ );
724
+ }
725
+ return { selectorUsed: selector, text };
726
+ }
727
+ case "assertUrl": {
728
+ const currentUrl = await this.page.url();
729
+ const expected = step.expect ?? step.url;
730
+ if (typeof expected !== "string") throw new Error("assertUrl requires expect or url");
731
+ if (!currentUrl.includes(expected)) {
732
+ throw new Error(
733
+ `Assertion failed: URL does not contain ${JSON.stringify(expected)}. Got: ${JSON.stringify(currentUrl)}`
734
+ );
735
+ }
736
+ return { value: currentUrl };
737
+ }
738
+ case "assertValue": {
739
+ if (!step.selector) throw new Error("assertValue requires selector");
740
+ const expected = step.expect ?? step.value;
741
+ if (typeof expected !== "string") throw new Error("assertValue requires expect or value");
742
+ const found = await this.page.waitFor(step.selector, {
743
+ timeout,
744
+ optional: true,
745
+ state: "attached"
746
+ });
747
+ if (!found) {
748
+ throw new Error(`Assertion failed: selector ${JSON.stringify(step.selector)} not found`);
749
+ }
750
+ const usedSelector = this.getUsedSelector(step.selector);
751
+ const actual = await this.page.evaluate(
752
+ `(function() { var el = document.querySelector(${JSON.stringify(usedSelector)}); return el ? el.value : null; })()`
753
+ );
754
+ if (actual !== expected) {
755
+ throw new Error(
756
+ `Assertion failed: value of ${JSON.stringify(usedSelector)} is ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`
757
+ );
758
+ }
759
+ return { selectorUsed: usedSelector, value: actual };
760
+ }
275
761
  default: {
276
762
  const action = step.action;
277
763
  const aliases = {
@@ -284,16 +770,48 @@ var BatchExecutor = class {
284
770
  capture: "screenshot",
285
771
  inspect: "snapshot",
286
772
  enter: "press",
773
+ keypress: "press",
774
+ hotkey: "shortcut",
775
+ keybinding: "shortcut",
776
+ nav: "goto",
287
777
  open: "goto",
288
778
  visit: "goto",
779
+ browse: "goto",
780
+ load: "goto",
781
+ write: "fill",
782
+ set: "fill",
783
+ pick: "select",
784
+ choose: "select",
785
+ send: "press",
289
786
  eval: "evaluate",
290
787
  js: "evaluate",
788
+ script: "evaluate",
291
789
  snap: "snapshot",
292
- frame: "switchFrame"
790
+ accessibility: "snapshot",
791
+ a11y: "snapshot",
792
+ formslist: "forms",
793
+ image: "screenshot",
794
+ pic: "screenshot",
795
+ frame: "switchFrame",
796
+ iframe: "switchFrame",
797
+ newtab: "newTab",
798
+ opentab: "newTab",
799
+ createtab: "newTab",
800
+ closetab: "closeTab",
801
+ assert_visible: "assertVisible",
802
+ assert_exists: "assertExists",
803
+ assert_text: "assertText",
804
+ assert_url: "assertUrl",
805
+ assert_value: "assertValue",
806
+ checkvisible: "assertVisible",
807
+ checkexists: "assertExists",
808
+ checktext: "assertText",
809
+ checkurl: "assertUrl",
810
+ checkvalue: "assertValue"
293
811
  };
294
812
  const suggestion = aliases[action.toLowerCase()];
295
813
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
296
- const valid = "goto, click, fill, type, select, check, uncheck, submit, press, focus, hover, scroll, wait, snapshot, screenshot, evaluate, text, switchFrame, switchToMain";
814
+ const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
297
815
  throw new Error(`Unknown action "${action}".${hint}
298
816
 
299
817
  Valid actions: ${valid}`);
@@ -342,6 +860,8 @@ var ACTION_ALIASES = {
342
860
  inspect: "snapshot",
343
861
  enter: "press",
344
862
  keypress: "press",
863
+ hotkey: "shortcut",
864
+ keybinding: "shortcut",
345
865
  nav: "goto",
346
866
  open: "goto",
347
867
  visit: "goto",
@@ -361,7 +881,22 @@ var ACTION_ALIASES = {
361
881
  image: "screenshot",
362
882
  pic: "screenshot",
363
883
  frame: "switchFrame",
364
- iframe: "switchFrame"
884
+ iframe: "switchFrame",
885
+ formslist: "forms",
886
+ newtab: "newTab",
887
+ opentab: "newTab",
888
+ createtab: "newTab",
889
+ closetab: "closeTab",
890
+ assert_visible: "assertVisible",
891
+ assert_exists: "assertExists",
892
+ assert_text: "assertText",
893
+ assert_url: "assertUrl",
894
+ assert_value: "assertValue",
895
+ checkvisible: "assertVisible",
896
+ checkexists: "assertExists",
897
+ checktext: "assertText",
898
+ checkurl: "assertUrl",
899
+ checkvalue: "assertValue"
365
900
  };
366
901
  var PROPERTY_ALIASES = {
367
902
  expression: "value",
@@ -382,10 +917,14 @@ var PROPERTY_ALIASES = {
382
917
  input: "value",
383
918
  content: "value",
384
919
  keys: "key",
920
+ shortcutKey: "combo",
921
+ hotkey: "combo",
922
+ keybinding: "combo",
385
923
  button: "key",
386
924
  address: "url",
387
925
  page: "url",
388
- path: "url"
926
+ path: "url",
927
+ tabId: "targetId"
389
928
  };
390
929
  var ACTION_RULES = {
391
930
  goto: {
@@ -395,7 +934,7 @@ var ACTION_RULES = {
395
934
  click: {
396
935
  required: { selector: { type: "string|string[]" } },
397
936
  optional: {
398
- waitForNavigation: { type: "boolean" }
937
+ waitForNavigation: { type: "boolean|auto" }
399
938
  }
400
939
  },
401
940
  fill: {
@@ -407,7 +946,8 @@ var ACTION_RULES = {
407
946
  type: {
408
947
  required: { selector: { type: "string|string[]" }, value: { type: "string" } },
409
948
  optional: {
410
- delay: { type: "number" }
949
+ delay: { type: "number" },
950
+ blur: { type: "boolean" }
411
951
  }
412
952
  },
413
953
  select: {
@@ -437,6 +977,12 @@ var ACTION_RULES = {
437
977
  },
438
978
  press: {
439
979
  required: { key: { type: "string" } },
980
+ optional: {
981
+ modifiers: { type: "string|string[]" }
982
+ }
983
+ },
984
+ shortcut: {
985
+ required: { combo: { type: "string" } },
440
986
  optional: {}
441
987
  },
442
988
  focus: {
@@ -479,6 +1025,10 @@ var ACTION_RULES = {
479
1025
  fullPage: { type: "boolean" }
480
1026
  }
481
1027
  },
1028
+ forms: {
1029
+ required: {},
1030
+ optional: {}
1031
+ },
482
1032
  evaluate: {
483
1033
  required: { value: { type: "string" } },
484
1034
  optional: {}
@@ -493,9 +1043,51 @@ var ACTION_RULES = {
493
1043
  required: { selector: { type: "string|string[]" } },
494
1044
  optional: {}
495
1045
  },
1046
+ newTab: {
1047
+ required: {},
1048
+ optional: {
1049
+ url: { type: "string" }
1050
+ }
1051
+ },
1052
+ closeTab: {
1053
+ required: {},
1054
+ optional: {
1055
+ targetId: { type: "string" }
1056
+ }
1057
+ },
496
1058
  switchToMain: {
497
1059
  required: {},
498
1060
  optional: {}
1061
+ },
1062
+ assertVisible: {
1063
+ required: { selector: { type: "string|string[]" } },
1064
+ optional: {}
1065
+ },
1066
+ assertExists: {
1067
+ required: { selector: { type: "string|string[]" } },
1068
+ optional: {}
1069
+ },
1070
+ assertText: {
1071
+ required: {},
1072
+ optional: {
1073
+ selector: { type: "string|string[]" },
1074
+ expect: { type: "string" },
1075
+ value: { type: "string" }
1076
+ }
1077
+ },
1078
+ assertUrl: {
1079
+ required: {},
1080
+ optional: {
1081
+ expect: { type: "string" },
1082
+ url: { type: "string" }
1083
+ }
1084
+ },
1085
+ assertValue: {
1086
+ required: { selector: { type: "string|string[]" } },
1087
+ optional: {
1088
+ expect: { type: "string" },
1089
+ value: { type: "string" }
1090
+ }
499
1091
  }
500
1092
  };
501
1093
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -505,7 +1097,10 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
505
1097
  "selector",
506
1098
  "url",
507
1099
  "value",
1100
+ "targetId",
508
1101
  "key",
1102
+ "combo",
1103
+ "modifiers",
509
1104
  "waitFor",
510
1105
  "timeout",
511
1106
  "optional",
@@ -522,7 +1117,10 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
522
1117
  "amount",
523
1118
  "format",
524
1119
  "quality",
525
- "fullPage"
1120
+ "fullPage",
1121
+ "expect",
1122
+ "retry",
1123
+ "retryDelay"
526
1124
  ]);
527
1125
  function resolveAction(name) {
528
1126
  if (VALID_ACTIONS.includes(name)) {
@@ -595,6 +1193,10 @@ function checkFieldType(value, rule) {
595
1193
  return `expected boolean or "auto", got ${typeof value}`;
596
1194
  }
597
1195
  return null;
1196
+ default: {
1197
+ const _exhaustive = rule.type;
1198
+ return `unknown type: ${_exhaustive}`;
1199
+ }
598
1200
  }
599
1201
  }
600
1202
  function validateSteps(steps) {
@@ -650,15 +1252,22 @@ function validateSteps(steps) {
650
1252
  const rule = ACTION_RULES[action];
651
1253
  for (const key of Object.keys(obj)) {
652
1254
  if (key === "action") continue;
653
- if (!KNOWN_STEP_FIELDS.has(key)) {
654
- const suggestion = suggestProperty(key);
655
- errors.push({
656
- stepIndex: i,
657
- field: key,
658
- message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
659
- suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
660
- });
1255
+ if (KNOWN_STEP_FIELDS.has(key)) continue;
1256
+ const canonical = PROPERTY_ALIASES[key];
1257
+ if (canonical) {
1258
+ if (!(canonical in obj)) {
1259
+ obj[canonical] = obj[key];
1260
+ }
1261
+ delete obj[key];
1262
+ continue;
661
1263
  }
1264
+ const suggestion = suggestProperty(key);
1265
+ errors.push({
1266
+ stepIndex: i,
1267
+ field: key,
1268
+ message: suggestion ? `unknown property "${key}". Did you mean "${suggestion}"?` : `unknown property "${key}".`,
1269
+ suggestion: suggestion ? `Did you mean "${suggestion}"?` : void 0
1270
+ });
662
1271
  }
663
1272
  for (const [field, fieldRule] of Object.entries(rule.required)) {
664
1273
  if (!(field in obj) || obj[field] === void 0) {
@@ -708,6 +1317,51 @@ function validateSteps(steps) {
708
1317
  });
709
1318
  }
710
1319
  }
1320
+ if ("retry" in obj && obj["retry"] !== void 0) {
1321
+ if (typeof obj["retry"] !== "number") {
1322
+ errors.push({
1323
+ stepIndex: i,
1324
+ field: "retry",
1325
+ message: `"retry" expected number, got ${typeof obj["retry"]}.`
1326
+ });
1327
+ }
1328
+ }
1329
+ if ("retryDelay" in obj && obj["retryDelay"] !== void 0) {
1330
+ if (typeof obj["retryDelay"] !== "number") {
1331
+ errors.push({
1332
+ stepIndex: i,
1333
+ field: "retryDelay",
1334
+ message: `"retryDelay" expected number, got ${typeof obj["retryDelay"]}.`
1335
+ });
1336
+ }
1337
+ }
1338
+ if (action === "assertText") {
1339
+ if (!("expect" in obj) && !("value" in obj)) {
1340
+ errors.push({
1341
+ stepIndex: i,
1342
+ field: "expect",
1343
+ message: 'assertText requires "expect" or "value" containing the expected text.'
1344
+ });
1345
+ }
1346
+ }
1347
+ if (action === "assertUrl") {
1348
+ if (!("expect" in obj) && !("url" in obj)) {
1349
+ errors.push({
1350
+ stepIndex: i,
1351
+ field: "expect",
1352
+ message: 'assertUrl requires "expect" or "url" containing the expected URL substring.'
1353
+ });
1354
+ }
1355
+ }
1356
+ if (action === "assertValue") {
1357
+ if (!("expect" in obj) && !("value" in obj)) {
1358
+ errors.push({
1359
+ stepIndex: i,
1360
+ field: "expect",
1361
+ message: 'assertValue requires "expect" or "value" containing the expected value.'
1362
+ });
1363
+ }
1364
+ }
711
1365
  if (action === "select") {
712
1366
  const hasNative = "selector" in obj && "value" in obj;
713
1367
  const hasCustom = "trigger" in obj && "option" in obj && "value" in obj;