btcp-browser-agent 0.1.11 → 0.1.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "btcp-browser-agent",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Give AI agents the power to control browsers. A foundation for building agentic systems with smart DOM snapshots and stable element references.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -349,31 +349,99 @@ export class DOMActions {
349
349
  }
350
350
  async type(selector, text, options = {}) {
351
351
  const element = this.getElement(selector);
352
- if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
352
+ // Check if element is contenteditable
353
+ const isContentEditable = element.getAttribute('contenteditable') === 'true' ||
354
+ element.getAttribute('contenteditable') === '';
355
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || isContentEditable)) {
353
356
  const actualType = element.tagName.toLowerCase();
354
357
  const availableActions = this.getAvailableActionsForElement(element);
355
- throw createElementNotCompatibleError(selector, 'type', actualType, ['input', 'textarea'], availableActions);
358
+ throw createElementNotCompatibleError(selector, 'type', actualType, ['input', 'textarea', 'contenteditable'], availableActions);
356
359
  }
357
- element.focus();
358
- if (options.clear) {
359
- element.value = '';
360
- element.dispatchEvent(new Event('input', { bubbles: true }));
360
+ // Focus the element (cast to HTMLElement for contenteditable)
361
+ if (element instanceof HTMLElement) {
362
+ element.focus();
361
363
  }
362
- for (const char of text) {
363
- element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
364
- element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
365
- element.value += char;
366
- element.dispatchEvent(new Event('input', { bubbles: true }));
367
- element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
368
- if (options.delay) {
369
- await this.sleep(options.delay);
364
+ // Handle contenteditable elements differently
365
+ if (isContentEditable) {
366
+ const htmlElement = element;
367
+ if (options.clear) {
368
+ htmlElement.textContent = '';
369
+ htmlElement.dispatchEvent(new Event('input', { bubbles: true }));
370
+ }
371
+ for (const char of text) {
372
+ htmlElement.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
373
+ htmlElement.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
374
+ // Insert text at cursor position or append
375
+ const selection = this.window.getSelection();
376
+ if (selection && selection.rangeCount > 0) {
377
+ const range = selection.getRangeAt(0);
378
+ range.deleteContents();
379
+ const textNode = this.document.createTextNode(char);
380
+ range.insertNode(textNode);
381
+ range.setStartAfter(textNode);
382
+ range.setEndAfter(textNode);
383
+ selection.removeAllRanges();
384
+ selection.addRange(range);
385
+ }
386
+ else {
387
+ htmlElement.textContent += char;
388
+ }
389
+ htmlElement.dispatchEvent(new Event('input', { bubbles: true }));
390
+ htmlElement.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
391
+ if (options.delay) {
392
+ await this.sleep(options.delay);
393
+ }
394
+ }
395
+ htmlElement.dispatchEvent(new Event('change', { bubbles: true }));
396
+ // Wait for verification that textContent contains typed text
397
+ const result = await waitForAssertion(() => {
398
+ const content = htmlElement.textContent || '';
399
+ const expected = text;
400
+ const actual = content;
401
+ if (!content.includes(text)) {
402
+ return {
403
+ success: false,
404
+ error: `Expected textContent to contain "${text}"`,
405
+ description: 'textContent check',
406
+ expected,
407
+ actual
408
+ };
409
+ }
410
+ return {
411
+ success: true,
412
+ error: null,
413
+ description: 'textContent check',
414
+ expected,
415
+ actual
416
+ };
417
+ }, { timeout: 1000, interval: 50 });
418
+ if (!result.success) {
419
+ throw createVerificationError('type', result, selector);
370
420
  }
371
421
  }
372
- element.dispatchEvent(new Event('change', { bubbles: true }));
373
- // Wait for verification that value contains typed text
374
- const result = await waitForAssertion(() => assertValueContains(element, text), { timeout: 1000, interval: 50 });
375
- if (!result.success) {
376
- throw createVerificationError('type', result, selector);
422
+ else {
423
+ // Handle regular input/textarea elements
424
+ const inputElement = element;
425
+ if (options.clear) {
426
+ inputElement.value = '';
427
+ inputElement.dispatchEvent(new Event('input', { bubbles: true }));
428
+ }
429
+ for (const char of text) {
430
+ inputElement.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
431
+ inputElement.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
432
+ inputElement.value += char;
433
+ inputElement.dispatchEvent(new Event('input', { bubbles: true }));
434
+ inputElement.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
435
+ if (options.delay) {
436
+ await this.sleep(options.delay);
437
+ }
438
+ }
439
+ inputElement.dispatchEvent(new Event('change', { bubbles: true }));
440
+ // Wait for verification that value contains typed text
441
+ const result = await waitForAssertion(() => assertValueContains(inputElement, text), { timeout: 1000, interval: 50 });
442
+ if (!result.success) {
443
+ throw createVerificationError('type', result, selector);
444
+ }
377
445
  }
378
446
  return { success: true, error: null };
379
447
  }
@@ -110,6 +110,11 @@ function getRole(element) {
110
110
  const type = element.type || 'text';
111
111
  return INPUT_ROLES[type] || 'textbox';
112
112
  }
113
+ // Detect contenteditable elements (ProseMirror, Quill, TinyMCE, etc.)
114
+ const contentEditable = element.getAttribute('contenteditable');
115
+ if (contentEditable === 'true' || contentEditable === '') {
116
+ return 'textbox';
117
+ }
113
118
  return IMPLICIT_ROLES[tagName] || null;
114
119
  }
115
120
  /**
@@ -320,6 +325,21 @@ function getAccessibleName(element) {
320
325
  if (isImage) {
321
326
  return getImageLabel(element);
322
327
  }
328
+ // Handle contenteditable elements (ProseMirror, Quill, TinyMCE, etc.)
329
+ const contentEditable = element.getAttribute('contenteditable');
330
+ if (contentEditable === 'true' || contentEditable === '') {
331
+ // Try data-placeholder attribute (common in rich text editors)
332
+ const placeholder = element.getAttribute('data-placeholder');
333
+ if (placeholder)
334
+ return placeholder.trim();
335
+ // Try finding placeholder in child paragraph element
336
+ const placeholderEl = element.querySelector('[data-placeholder]');
337
+ if (placeholderEl) {
338
+ const placeholderText = placeholderEl.getAttribute('data-placeholder');
339
+ if (placeholderText)
340
+ return placeholderText.trim();
341
+ }
342
+ }
323
343
  const ariaLabel = element.getAttribute('aria-label');
324
344
  if (ariaLabel)
325
345
  return ariaLabel.trim();