btcp-browser-agent 0.1.0 → 0.1.1

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.
Files changed (136) hide show
  1. package/package.json +8 -9
  2. package/packages/core/dist/actions.d.ts +97 -0
  3. package/packages/core/dist/actions.js +940 -0
  4. package/packages/core/dist/errors.d.ts +138 -0
  5. package/packages/core/dist/errors.js +157 -0
  6. package/packages/core/dist/index.d.ts +120 -0
  7. package/packages/core/dist/index.js +134 -0
  8. package/packages/core/dist/ref-map.d.ts +16 -0
  9. package/packages/core/dist/ref-map.js +91 -0
  10. package/packages/core/dist/snapshot.d.ts +37 -0
  11. package/packages/core/dist/snapshot.js +751 -0
  12. package/packages/core/dist/types.d.ts +396 -0
  13. package/packages/core/dist/types.js +7 -0
  14. package/packages/extension/dist/background.d.ts +227 -0
  15. package/packages/extension/dist/background.js +737 -0
  16. package/packages/extension/dist/content.d.ts +18 -0
  17. package/packages/extension/dist/content.js +149 -0
  18. package/packages/extension/dist/index.d.ts +228 -0
  19. package/packages/extension/dist/index.js +350 -0
  20. package/packages/extension/dist/session-manager.d.ts +87 -0
  21. package/packages/extension/dist/session-manager.js +322 -0
  22. package/packages/extension/{src/session-types.ts → dist/session-types.d.ts} +113 -144
  23. package/packages/extension/dist/session-types.js +5 -0
  24. package/packages/extension/dist/types.d.ts +88 -0
  25. package/packages/extension/dist/types.js +7 -0
  26. package/CLAUDE.md +0 -230
  27. package/SKILL.md +0 -143
  28. package/SNAPSHOT_IMPROVEMENTS.md +0 -302
  29. package/USAGE.md +0 -146
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js.map +0 -1
  32. package/docs/browser-cli-design.md +0 -500
  33. package/examples/chrome-extension/CHANGELOG.md +0 -210
  34. package/examples/chrome-extension/DEBUG.md +0 -231
  35. package/examples/chrome-extension/ERROR_FIXED.md +0 -147
  36. package/examples/chrome-extension/QUICK_TEST.md +0 -189
  37. package/examples/chrome-extension/README.md +0 -149
  38. package/examples/chrome-extension/SESSION_ONLY_MODE.md +0 -305
  39. package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +0 -97
  40. package/examples/chrome-extension/build.js +0 -43
  41. package/examples/chrome-extension/manifest.json +0 -37
  42. package/examples/chrome-extension/package-lock.json +0 -1063
  43. package/examples/chrome-extension/package.json +0 -21
  44. package/examples/chrome-extension/popup.html +0 -195
  45. package/examples/chrome-extension/src/background.ts +0 -12
  46. package/examples/chrome-extension/src/content.ts +0 -7
  47. package/examples/chrome-extension/src/popup.ts +0 -303
  48. package/examples/chrome-extension/src/scenario-google-github.ts +0 -389
  49. package/examples/chrome-extension/test-page.html +0 -127
  50. package/examples/chrome-extension/tests/README.md +0 -206
  51. package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +0 -380
  52. package/examples/chrome-extension/tsconfig.json +0 -14
  53. package/examples/snapshots/README.md +0 -207
  54. package/examples/snapshots/amazon-com-detail.html +0 -9528
  55. package/examples/snapshots/amazon-com-detail.snapshot.txt +0 -997
  56. package/examples/snapshots/convert-snapshots.ts +0 -97
  57. package/examples/snapshots/edition-cnn-com.html +0 -13292
  58. package/examples/snapshots/edition-cnn-com.snapshot.txt +0 -562
  59. package/examples/snapshots/github-com-microsoft-vscode.html +0 -2916
  60. package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +0 -455
  61. package/examples/snapshots/google-search.html +0 -20012
  62. package/examples/snapshots/google-search.snapshot.txt +0 -195
  63. package/examples/snapshots/metadata.json +0 -86
  64. package/examples/snapshots/npr-org-templates.html +0 -2031
  65. package/examples/snapshots/npr-org-templates.snapshot.txt +0 -224
  66. package/examples/snapshots/stackoverflow-com.html +0 -5216
  67. package/examples/snapshots/stackoverflow-com.snapshot.txt +0 -2404
  68. package/examples/snapshots/test-all-mode.html +0 -46
  69. package/examples/snapshots/test-all-mode.snapshot.txt +0 -5
  70. package/examples/snapshots/validate.test.ts +0 -296
  71. package/packages/cli/package.json +0 -42
  72. package/packages/cli/src/__tests__/cli.test.ts +0 -434
  73. package/packages/cli/src/__tests__/errors.test.ts +0 -226
  74. package/packages/cli/src/__tests__/executor.test.ts +0 -275
  75. package/packages/cli/src/__tests__/formatter.test.ts +0 -260
  76. package/packages/cli/src/__tests__/parser.test.ts +0 -288
  77. package/packages/cli/src/__tests__/suggestions.test.ts +0 -255
  78. package/packages/cli/src/commands/back.ts +0 -22
  79. package/packages/cli/src/commands/check.ts +0 -33
  80. package/packages/cli/src/commands/clear.ts +0 -33
  81. package/packages/cli/src/commands/click.ts +0 -32
  82. package/packages/cli/src/commands/closetab.ts +0 -31
  83. package/packages/cli/src/commands/eval.ts +0 -41
  84. package/packages/cli/src/commands/fill.ts +0 -30
  85. package/packages/cli/src/commands/focus.ts +0 -33
  86. package/packages/cli/src/commands/forward.ts +0 -22
  87. package/packages/cli/src/commands/goto.ts +0 -34
  88. package/packages/cli/src/commands/help.ts +0 -162
  89. package/packages/cli/src/commands/hover.ts +0 -34
  90. package/packages/cli/src/commands/index.ts +0 -129
  91. package/packages/cli/src/commands/newtab.ts +0 -35
  92. package/packages/cli/src/commands/press.ts +0 -40
  93. package/packages/cli/src/commands/reload.ts +0 -25
  94. package/packages/cli/src/commands/screenshot.ts +0 -27
  95. package/packages/cli/src/commands/scroll.ts +0 -64
  96. package/packages/cli/src/commands/select.ts +0 -35
  97. package/packages/cli/src/commands/snapshot.ts +0 -21
  98. package/packages/cli/src/commands/tab.ts +0 -32
  99. package/packages/cli/src/commands/tabs.ts +0 -26
  100. package/packages/cli/src/commands/text.ts +0 -27
  101. package/packages/cli/src/commands/title.ts +0 -17
  102. package/packages/cli/src/commands/type.ts +0 -38
  103. package/packages/cli/src/commands/uncheck.ts +0 -33
  104. package/packages/cli/src/commands/url.ts +0 -17
  105. package/packages/cli/src/commands/wait.ts +0 -54
  106. package/packages/cli/src/errors.ts +0 -164
  107. package/packages/cli/src/executor.ts +0 -68
  108. package/packages/cli/src/formatter.ts +0 -215
  109. package/packages/cli/src/index.ts +0 -257
  110. package/packages/cli/src/parser.ts +0 -195
  111. package/packages/cli/src/suggestions.ts +0 -207
  112. package/packages/cli/src/terminal/Terminal.ts +0 -365
  113. package/packages/cli/src/terminal/index.ts +0 -5
  114. package/packages/cli/src/types.ts +0 -155
  115. package/packages/cli/tsconfig.json +0 -20
  116. package/packages/core/package.json +0 -35
  117. package/packages/core/src/actions.ts +0 -1210
  118. package/packages/core/src/errors.ts +0 -296
  119. package/packages/core/src/index.test.ts +0 -638
  120. package/packages/core/src/index.ts +0 -220
  121. package/packages/core/src/ref-map.ts +0 -107
  122. package/packages/core/src/snapshot.ts +0 -873
  123. package/packages/core/src/types.ts +0 -536
  124. package/packages/core/tsconfig.json +0 -23
  125. package/packages/extension/README.md +0 -129
  126. package/packages/extension/package.json +0 -43
  127. package/packages/extension/src/background.ts +0 -888
  128. package/packages/extension/src/content.ts +0 -172
  129. package/packages/extension/src/index.ts +0 -579
  130. package/packages/extension/src/session-manager.ts +0 -385
  131. package/packages/extension/src/types.ts +0 -162
  132. package/packages/extension/tsconfig.json +0 -28
  133. package/src/index.ts +0 -64
  134. package/tsconfig.build.json +0 -12
  135. package/tsconfig.json +0 -26
  136. package/vitest.config.ts +0 -13
