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.
|
|
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
|
-
|
|
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
|
|
358
|
-
if (
|
|
359
|
-
element.
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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();
|