btcp-browser-agent 0.1.7 → 0.1.9
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/LICENSE +21 -21
- package/README.md +338 -306
- package/package.json +69 -69
- package/packages/core/dist/actions.js +98 -67
- package/packages/core/dist/assertions.d.ts +118 -0
- package/packages/core/dist/assertions.js +230 -0
- package/packages/core/dist/errors.d.ts +23 -1
- package/packages/core/dist/errors.js +49 -0
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.js +1 -0
- package/packages/extension/dist/background.d.ts +8 -1
- package/packages/extension/dist/background.js +73 -14
- package/packages/extension/dist/content.js +114 -0
- package/packages/extension/dist/index.d.ts +43 -0
- package/packages/extension/dist/index.js +26 -0
- package/packages/extension/dist/remote.d.ts +133 -0
- package/packages/extension/dist/remote.js +668 -0
- package/packages/extension/dist/script-messenger.d.ts +132 -0
- package/packages/extension/dist/script-messenger.js +86 -0
- package/packages/extension/dist/types.d.ts +76 -2
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "btcp-browser-agent",
|
|
3
|
-
"version": "0.1.
|
|
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
|
-
"type": "module",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js",
|
|
12
|
-
"default": "./dist/index.js"
|
|
13
|
-
},
|
|
14
|
-
"./core": {
|
|
15
|
-
"types": "./packages/core/dist/index.d.ts",
|
|
16
|
-
"import": "./packages/core/dist/index.js",
|
|
17
|
-
"default": "./packages/core/dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./extension": {
|
|
20
|
-
"types": "./packages/extension/dist/index.d.ts",
|
|
21
|
-
"import": "./packages/extension/dist/index.js",
|
|
22
|
-
"default": "./packages/extension/dist/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./extension/content": {
|
|
25
|
-
"types": "./packages/extension/dist/content.d.ts",
|
|
26
|
-
"import": "./packages/extension/dist/content.js",
|
|
27
|
-
"default": "./packages/extension/dist/content.js"
|
|
28
|
-
},
|
|
29
|
-
"./extension/background": {
|
|
30
|
-
"types": "./packages/extension/dist/background.d.ts",
|
|
31
|
-
"import": "./packages/extension/dist/background.js",
|
|
32
|
-
"default": "./packages/extension/dist/background.js"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"scripts": {
|
|
36
|
-
"build": "npm run build:packages && tsc -p tsconfig.build.json",
|
|
37
|
-
"build:packages": "tsc -p packages/core/tsconfig.json && tsc -p packages/extension/tsconfig.json && tsc -p packages/cli/tsconfig.json",
|
|
38
|
-
"clean": "rm -rf dist packages/*/dist",
|
|
39
|
-
"prepare": "npm run build",
|
|
40
|
-
"test": "vitest run",
|
|
41
|
-
"test:watch": "vitest",
|
|
42
|
-
"typecheck": "tsc --noEmit"
|
|
43
|
-
},
|
|
44
|
-
"workspaces": [
|
|
45
|
-
"packages/core",
|
|
46
|
-
"packages/extension",
|
|
47
|
-
"packages/cli"
|
|
48
|
-
],
|
|
49
|
-
"files": [
|
|
50
|
-
"dist",
|
|
51
|
-
"packages/core/dist",
|
|
52
|
-
"packages/extension/dist",
|
|
53
|
-
"!**/__tests__",
|
|
54
|
-
"!**/*.map"
|
|
55
|
-
],
|
|
56
|
-
"license": "Apache-2.0",
|
|
57
|
-
"repository": {
|
|
58
|
-
"type": "git",
|
|
59
|
-
"url": "git+https://github.com/browser-tool-calling-protocol/btcp-browser-agent.git"
|
|
60
|
-
},
|
|
61
|
-
"dependencies": {},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@types/chrome": "^0.0.268",
|
|
64
|
-
"@types/node": "^20.10.0",
|
|
65
|
-
"jsdom": "^24.0.0",
|
|
66
|
-
"typescript": "^5.3.0",
|
|
67
|
-
"vitest": "^2.0.0"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "btcp-browser-agent",
|
|
3
|
+
"version": "0.1.9",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./core": {
|
|
15
|
+
"types": "./packages/core/dist/index.d.ts",
|
|
16
|
+
"import": "./packages/core/dist/index.js",
|
|
17
|
+
"default": "./packages/core/dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./extension": {
|
|
20
|
+
"types": "./packages/extension/dist/index.d.ts",
|
|
21
|
+
"import": "./packages/extension/dist/index.js",
|
|
22
|
+
"default": "./packages/extension/dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./extension/content": {
|
|
25
|
+
"types": "./packages/extension/dist/content.d.ts",
|
|
26
|
+
"import": "./packages/extension/dist/content.js",
|
|
27
|
+
"default": "./packages/extension/dist/content.js"
|
|
28
|
+
},
|
|
29
|
+
"./extension/background": {
|
|
30
|
+
"types": "./packages/extension/dist/background.d.ts",
|
|
31
|
+
"import": "./packages/extension/dist/background.js",
|
|
32
|
+
"default": "./packages/extension/dist/background.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "npm run build:packages && tsc -p tsconfig.build.json",
|
|
37
|
+
"build:packages": "tsc -p packages/core/tsconfig.json && tsc -p packages/extension/tsconfig.json && tsc -p packages/cli/tsconfig.json",
|
|
38
|
+
"clean": "rm -rf dist packages/*/dist",
|
|
39
|
+
"prepare": "npm run build",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"workspaces": [
|
|
45
|
+
"packages/core",
|
|
46
|
+
"packages/extension",
|
|
47
|
+
"packages/cli"
|
|
48
|
+
],
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"packages/core/dist",
|
|
52
|
+
"packages/extension/dist",
|
|
53
|
+
"!**/__tests__",
|
|
54
|
+
"!**/*.map"
|
|
55
|
+
],
|
|
56
|
+
"license": "Apache-2.0",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/browser-tool-calling-protocol/btcp-browser-agent.git"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/chrome": "^0.0.268",
|
|
64
|
+
"@types/node": "^20.10.0",
|
|
65
|
+
"jsdom": "^24.0.0",
|
|
66
|
+
"typescript": "^5.3.0",
|
|
67
|
+
"vitest": "^2.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Element interaction handlers using native browser APIs.
|
|
5
5
|
*/
|
|
6
6
|
import { createSnapshot } from './snapshot.js';
|
|
7
|
-
import { DetailedError, createElementNotFoundError, createElementNotCompatibleError, createTimeoutError, createInvalidParametersError, } from './errors.js';
|
|
7
|
+
import { DetailedError, createElementNotFoundError, createElementNotCompatibleError, createTimeoutError, createInvalidParametersError, createVerificationError, } from './errors.js';
|
|
8
|
+
import { assertConnected, assertValueContains, assertValueEquals, assertChecked, assertSelected, waitForAssertion, } from './assertions.js';
|
|
8
9
|
// Command ID counter for auto-generated IDs
|
|
9
10
|
let commandIdCounter = 0;
|
|
10
11
|
/**
|
|
@@ -336,12 +337,15 @@ export class DOMActions {
|
|
|
336
337
|
element.dispatchEvent(new MouseEvent('mouseup', eventInit));
|
|
337
338
|
element.dispatchEvent(new MouseEvent('click', eventInit));
|
|
338
339
|
}
|
|
339
|
-
|
|
340
|
+
// Check if element is still connected (graceful - doesn't throw)
|
|
341
|
+
// Element may be intentionally removed by click handlers (e.g., modal close, navigation)
|
|
342
|
+
const connectionResult = assertConnected(element);
|
|
343
|
+
return { success: true, error: null, connected: connectionResult.success };
|
|
340
344
|
}
|
|
341
345
|
async dblclick(selector) {
|
|
342
346
|
const element = this.getElement(selector);
|
|
343
347
|
element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }));
|
|
344
|
-
return {
|
|
348
|
+
return { success: true, error: null };
|
|
345
349
|
}
|
|
346
350
|
async type(selector, text, options = {}) {
|
|
347
351
|
const element = this.getElement(selector);
|
|
@@ -366,7 +370,12 @@ export class DOMActions {
|
|
|
366
370
|
}
|
|
367
371
|
}
|
|
368
372
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
369
|
-
|
|
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);
|
|
377
|
+
}
|
|
378
|
+
return { success: true, error: null };
|
|
370
379
|
}
|
|
371
380
|
async fill(selector, value) {
|
|
372
381
|
const element = this.getElement(selector);
|
|
@@ -379,7 +388,12 @@ export class DOMActions {
|
|
|
379
388
|
element.value = value;
|
|
380
389
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
381
390
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
382
|
-
|
|
391
|
+
// Wait for verification that value equals expected
|
|
392
|
+
const result = await waitForAssertion(() => assertValueEquals(element, value), { timeout: 1000, interval: 50 });
|
|
393
|
+
if (!result.success) {
|
|
394
|
+
throw createVerificationError('fill', result, selector);
|
|
395
|
+
}
|
|
396
|
+
return { success: true, error: null };
|
|
383
397
|
}
|
|
384
398
|
async clear(selector) {
|
|
385
399
|
const element = this.getElement(selector);
|
|
@@ -388,7 +402,7 @@ export class DOMActions {
|
|
|
388
402
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
389
403
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
390
404
|
}
|
|
391
|
-
return {
|
|
405
|
+
return { success: true, error: null };
|
|
392
406
|
}
|
|
393
407
|
async check(selector) {
|
|
394
408
|
const element = this.getElement(selector);
|
|
@@ -400,7 +414,12 @@ export class DOMActions {
|
|
|
400
414
|
if (!element.checked) {
|
|
401
415
|
element.click();
|
|
402
416
|
}
|
|
403
|
-
|
|
417
|
+
// Wait for verification that element is checked
|
|
418
|
+
const result = await waitForAssertion(() => assertChecked(element, true), { timeout: 1000, interval: 50 });
|
|
419
|
+
if (!result.success) {
|
|
420
|
+
throw createVerificationError('check', result, selector);
|
|
421
|
+
}
|
|
422
|
+
return { success: true, error: null };
|
|
404
423
|
}
|
|
405
424
|
async uncheck(selector) {
|
|
406
425
|
const element = this.getElement(selector);
|
|
@@ -412,7 +431,12 @@ export class DOMActions {
|
|
|
412
431
|
if (element.checked) {
|
|
413
432
|
element.click();
|
|
414
433
|
}
|
|
415
|
-
|
|
434
|
+
// Wait for verification that element is unchecked
|
|
435
|
+
const result = await waitForAssertion(() => assertChecked(element, false), { timeout: 1000, interval: 50 });
|
|
436
|
+
if (!result.success) {
|
|
437
|
+
throw createVerificationError('uncheck', result, selector);
|
|
438
|
+
}
|
|
439
|
+
return { success: true, error: null };
|
|
416
440
|
}
|
|
417
441
|
async select(selector, values) {
|
|
418
442
|
const element = this.getElement(selector);
|
|
@@ -426,27 +450,32 @@ export class DOMActions {
|
|
|
426
450
|
option.selected = valueArray.includes(option.value);
|
|
427
451
|
}
|
|
428
452
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
429
|
-
|
|
453
|
+
// Wait for verification that expected options are selected
|
|
454
|
+
const result = await waitForAssertion(() => assertSelected(element, valueArray), { timeout: 1000, interval: 50 });
|
|
455
|
+
if (!result.success) {
|
|
456
|
+
throw createVerificationError('select', result, selector);
|
|
457
|
+
}
|
|
458
|
+
return { success: true, error: null, values: valueArray };
|
|
430
459
|
}
|
|
431
460
|
async focus(selector) {
|
|
432
461
|
const element = this.getElement(selector);
|
|
433
462
|
if (element instanceof HTMLElement) {
|
|
434
463
|
element.focus();
|
|
435
464
|
}
|
|
436
|
-
return {
|
|
465
|
+
return { success: true, error: null };
|
|
437
466
|
}
|
|
438
467
|
async blur(selector) {
|
|
439
468
|
const element = this.getElement(selector);
|
|
440
469
|
if (element instanceof HTMLElement) {
|
|
441
470
|
element.blur();
|
|
442
471
|
}
|
|
443
|
-
return {
|
|
472
|
+
return { success: true, error: null };
|
|
444
473
|
}
|
|
445
474
|
async hover(selector) {
|
|
446
475
|
const element = this.getElement(selector);
|
|
447
476
|
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
448
477
|
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
449
|
-
return {
|
|
478
|
+
return { success: true, error: null };
|
|
450
479
|
}
|
|
451
480
|
async scroll(selector, options) {
|
|
452
481
|
// Validate parameter combinations
|
|
@@ -481,12 +510,12 @@ export class DOMActions {
|
|
|
481
510
|
else {
|
|
482
511
|
this.window.scrollBy(deltaX, deltaY);
|
|
483
512
|
}
|
|
484
|
-
return {
|
|
513
|
+
return { success: true, error: null };
|
|
485
514
|
}
|
|
486
515
|
async scrollIntoView(selector, block = 'center') {
|
|
487
516
|
const element = this.getElement(selector);
|
|
488
517
|
element.scrollIntoView({ behavior: 'smooth', block });
|
|
489
|
-
return {
|
|
518
|
+
return { success: true, error: null };
|
|
490
519
|
}
|
|
491
520
|
async snapshot(options) {
|
|
492
521
|
const root = options.selector
|
|
@@ -512,32 +541,34 @@ export class DOMActions {
|
|
|
512
541
|
async querySelector(selector) {
|
|
513
542
|
const element = this.queryElement(selector);
|
|
514
543
|
if (!element) {
|
|
515
|
-
return {
|
|
544
|
+
return { success: false, error: `Element not found: ${selector}` };
|
|
516
545
|
}
|
|
517
546
|
const ref = this.refMap.generateRef(element);
|
|
518
|
-
return {
|
|
547
|
+
return { success: true, error: null, ref };
|
|
519
548
|
}
|
|
520
549
|
async querySelectorAll(selector) {
|
|
521
550
|
const elements = this.queryElements(selector);
|
|
522
551
|
const refs = elements.map((el) => this.refMap.generateRef(el));
|
|
523
|
-
return { count: elements.length, refs };
|
|
552
|
+
return { success: true, error: null, count: elements.length, refs };
|
|
524
553
|
}
|
|
525
554
|
async getText(selector) {
|
|
526
555
|
const element = this.getElement(selector);
|
|
527
|
-
return { text: element.textContent };
|
|
556
|
+
return { success: true, error: null, text: element.textContent };
|
|
528
557
|
}
|
|
529
558
|
async getAttribute(selector, attribute) {
|
|
530
559
|
const element = this.getElement(selector);
|
|
531
|
-
return { value: element.getAttribute(attribute) };
|
|
560
|
+
return { success: true, error: null, value: element.getAttribute(attribute) };
|
|
532
561
|
}
|
|
533
562
|
async getProperty(selector, property) {
|
|
534
563
|
const element = this.getElement(selector);
|
|
535
|
-
return { value: element[property] };
|
|
564
|
+
return { success: true, error: null, value: element[property] };
|
|
536
565
|
}
|
|
537
566
|
async getBoundingBox(selector) {
|
|
538
567
|
const element = this.getElement(selector);
|
|
539
568
|
const rect = element.getBoundingClientRect();
|
|
540
569
|
return {
|
|
570
|
+
success: true,
|
|
571
|
+
error: null,
|
|
541
572
|
box: {
|
|
542
573
|
x: rect.x,
|
|
543
574
|
y: rect.y,
|
|
@@ -549,23 +580,23 @@ export class DOMActions {
|
|
|
549
580
|
async isVisible(selector) {
|
|
550
581
|
const element = this.queryElement(selector);
|
|
551
582
|
if (!element || !(element instanceof HTMLElement)) {
|
|
552
|
-
return { visible: false };
|
|
583
|
+
return { success: true, error: null, visible: false };
|
|
553
584
|
}
|
|
554
585
|
const style = this.window.getComputedStyle(element);
|
|
555
586
|
const visible = style.display !== 'none' &&
|
|
556
587
|
style.visibility !== 'hidden' &&
|
|
557
588
|
style.opacity !== '0';
|
|
558
|
-
return { visible };
|
|
589
|
+
return { success: true, error: null, visible };
|
|
559
590
|
}
|
|
560
591
|
async isEnabled(selector) {
|
|
561
592
|
const element = this.getElement(selector);
|
|
562
593
|
const enabled = !element.disabled;
|
|
563
|
-
return { enabled };
|
|
594
|
+
return { success: true, error: null, enabled };
|
|
564
595
|
}
|
|
565
596
|
async isChecked(selector) {
|
|
566
597
|
const element = this.getElement(selector);
|
|
567
598
|
const checked = element.checked ?? false;
|
|
568
|
-
return { checked };
|
|
599
|
+
return { success: true, error: null, checked };
|
|
569
600
|
}
|
|
570
601
|
async press(key, selector, modifiers = []) {
|
|
571
602
|
const target = selector
|
|
@@ -584,23 +615,23 @@ export class DOMActions {
|
|
|
584
615
|
target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
|
|
585
616
|
target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
|
|
586
617
|
target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
|
|
587
|
-
return {
|
|
618
|
+
return { success: true, error: null };
|
|
588
619
|
}
|
|
589
620
|
async keyDown(key) {
|
|
590
621
|
const target = this.document.activeElement || this.document.body;
|
|
591
622
|
target.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
592
|
-
return {
|
|
623
|
+
return { success: true, error: null };
|
|
593
624
|
}
|
|
594
625
|
async keyUp(key) {
|
|
595
626
|
const target = this.document.activeElement || this.document.body;
|
|
596
627
|
target.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
|
|
597
|
-
return {
|
|
628
|
+
return { success: true, error: null };
|
|
598
629
|
}
|
|
599
630
|
async wait(selector, options = {}) {
|
|
600
631
|
const { state = 'visible', timeout = 5000 } = options;
|
|
601
632
|
if (!selector) {
|
|
602
633
|
await this.sleep(timeout);
|
|
603
|
-
return {
|
|
634
|
+
return { success: true, error: null };
|
|
604
635
|
}
|
|
605
636
|
const startTime = Date.now();
|
|
606
637
|
let lastState;
|
|
@@ -649,7 +680,7 @@ export class DOMActions {
|
|
|
649
680
|
break;
|
|
650
681
|
}
|
|
651
682
|
if (conditionMet) {
|
|
652
|
-
return {
|
|
683
|
+
return { success: true, error: null };
|
|
653
684
|
}
|
|
654
685
|
await this.sleep(100);
|
|
655
686
|
}
|
|
@@ -659,7 +690,7 @@ export class DOMActions {
|
|
|
659
690
|
async evaluate(script, args) {
|
|
660
691
|
const fn = new Function(...(args?.map((_, i) => `arg${i}`) || []), `return (${script})`);
|
|
661
692
|
const result = fn.call(this.window, ...(args || []));
|
|
662
|
-
return { result };
|
|
693
|
+
return { success: true, error: null, result };
|
|
663
694
|
}
|
|
664
695
|
/**
|
|
665
696
|
* Validate element capabilities before attempting an action
|
|
@@ -765,15 +796,15 @@ export class DOMActions {
|
|
|
765
796
|
// Create overlay container with absolute positioning covering entire document
|
|
766
797
|
this.overlayContainer = this.document.createElement('div');
|
|
767
798
|
this.overlayContainer.id = 'btcp-highlight-overlay';
|
|
768
|
-
this.overlayContainer.style.cssText = `
|
|
769
|
-
position: absolute;
|
|
770
|
-
top: 0;
|
|
771
|
-
left: 0;
|
|
772
|
-
width: ${this.document.documentElement.scrollWidth}px;
|
|
773
|
-
height: ${this.document.documentElement.scrollHeight}px;
|
|
774
|
-
pointer-events: none;
|
|
775
|
-
z-index: 999999;
|
|
776
|
-
contain: layout style paint;
|
|
799
|
+
this.overlayContainer.style.cssText = `
|
|
800
|
+
position: absolute;
|
|
801
|
+
top: 0;
|
|
802
|
+
left: 0;
|
|
803
|
+
width: ${this.document.documentElement.scrollWidth}px;
|
|
804
|
+
height: ${this.document.documentElement.scrollHeight}px;
|
|
805
|
+
pointer-events: none;
|
|
806
|
+
z-index: 999999;
|
|
807
|
+
contain: layout style paint;
|
|
777
808
|
`;
|
|
778
809
|
let highlightedCount = 0;
|
|
779
810
|
// Create border overlays and labels for each ref
|
|
@@ -794,17 +825,17 @@ export class DOMActions {
|
|
|
794
825
|
const border = this.document.createElement('div');
|
|
795
826
|
border.className = 'btcp-ref-border';
|
|
796
827
|
border.dataset.ref = ref;
|
|
797
|
-
border.style.cssText = `
|
|
798
|
-
position: absolute;
|
|
799
|
-
width: ${bbox.width}px;
|
|
800
|
-
height: ${bbox.height}px;
|
|
801
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
802
|
-
border: 2px solid rgba(59, 130, 246, 0.8);
|
|
803
|
-
border-radius: 2px;
|
|
804
|
-
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
|
|
805
|
-
pointer-events: none;
|
|
806
|
-
will-change: transform;
|
|
807
|
-
contain: layout style paint;
|
|
828
|
+
border.style.cssText = `
|
|
829
|
+
position: absolute;
|
|
830
|
+
width: ${bbox.width}px;
|
|
831
|
+
height: ${bbox.height}px;
|
|
832
|
+
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
833
|
+
border: 2px solid rgba(59, 130, 246, 0.8);
|
|
834
|
+
border-radius: 2px;
|
|
835
|
+
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
|
|
836
|
+
pointer-events: none;
|
|
837
|
+
will-change: transform;
|
|
838
|
+
contain: layout style paint;
|
|
808
839
|
`;
|
|
809
840
|
// Create label
|
|
810
841
|
const label = this.document.createElement('div');
|
|
@@ -812,21 +843,21 @@ export class DOMActions {
|
|
|
812
843
|
label.dataset.ref = ref;
|
|
813
844
|
// Extract number from ref (e.g., "@ref:5" -> "5")
|
|
814
845
|
label.textContent = ref.replace('@ref:', '');
|
|
815
|
-
label.style.cssText = `
|
|
816
|
-
position: absolute;
|
|
817
|
-
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
818
|
-
background: rgba(59, 130, 246, 0.9);
|
|
819
|
-
color: white;
|
|
820
|
-
padding: 2px 6px;
|
|
821
|
-
border-radius: 3px;
|
|
822
|
-
font-family: monospace;
|
|
823
|
-
font-size: 11px;
|
|
824
|
-
font-weight: bold;
|
|
825
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
826
|
-
pointer-events: none;
|
|
827
|
-
white-space: nowrap;
|
|
828
|
-
will-change: transform;
|
|
829
|
-
contain: layout style paint;
|
|
846
|
+
label.style.cssText = `
|
|
847
|
+
position: absolute;
|
|
848
|
+
transform: translate3d(${bbox.left + this.window.scrollX}px, ${bbox.top + this.window.scrollY}px, 0);
|
|
849
|
+
background: rgba(59, 130, 246, 0.9);
|
|
850
|
+
color: white;
|
|
851
|
+
padding: 2px 6px;
|
|
852
|
+
border-radius: 3px;
|
|
853
|
+
font-family: monospace;
|
|
854
|
+
font-size: 11px;
|
|
855
|
+
font-weight: bold;
|
|
856
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
857
|
+
pointer-events: none;
|
|
858
|
+
white-space: nowrap;
|
|
859
|
+
will-change: transform;
|
|
860
|
+
contain: layout style paint;
|
|
830
861
|
`;
|
|
831
862
|
this.overlayContainer.appendChild(border);
|
|
832
863
|
this.overlayContainer.appendChild(label);
|
|
@@ -852,7 +883,7 @@ export class DOMActions {
|
|
|
852
883
|
};
|
|
853
884
|
// Use passive listener for better scroll performance
|
|
854
885
|
this.window.addEventListener('scroll', this.scrollListener, { passive: true });
|
|
855
|
-
return {
|
|
886
|
+
return { success: true, error: null, count: highlightedCount };
|
|
856
887
|
}
|
|
857
888
|
/**
|
|
858
889
|
* Invalidate snapshot data (called on navigation or manual clear)
|
|
@@ -921,7 +952,7 @@ export class DOMActions {
|
|
|
921
952
|
*/
|
|
922
953
|
async clearHighlight() {
|
|
923
954
|
this.clearExistingOverlay();
|
|
924
|
-
return {
|
|
955
|
+
return { success: true, error: null };
|
|
925
956
|
}
|
|
926
957
|
/**
|
|
927
958
|
* Remove existing overlay if it exists
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @btcp/core - Assertions Module
|
|
3
|
+
*
|
|
4
|
+
* Provides verification utilities for DOM actions with async waiting support.
|
|
5
|
+
* Returns structured results for consistent error handling.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Base interface for all action results
|
|
9
|
+
* Provides unified success/error pattern across all DOM operations
|
|
10
|
+
*
|
|
11
|
+
* Use intersection types to add action-specific data:
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* type ClickResult = ActionResult & { connected: boolean };
|
|
15
|
+
* type GetTextResult = ActionResult & { text: string | null };
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface ActionResult {
|
|
19
|
+
/** Whether the action succeeded */
|
|
20
|
+
success: boolean;
|
|
21
|
+
/** Error message if action failed, null otherwise */
|
|
22
|
+
error: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Result of a single assertion check
|
|
26
|
+
*/
|
|
27
|
+
export interface AssertionResult {
|
|
28
|
+
/** Whether the assertion succeeded */
|
|
29
|
+
success: boolean;
|
|
30
|
+
/** Error message if assertion failed, null otherwise */
|
|
31
|
+
error: string | null;
|
|
32
|
+
/** Human-readable description of what was checked */
|
|
33
|
+
description: string;
|
|
34
|
+
/** The expected value */
|
|
35
|
+
expected: unknown;
|
|
36
|
+
/** The actual value found */
|
|
37
|
+
actual: unknown;
|
|
38
|
+
/** Additional context information */
|
|
39
|
+
context?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Options for waiting on an assertion
|
|
43
|
+
*/
|
|
44
|
+
export interface WaitForAssertionOptions {
|
|
45
|
+
/** Maximum time to wait in milliseconds (default: 1000) */
|
|
46
|
+
timeout?: number;
|
|
47
|
+
/** Interval between checks in milliseconds (default: 50) */
|
|
48
|
+
interval?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Result of waiting for an assertion
|
|
52
|
+
*/
|
|
53
|
+
export interface WaitResult {
|
|
54
|
+
/** Whether the assertion passed within the timeout */
|
|
55
|
+
success: boolean;
|
|
56
|
+
/** Total time elapsed in milliseconds */
|
|
57
|
+
elapsed: number;
|
|
58
|
+
/** Number of attempts made */
|
|
59
|
+
attempts: number;
|
|
60
|
+
/** The final assertion result */
|
|
61
|
+
result: AssertionResult;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Assert that an element is still connected to the DOM
|
|
65
|
+
*/
|
|
66
|
+
export declare function assertConnected(element: Element): AssertionResult;
|
|
67
|
+
/**
|
|
68
|
+
* Assert that an input/textarea value contains the expected text
|
|
69
|
+
*/
|
|
70
|
+
export declare function assertValueContains(element: HTMLInputElement | HTMLTextAreaElement, text: string): AssertionResult;
|
|
71
|
+
/**
|
|
72
|
+
* Assert that an input/textarea value equals the expected value exactly
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertValueEquals(element: HTMLInputElement | HTMLTextAreaElement, value: string): AssertionResult;
|
|
75
|
+
/**
|
|
76
|
+
* Assert that a checkbox/radio is in the expected checked state
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertChecked(element: HTMLInputElement, expected: boolean): AssertionResult;
|
|
79
|
+
/**
|
|
80
|
+
* Assert that a select element has the expected values selected
|
|
81
|
+
*/
|
|
82
|
+
export declare function assertSelected(element: HTMLSelectElement, values: string[]): AssertionResult;
|
|
83
|
+
/**
|
|
84
|
+
* Assert that a URL matches the expected origin
|
|
85
|
+
*/
|
|
86
|
+
export declare function assertUrlOrigin(actualUrl: string, expectedUrl: string): AssertionResult;
|
|
87
|
+
/**
|
|
88
|
+
* Assert that an element is visible (or hidden)
|
|
89
|
+
*/
|
|
90
|
+
export declare function assertVisible(element: HTMLElement, win: Window, expected?: boolean): AssertionResult;
|
|
91
|
+
/**
|
|
92
|
+
* Assert that an element is enabled (or disabled)
|
|
93
|
+
*/
|
|
94
|
+
export declare function assertEnabled(element: HTMLElement, expected?: boolean): AssertionResult;
|
|
95
|
+
/**
|
|
96
|
+
* Wait for an assertion to pass within a timeout period
|
|
97
|
+
*
|
|
98
|
+
* Polls the assertion function at regular intervals until it passes
|
|
99
|
+
* or the timeout is reached.
|
|
100
|
+
*
|
|
101
|
+
* @param assertFn - Function that returns an AssertionResult
|
|
102
|
+
* @param options - Timeout and interval options
|
|
103
|
+
* @returns WaitResult with success status and timing information
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const result = await waitForAssertion(
|
|
108
|
+
* () => assertValueEquals(input, 'hello'),
|
|
109
|
+
* { timeout: 1000, interval: 50 }
|
|
110
|
+
* );
|
|
111
|
+
*
|
|
112
|
+
* if (!result.success) {
|
|
113
|
+
* throw createVerificationError('fill', result, selector);
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export declare function waitForAssertion(assertFn: () => AssertionResult, options?: WaitForAssertionOptions): Promise<WaitResult>;
|
|
118
|
+
//# sourceMappingURL=assertions.d.ts.map
|