@@ -0,0 +1,940 @@
1
+ /**
2
+ * @btcp/core - DOM Actions
3
+ *
4
+ * Element interaction handlers using native browser APIs.
5
+ */
6
+ import { createSnapshot } from './snapshot.js';
7
+ import { DetailedError, createElementNotFoundError, createElementNotCompatibleError, createTimeoutError, createInvalidParametersError, } from './errors.js';
8
+ /**
9
+ * DOM Actions executor
10
+ */
11
+ export class DOMActions {
12
+ document;
13
+ window;
14
+ refMap;
15
+ lastSnapshotData = null;
16
+ overlayContainer = null;
17
+ scrollListener = null;
18
+ rafId = null;
19
+ constructor(doc, win, refMap) {
20
+ this.document = doc;
21
+ this.window = win;
22
+ this.refMap = refMap;
23
+ }
24
+ /**
25
+ * Execute a command and return a response
26
+ */
27
+ async execute(command) {
28
+ try {
29
+ const data = await this.dispatch(command);
30
+ return { id: command.id, success: true, data };
31
+ }
32
+ catch (error) {
33
+ const message = error instanceof Error ? error.message : String(error);
34
+ // Include structured error data if available
35
+ if (error instanceof DetailedError) {
36
+ return {
37
+ id: command.id,
38
+ success: false,
39
+ error: message,
40
+ errorCode: error.code,
41
+ errorContext: error.context,
42
+ suggestions: error.suggestions,
43
+ };
44
+ }
45
+ return { id: command.id, success: false, error: message };
46
+ }
47
+ }
48
+ async dispatch(command) {
49
+ switch (command.action) {
50
+ case 'click':
51
+ return this.click(command.selector, {
52
+ button: command.button,
53
+ clickCount: command.clickCount,
54
+ modifiers: command.modifiers,
55
+ });
56
+ case 'dblclick':
57
+ return this.dblclick(command.selector);
58
+ case 'type':
59
+ return this.type(command.selector, command.text, {
60
+ delay: command.delay,
61
+ clear: command.clear,
62
+ });
63
+ case 'fill':
64
+ return this.fill(command.selector, command.value);
65
+ case 'clear':
66
+ return this.clear(command.selector);
67
+ case 'check':
68
+ return this.check(command.selector);
69
+ case 'uncheck':
70
+ return this.uncheck(command.selector);
71
+ case 'select':
72
+ return this.select(command.selector, command.values);
73
+ case 'focus':
74
+ return this.focus(command.selector);
75
+ case 'blur':
76
+ return this.blur(command.selector);
77
+ case 'hover':
78
+ return this.hover(command.selector);
79
+ case 'scroll':
80
+ return this.scroll(command.selector, {
81
+ x: command.x,
82
+ y: command.y,
83
+ direction: command.direction,
84
+ amount: command.amount,
85
+ });
86
+ case 'scrollIntoView':
87
+ return this.scrollIntoView(command.selector, command.block);
88
+ case 'snapshot':
89
+ return this.snapshot({
90
+ selector: command.selector,
91
+ maxDepth: command.maxDepth,
92
+ includeHidden: command.includeHidden,
93
+ interactive: command.interactive,
94
+ compact: command.compact,
95
+ all: command.all,
96
+ format: command.format,
97
+ grep: command.grep,
98
+ });
99
+ case 'querySelector':
100
+ return this.querySelector(command.selector);
101
+ case 'querySelectorAll':
102
+ return this.querySelectorAll(command.selector);
103
+ case 'getText':
104
+ return this.getText(command.selector);
105
+ case 'getAttribute':
106
+ return this.getAttribute(command.selector, command.attribute);
107
+ case 'getProperty':
108
+ return this.getProperty(command.selector, command.property);
109
+ case 'getBoundingBox':
110
+ return this.getBoundingBox(command.selector);
111
+ case 'isVisible':
112
+ return this.isVisible(command.selector);
113
+ case 'isEnabled':
114
+ return this.isEnabled(command.selector);
115
+ case 'isChecked':
116
+ return this.isChecked(command.selector);
117
+ case 'press':
118
+ return this.press(command.key, command.selector, command.modifiers);
119
+ case 'keyDown':
120
+ return this.keyDown(command.key);
121
+ case 'keyUp':
122
+ return this.keyUp(command.key);
123
+ case 'wait':
124
+ return this.wait(command.selector, {
125
+ state: command.state,
126
+ timeout: command.timeout,
127
+ });
128
+ case 'evaluate':
129
+ return this.evaluate(command.script, command.args);
130
+ case 'validateElement':
131
+ return this.validateElement(command.selector, {
132
+ expectedType: command.expectedType,
133
+ capabilities: command.capabilities,
134
+ });
135
+ case 'validateRefs':
136
+ return this.validateRefs(command.refs);
137
+ case 'highlight':
138
+ return this.highlight();
139
+ case 'clearHighlight':
140
+ return this.clearHighlight();
141
+ default:
142
+ throw new Error(`Unknown action: ${command.action}`);
143
+ }
144
+ }
145
+ // --- Element Resolution ---
146
+ getElement(selector) {
147
+ const element = this.queryElement(selector);
148
+ if (!element) {
149
+ const isRef = selector.startsWith('@ref:');
150
+ const similarSelectors = isRef ? [] : this.findSimilarSelectors(selector);
151
+ const nearbyElements = this.getNearbyInteractiveElements();
152
+ throw createElementNotFoundError(selector, {
153
+ similarSelectors,
154
+ nearbyElements: nearbyElements.slice(0, 5),
155
+ isRef,
156
+ });
157
+ }
158
+ return element;
159
+ }
160
+ /**
161
+ * Find selectors similar to the given selector
162
+ */
163
+ findSimilarSelectors(selector) {
164
+ const results = [];
165
+ try {
166
+ // Try to extract ID or class from selector
167
+ const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
168
+ const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
169
+ if (idMatch) {
170
+ // Look for similar IDs
171
+ const targetId = idMatch[1].toLowerCase();
172
+ const allElements = this.document.querySelectorAll('[id]');
173
+ allElements.forEach(el => {
174
+ const elId = el.id.toLowerCase();
175
+ if (elId !== targetId && (elId.includes(targetId) || targetId.includes(elId))) {
176
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
177
+ const name = el.textContent?.trim().substring(0, 30) || el.getAttribute('aria-label') || '';
178
+ results.push({
179
+ selector: `#${el.id}`,
180
+ role,
181
+ name,
182
+ });
183
+ }
184
+ });
185
+ }
186
+ if (classMatch && results.length < 3) {
187
+ // Look for similar classes
188
+ const targetClass = classMatch[1].toLowerCase();
189
+ const allElements = this.document.querySelectorAll('[class]');
190
+ allElements.forEach(el => {
191
+ const classes = Array.from(el.classList).map(c => c.toLowerCase());
192
+ const similarClass = classes.find(c => c !== targetClass && (c.includes(targetClass) || targetClass.includes(c)));
193
+ if (similarClass) {
194
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
195
+ const name = el.textContent?.trim().substring(0, 30) || el.getAttribute('aria-label') || '';
196
+ results.push({
197
+ selector: `.${similarClass}`,
198
+ role,
199
+ name,
200
+ });
201
+ }
202
+ });
203
+ }
204
+ }
205
+ catch (e) {
206
+ // Ignore errors in similarity search
207
+ }
208
+ return results.slice(0, 3);
209
+ }
210
+ /**
211
+ * Get nearby interactive elements
212
+ */
213
+ getNearbyInteractiveElements() {
214
+ const results = [];
215
+ try {
216
+ const interactiveSelectors = [
217
+ 'button',
218
+ 'a[href]',
219
+ 'input',
220
+ 'textarea',
221
+ 'select',
222
+ '[role="button"]',
223
+ '[role="link"]',
224
+ '[tabindex]'
225
+ ];
226
+ const elements = this.document.querySelectorAll(interactiveSelectors.join(','));
227
+ elements.forEach(el => {
228
+ if (el instanceof HTMLElement) {
229
+ const style = this.window.getComputedStyle(el);
230
+ const isVisible = style.display !== 'none' && style.visibility !== 'hidden';
231
+ if (isVisible) {
232
+ const ref = this.refMap.generateRef(el);
233
+ const role = el.getAttribute('role') || el.tagName.toLowerCase();
234
+ const name = el.textContent?.trim().substring(0, 30) ||
235
+ el.getAttribute('aria-label') ||
236
+ el.value?.substring(0, 30) ||
237
+ el.placeholder ||
238
+ '';
239
+ results.push({ ref, role, name });
240
+ }
241
+ }
242
+ });
243
+ }
244
+ catch (e) {
245
+ // Ignore errors in nearby element search
246
+ }
247
+ return results.slice(0, 10);
248
+ }
249
+ /**
250
+ * Get available actions for an element based on its type
251
+ */
252
+ getAvailableActionsForElement(element) {
253
+ const actions = [];
254
+ // All elements can be queried and inspected
255
+ actions.push('querySelector', 'getText', 'getAttribute', 'getProperty', 'getBoundingBox', 'isVisible');
256
+ // Clickable elements
257
+ if (element instanceof HTMLButtonElement ||
258
+ element instanceof HTMLAnchorElement ||
259
+ element.getAttribute('role') === 'button' ||
260
+ element.getAttribute('role') === 'link' ||
261
+ element.hasAttribute('onclick')) {
262
+ actions.push('click', 'dblclick', 'hover');
263
+ }
264
+ // Input elements
265
+ if (element instanceof HTMLInputElement) {
266
+ actions.push('fill', 'clear', 'focus', 'blur', 'isEnabled');
267
+ if (element.type === 'checkbox' || element.type === 'radio') {
268
+ actions.push('check', 'uncheck', 'isChecked');
269
+ }
270
+ else {
271
+ actions.push('type');
272
+ }
273
+ }
274
+ // Textarea elements
275
+ if (element instanceof HTMLTextAreaElement) {
276
+ actions.push('type', 'fill', 'clear', 'focus', 'blur');
277
+ }
278
+ // Select elements
279
+ if (element instanceof HTMLSelectElement) {
280
+ actions.push('select', 'focus', 'blur');
281
+ }
282
+ // Focusable elements
283
+ if (element instanceof HTMLElement) {
284
+ actions.push('focus', 'blur', 'scroll', 'scrollIntoView', 'press');
285
+ }
286
+ return actions;
287
+ }
288
+ queryElement(selector) {
289
+ // Check if it's a ref
290
+ if (selector.startsWith('@ref:')) {
291
+ return this.refMap.get(selector);
292
+ }
293
+ // CSS selector
294
+ return this.document.querySelector(selector);
295
+ }
296
+ queryElements(selector) {
297
+ if (selector.startsWith('@ref:')) {
298
+ const el = this.refMap.get(selector);
299
+ return el ? [el] : [];
300
+ }
301
+ return Array.from(this.document.querySelectorAll(selector));
302
+ }
303
+ // --- Actions ---
304
+ async click(selector, options = {}) {
305
+ const element = this.getElement(selector);
306
+ const { button = 'left', clickCount = 1, modifiers = [] } = options;
307
+ if (element instanceof HTMLElement) {
308
+ element.focus();
309
+ }
310
+ const buttonCode = button === 'right' ? 2 : button === 'middle' ? 1 : 0;
311
+ const eventInit = {
312
+ bubbles: true,
313
+ cancelable: true,
314
+ button: buttonCode,
315
+ altKey: modifiers.includes('Alt'),
316
+ ctrlKey: modifiers.includes('Control'),
317
+ metaKey: modifiers.includes('Meta'),
318
+ shiftKey: modifiers.includes('Shift'),
319
+ };
320
+ for (let i = 0; i < clickCount; i++) {
321
+ element.dispatchEvent(new MouseEvent('mousedown', eventInit));
322
+ element.dispatchEvent(new MouseEvent('mouseup', eventInit));
323
+ element.dispatchEvent(new MouseEvent('click', eventInit));
324
+ }
325
+ return { clicked: true };
326
+ }
327
+ async dblclick(selector) {
328
+ const element = this.getElement(selector);
329
+ element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
330
+ return { clicked: true };
331
+ }
332
+ async type(selector, text, options = {}) {
333
+ const element = this.getElement(selector);
334
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
335
+ const actualType = element.tagName.toLowerCase();
336
+ const availableActions = this.getAvailableActionsForElement(element);
337
+ throw createElementNotCompatibleError(selector, 'type', actualType, ['input', 'textarea'], availableActions);
338
+ }
339
+ element.focus();
340
+ if (options.clear) {
341
+ element.value = '';
342
+ element.dispatchEvent(new Event('input', { bubbles: true }));
343
+ }
344
+ for (const char of text) {
345
+ element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
346
+ element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
347
+ element.value += char;
348
+ element.dispatchEvent(new Event('input', { bubbles: true }));
349
+ element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
350
+ if (options.delay) {
351
+ await this.sleep(options.delay);
352
+ }
353
+ }
354
+ element.dispatchEvent(new Event('change', { bubbles: true }));
355
+ return { typed: true };
356
+ }
357
+ async fill(selector, value) {
358
+ const element = this.getElement(selector);
359
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
360
+ const actualType = element.tagName.toLowerCase();
361
+ const availableActions = this.getAvailableActionsForElement(element);
362
+ throw createElementNotCompatibleError(selector, 'fill', actualType, ['input', 'textarea'], availableActions);
363
+ }
364
+ element.focus();
365
+ element.value = value;
366
+ element.dispatchEvent(new Event('input', { bubbles: true }));
367
+ element.dispatchEvent(new Event('change', { bubbles: true }));
368
+ return { filled: true };
369
+ }
370
+ async clear(selector) {
371
+ const element = this.getElement(selector);
372
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
373
+ element.value = '';
374
+ element.dispatchEvent(new Event('input', { bubbles: true }));
375
+ element.dispatchEvent(new Event('change', { bubbles: true }));
376
+ }
377
+ return { cleared: true };
378
+ }
379
+ async check(selector) {
380
+ const element = this.getElement(selector);
381
+ if (!(element instanceof HTMLInputElement)) {
382
+ const actualType = element.tagName.toLowerCase();
383
+ const availableActions = this.getAvailableActionsForElement(element);
384
+ throw createElementNotCompatibleError(selector, 'check', actualType, ['input[type=checkbox]', 'input[type=radio]'], availableActions);
385
+ }
386
+ if (!element.checked) {
387
+ element.click();
388
+ }
389
+ return { checked: true };
390
+ }
391
+ async uncheck(selector) {
392
+ const element = this.getElement(selector);
393
+ if (!(element instanceof HTMLInputElement)) {
394
+ const actualType = element.tagName.toLowerCase();
395
+ const availableActions = this.getAvailableActionsForElement(element);
396
+ throw createElementNotCompatibleError(selector, 'uncheck', actualType, ['input[type=checkbox]'], availableActions);
397
+ }
398
+ if (element.checked) {
399
+ element.click();
400
+ }
401
+ return { unchecked: true };
402
+ }
403
+ async select(selector, values) {
404
+ const element = this.getElement(selector);
405
+ if (!(element instanceof HTMLSelectElement)) {
406
+ const actualType = element.tagName.toLowerCase();
407
+ const availableActions = this.getAvailableActionsForElement(element);
408
+ throw createElementNotCompatibleError(selector, 'select', actualType, ['select'], availableActions);
409
+ }
410
+ const valueArray = Array.isArray(values) ? values : [values];
411
+ for (const option of element.options) {
412
+ option.selected = valueArray.includes(option.value);
413
+ }
414
+ element.dispatchEvent(new Event('change', { bubbles: true }));
415
+ return { selected: valueArray };
416
+ }
417
+ async focus(selector) {
418
+ const element = this.getElement(selector);
419
+ if (element instanceof HTMLElement) {
420
+ element.focus();
421
+ }
422
+ return { focused: true };
423
+ }
424
+ async blur(selector) {
425
+ const element = this.getElement(selector);
426
+ if (element instanceof HTMLElement) {
427
+ element.blur();
428
+ }
429
+ return { blurred: true };
430
+ }
431
+ async hover(selector) {
432
+ const element = this.getElement(selector);
433
+ element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
434
+ element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
435
+ return { hovered: true };
436
+ }
437
+ async scroll(selector, options) {
438
+ // Validate parameter combinations
439
+ const hasXY = options.x !== undefined || options.y !== undefined;
440
+ const hasDirection = options.direction !== undefined;
441
+ if (hasXY && hasDirection) {
442
+ throw createInvalidParametersError('Scroll command has conflicting parameters', ['x/y', 'direction'], 'Use either { x, y } for absolute scrolling OR { direction, amount } for relative scrolling, not both');
443
+ }
444
+ let deltaX = options.x ?? 0;
445
+ let deltaY = options.y ?? 0;
446
+ if (options.direction) {
447
+ const amount = options.amount ?? 100;
448
+ switch (options.direction) {
449
+ case 'up':
450
+ deltaY = -amount;
451
+ break;
452
+ case 'down':
453
+ deltaY = amount;
454
+ break;
455
+ case 'left':
456
+ deltaX = -amount;
457
+ break;
458
+ case 'right':
459
+ deltaX = amount;
460
+ break;
461
+ }
462
+ }
463
+ if (selector) {
464
+ const element = this.getElement(selector);
465
+ element.scrollBy(deltaX, deltaY);
466
+ }
467
+ else {
468
+ this.window.scrollBy(deltaX, deltaY);
469
+ }
470
+ return { scrolled: true };
471
+ }
472
+ async scrollIntoView(selector, block = 'center') {
473
+ const element = this.getElement(selector);
474
+ element.scrollIntoView({ behavior: 'smooth', block });
475
+ return { scrolled: true };
476
+ }
477
+ async snapshot(options) {
478
+ const root = options.selector
479
+ ? this.getElement(options.selector)
480
+ : this.document.body;
481
+ const snapshotData = createSnapshot(this.document, this.refMap, {
482
+ root,
483
+ maxDepth: options.maxDepth,
484
+ includeHidden: options.includeHidden,
485
+ interactive: options.interactive,
486
+ compact: options.compact,
487
+ all: options.all,
488
+ format: options.format,
489
+ grep: options.grep,
490
+ });
491
+ // Store snapshot data for highlight command (preserve refs internally)
492
+ this.lastSnapshotData = snapshotData;
493
+ // Return only the tree string
494
+ return snapshotData.tree;
495
+ }
496
+ async querySelector(selector) {
497
+ const element = this.queryElement(selector);
498
+ if (!element) {
499
+ return { found: false };
500
+ }
501
+ const ref = this.refMap.generateRef(element);
502
+ return { found: true, ref };
503
+ }
504
+ async querySelectorAll(selector) {
505
+ const elements = this.queryElements(selector);
506
+ const refs = elements.map((el) => this.refMap.generateRef(el));
507
+ return { count: elements.length, refs };
508
+ }
509
+ async getText(selector) {
510
+ const element = this.getElement(selector);
511
+ return { text: element.textContent };
512
+ }
513
+ async getAttribute(selector, attribute) {
514
+ const element = this.getElement(selector);
515
+ return { value: element.getAttribute(attribute) };
516
+ }
517
+ async getProperty(selector, property) {
518
+ const element = this.getElement(selector);
519
+ return { value: element[property] };
520
+ }
521
+ async getBoundingBox(selector) {
522
+ const element = this.getElement(selector);
523
+ const rect = element.getBoundingClientRect();
524
+ return {
525
+ box: {
526
+ x: rect.x,
527
+ y: rect.y,
528
+ width: rect.width,
529
+ height: rect.height,
530
+ },
531
+ };
532
+ }
533
+ async isVisible(selector) {
534
+ const element = this.queryElement(selector);
535
+ if (!element || !(element instanceof HTMLElement)) {
536
+ return { visible: false };
537
+ }
538
+ const style = this.window.getComputedStyle(element);
539
+ const visible = style.display !== 'none' &&
540
+ style.visibility !== 'hidden' &&
541
+ style.opacity !== '0';
542
+ return { visible };
543
+ }
544
+ async isEnabled(selector) {
545
+ const element = this.getElement(selector);
546
+ const enabled = !element.disabled;
547
+ return { enabled };
548
+ }
549
+ async isChecked(selector) {
550
+ const element = this.getElement(selector);
551
+ const checked = element.checked ?? false;
552
+ return { checked };
553
+ }
554
+ async press(key, selector, modifiers = []) {
555
+ const target = selector
556
+ ? this.getElement(selector)
557
+ : this.document.activeElement || this.document.body;
558
+ const eventInit = {
559
+ key,
560
+ code: key,
561
+ bubbles: true,
562
+ cancelable: true,
563
+ altKey: modifiers.includes('Alt'),
564
+ ctrlKey: modifiers.includes('Control'),
565
+ metaKey: modifiers.includes('Meta'),
566
+ shiftKey: modifiers.includes('Shift'),
567
+ };
568
+ target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
569
+ target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
570
+ target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
571
+ return { pressed: true };
572
+ }
573
+ async keyDown(key) {
574
+ const target = this.document.activeElement || this.document.body;
575
+ target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
576
+ return { down: true };
577
+ }
578
+ async keyUp(key) {
579
+ const target = this.document.activeElement || this.document.body;
580
+ target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
581
+ return { up: true };
582
+ }
583
+ async wait(selector, options = {}) {
584
+ const { state = 'visible', timeout = 5000 } = options;
585
+ if (!selector) {
586
+ await this.sleep(timeout);
587
+ return { waited: true };
588
+ }
589
+ const startTime = Date.now();
590
+ let lastState;
591
+ while (Date.now() - startTime < timeout) {
592
+ const element = this.queryElement(selector);
593
+ // Track element state for error reporting
594
+ if (element instanceof HTMLElement) {
595
+ const style = this.window.getComputedStyle(element);
596
+ lastState = {
597
+ attached: true,
598
+ visible: style.display !== 'none' && style.visibility !== 'hidden',
599
+ enabled: !element.disabled,
600
+ };
601
+ }
602
+ else if (element) {
603
+ lastState = {
604
+ attached: true,
605
+ visible: false,
606
+ enabled: true,
607
+ };
608
+ }
609
+ let conditionMet = false;
610
+ switch (state) {
611
+ case 'attached':
612
+ conditionMet = element !== null;
613
+ break;
614
+ case 'detached':
615
+ conditionMet = element === null;
616
+ break;
617
+ case 'visible':
618
+ if (element instanceof HTMLElement) {
619
+ const style = this.window.getComputedStyle(element);
620
+ conditionMet =
621
+ style.display !== 'none' &&
622
+ style.visibility !== 'hidden';
623
+ }
624
+ break;
625
+ case 'hidden':
626
+ conditionMet =
627
+ !element ||
628
+ (element instanceof HTMLElement &&
629
+ this.window.getComputedStyle(element).display === 'none');
630
+ break;
631
+ case 'enabled':
632
+ conditionMet = element !== null && !element.disabled;
633
+ break;
634
+ }
635
+ if (conditionMet) {
636
+ return { waited: true };
637
+ }
638
+ await this.sleep(100);
639
+ }
640
+ // Provide detailed timeout error with current state
641
+ throw createTimeoutError(selector, state, lastState);
642
+ }
643
+ async evaluate(script, args) {
644
+ const fn = new Function(...(args?.map((_, i) => `arg${i}`) || []), `return (${script})`);
645
+ const result = fn.call(this.window, ...(args || []));
646
+ return { result };
647
+ }
648
+ /**
649
+ * Validate element capabilities before attempting an action
650
+ */
651
+ async validateElement(selector, options = {}) {
652
+ const element = this.getElement(selector);
653
+ const actualRole = element.getAttribute('role') || element.tagName.toLowerCase();
654
+ const actualType = element instanceof HTMLInputElement ? element.type : undefined;
655
+ // Get element capabilities
656
+ const capabilities = this.getAvailableActionsForElement(element);
657
+ // Get element state
658
+ const style = element instanceof HTMLElement ? this.window.getComputedStyle(element) : null;
659
+ const state = {
660
+ attached: true,
661
+ visible: style ? style.display !== 'none' && style.visibility !== 'hidden' : false,
662
+ enabled: !element.disabled,
663
+ };
664
+ // Check type compatibility
665
+ let compatible = true;
666
+ let suggestion;
667
+ if (options.expectedType) {
668
+ const typeMatch = options.expectedType === actualRole ||
669
+ (options.expectedType === 'input' && element instanceof HTMLInputElement) ||
670
+ (options.expectedType === 'textarea' && element instanceof HTMLTextAreaElement) ||
671
+ (options.expectedType === 'button' && element instanceof HTMLButtonElement) ||
672
+ (options.expectedType === 'link' && element instanceof HTMLAnchorElement) ||
673
+ (options.expectedType === 'select' && element instanceof HTMLSelectElement);
674
+ if (!typeMatch) {
675
+ compatible = false;
676
+ suggestion = `Element is ${actualRole}, not ${options.expectedType}. Available actions: ${capabilities.slice(0, 5).join(', ')}`;
677
+ }
678
+ }
679
+ // Check capability requirements
680
+ if (options.capabilities && compatible) {
681
+ const capabilityMap = {
682
+ clickable: element instanceof HTMLButtonElement ||
683
+ element instanceof HTMLAnchorElement ||
684
+ element.getAttribute('role') === 'button' ||
685
+ element.hasAttribute('onclick'),
686
+ editable: element instanceof HTMLInputElement ||
687
+ element instanceof HTMLTextAreaElement,
688
+ checkable: element instanceof HTMLInputElement &&
689
+ (element.type === 'checkbox' || element.type === 'radio'),
690
+ hoverable: element instanceof HTMLElement,
691
+ };
692
+ for (const cap of options.capabilities) {
693
+ if (!capabilityMap[cap]) {
694
+ compatible = false;
695
+ suggestion = `Element does not support capability: ${cap}. Available actions: ${capabilities.slice(0, 5).join(', ')}`;
696
+ break;
697
+ }
698
+ }
699
+ }
700
+ return {
701
+ compatible,
702
+ actualRole,
703
+ actualType,
704
+ capabilities,
705
+ state,
706
+ suggestion,
707
+ };
708
+ }
709
+ /**
710
+ * Validate that refs are still valid
711
+ */
712
+ async validateRefs(refs) {
713
+ const valid = [];
714
+ const invalid = [];
715
+ const reasons = {};
716
+ for (const ref of refs) {
717
+ const element = this.refMap.get(ref);
718
+ if (element) {
719
+ // Check if element is still in the DOM
720
+ if (this.document.contains(element)) {
721
+ valid.push(ref);
722
+ }
723
+ else {
724
+ invalid.push(ref);
725
+ reasons[ref] = 'Element has been removed from the DOM';
726
+ }
727
+ }
728
+ else {
729
+ invalid.push(ref);
730
+ reasons[ref] = 'Ref not found. Refs are cleared on each snapshot() call.';
731
+ }
732
+ }
733
+ return {
734
+ valid,
735
+ invalid,
736
+ reasons,
737
+ };
738
+ }
739
+ /**
740
+ * Display visual overlay labels for interactive elements
741
+ */
742
+ async highlight() {
743
+ // Verify snapshot exists
744
+ if (!this.lastSnapshotData || !this.lastSnapshotData.refs) {
745
+ throw new Error('No snapshot data available. Please run snapshot() command first.');
746
+ }
747
+ // Clear any existing highlights
748
+ this.clearExistingOverlay();
749
+ // Create overlay container with absolute positioning covering entire document
750
+ this.overlayContainer = this.document.createElement('div');
751
+ this.overlayContainer.id = 'btcp-highlight-overlay';
752
+ this.overlayContainer.style.cssText = `
753
+ position: absolute;
754
+ top: 0;
755
+ left: 0;
756
+ width: ${this.document.documentElement.scrollWidth}px;
757
+ height: ${this.document.documentElement.scrollHeight}px;
758
+ pointer-events: none;
759
+ z-index: 999999;
760
+ contain: layout style paint;
761
+ `;
762
+ let highlightedCount = 0;
763
+ // Create border overlays and labels for each ref
764
+ for (const [ref, _refData] of Object.entries(this.lastSnapshotData.refs)) {
765
+ const element = this.refMap.get(ref);
766
+ // Skip if element no longer exists or is disconnected
767
+ if (!element || !element.isConnected) {
768
+ continue;
769
+ }
770
+ try {
771
+ // Get current bounding box (element might have moved)
772
+ const bbox = element.getBoundingClientRect();
773
+ // Skip elements with no dimensions
774
+ if (bbox.width === 0 && bbox.height === 0) {
775
+ continue;
776
+ }
777
+ // Create border overlay
778
+ const border = this.document.createElement('div');
779
+ border.className = 'btcp-ref-border';
780
+ border.dataset.ref = ref;
781
+ border.style.cssText = `
782
+ position: absolute;
783
+ width: ${bbox.width}px;
784
+ height: ${bbox.height}px;
785
+ transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
786
+ border: 2px solid rgba(59, 130, 246, 0.8);
787
+ border-radius: 2px;
788
+ box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
789
+ pointer-events: none;
790
+ will-change: transform;
791
+ contain: layout style paint;
792
+ `;
793
+ // Create label
794
+ const label = this.document.createElement('div');
795
+ label.className = 'btcp-ref-label';
796
+ label.dataset.ref = ref;
797
+ // Extract number from ref (e.g., "@ref:5" -> "5")
798
+ label.textContent = ref.replace('@ref:', '');
799
+ label.style.cssText = `
800
+ position: absolute;
801
+ transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
802
+ background: rgba(59, 130, 246, 0.9);
803
+ color: white;
804
+ padding: 2px 6px;
805
+ border-radius: 3px;
806
+ font-family: monospace;
807
+ font-size: 11px;
808
+ font-weight: bold;
809
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
810
+ pointer-events: none;
811
+ white-space: nowrap;
812
+ will-change: transform;
813
+ contain: layout style paint;
814
+ `;
815
+ this.overlayContainer.appendChild(border);
816
+ this.overlayContainer.appendChild(label);
817
+ highlightedCount++;
818
+ }
819
+ catch (error) {
820
+ // Skip elements that throw errors
821
+ continue;
822
+ }
823
+ }
824
+ // Inject overlay into page
825
+ this.document.body.appendChild(this.overlayContainer);
826
+ // Set up scroll listener with rAF throttling
827
+ let ticking = false;
828
+ this.scrollListener = () => {
829
+ if (!ticking) {
830
+ this.rafId = this.window.requestAnimationFrame(() => {
831
+ this.updateHighlightPositions();
832
+ ticking = false;
833
+ });
834
+ ticking = true;
835
+ }
836
+ };
837
+ // Use passive listener for better scroll performance
838
+ this.window.addEventListener('scroll', this.scrollListener, { passive: true });
839
+ return { highlighted: highlightedCount };
840
+ }
841
+ /**
842
+ * Invalidate snapshot data (called on navigation or manual clear)
843
+ */
844
+ invalidateSnapshot() {
845
+ this.lastSnapshotData = null;
846
+ this.clearHighlight();
847
+ }
848
+ /**
849
+ * Update highlight positions on scroll (GPU-accelerated)
850
+ */
851
+ updateHighlightPositions() {
852
+ if (!this.overlayContainer || !this.lastSnapshotData) {
853
+ return;
854
+ }
855
+ // Batch DOM reads
856
+ const updates = [];
857
+ // Read phase - get all bounding boxes first
858
+ const borders = this.overlayContainer.querySelectorAll('.btcp-ref-border');
859
+ const labels = this.overlayContainer.querySelectorAll('.btcp-ref-label');
860
+ borders.forEach((borderEl) => {
861
+ const ref = borderEl.dataset.ref;
862
+ if (!ref)
863
+ return;
864
+ const element = this.refMap.get(ref);
865
+ if (!element || !element.isConnected)
866
+ return;
867
+ const bbox = element.getBoundingClientRect();
868
+ if (bbox.width === 0 && bbox.height === 0)
869
+ return;
870
+ updates.push({
871
+ element: borderEl,
872
+ x: bbox.left + this.window.scrollX,
873
+ y: bbox.top + this.window.scrollY,
874
+ width: bbox.width,
875
+ height: bbox.height,
876
+ });
877
+ });
878
+ labels.forEach((labelEl) => {
879
+ const ref = labelEl.dataset.ref;
880
+ if (!ref)
881
+ return;
882
+ const element = this.refMap.get(ref);
883
+ if (!element || !element.isConnected)
884
+ return;
885
+ const bbox = element.getBoundingClientRect();
886
+ if (bbox.width === 0 && bbox.height === 0)
887
+ return;
888
+ updates.push({
889
+ element: labelEl,
890
+ x: bbox.left + this.window.scrollX,
891
+ y: bbox.top + this.window.scrollY,
892
+ });
893
+ });
894
+ // Write phase - update transforms
895
+ updates.forEach(({ element, x, y, width, height }) => {
896
+ element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
897
+ if (width !== undefined && height !== undefined) {
898
+ element.style.width = `${width}px`;
899
+ element.style.height = `${height}px`;
900
+ }
901
+ });
902
+ }
903
+ /**
904
+ * Remove visual overlay labels
905
+ */
906
+ async clearHighlight() {
907
+ this.clearExistingOverlay();
908
+ return { cleared: true };
909
+ }
910
+ /**
911
+ * Remove existing overlay if it exists
912
+ */
913
+ clearExistingOverlay() {
914
+ // Remove scroll listener
915
+ if (this.scrollListener) {
916
+ this.window.removeEventListener('scroll', this.scrollListener);
917
+ this.scrollListener = null;
918
+ }
919
+ // Cancel any pending animation frame
920
+ if (this.rafId !== null) {
921
+ this.window.cancelAnimationFrame(this.rafId);
922
+ this.rafId = null;
923
+ }
924
+ // Remove overlay container
925
+ if (this.overlayContainer && this.overlayContainer.parentNode) {
926
+ this.overlayContainer.parentNode.removeChild(this.overlayContainer);
927
+ }
928
+ this.overlayContainer = null;
929
+ // Also remove any orphaned overlays
930
+ const existingOverlay = this.document.getElementById('btcp-highlight-overlay');
931
+ if (existingOverlay && existingOverlay.parentNode) {
932
+ existingOverlay.parentNode.removeChild(existingOverlay);
933
+ }
934
+ }
935
+ // --- Utilities ---
936
+ sleep(ms) {
937
+ return new Promise((resolve) => setTimeout(resolve, ms));
938
+ }
939
+ }
940
+ //# sourceMappingURL=actions.js.map