@browserbridge/bbx 1.11.3 → 1.12.0
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 +14 -8
- package/packages/agent-client/src/client.js +18 -1
- package/packages/agent-client/src/types.ts +2 -0
- package/packages/mcp-server/src/handlers-capture.js +19 -7
- package/packages/mcp-server/src/handlers-dom.js +6 -1
- package/packages/mcp-server/src/handlers-utils.js +3 -0
- package/packages/mcp-server/src/server.js +59 -3
- package/packages/protocol/src/defaults.js +16 -0
- package/packages/protocol/src/protocol.js +366 -12
- package/packages/protocol/src/registry.js +34 -10
- package/packages/protocol/src/summary.js +249 -38
- package/packages/protocol/src/types.ts +157 -7
- package/skills/browser-bridge/SKILL.md +23 -13
- package/skills/browser-bridge/references/interaction.md +86 -21
- package/skills/browser-bridge/references/protocol.md +39 -10
- package/skills/browser-bridge/references/token-efficiency.md +12 -6
|
@@ -19,19 +19,25 @@ import {
|
|
|
19
19
|
DEFAULT_NETWORK_INTERCEPT_ACTION,
|
|
20
20
|
DEFAULT_NETWORK_LIMIT,
|
|
21
21
|
DEFAULT_PAGE_TEXT_BUDGET,
|
|
22
|
+
DEFAULT_PERFORM_TIMEOUT_MS,
|
|
22
23
|
DEFAULT_TEXT_BUDGET,
|
|
24
|
+
DEFAULT_TOUCH_HOLD_MS,
|
|
23
25
|
DEFAULT_VIEWPORT_HEIGHT,
|
|
24
26
|
DEFAULT_VIEWPORT_WIDTH,
|
|
25
27
|
DEFAULT_WAIT_TIMEOUT_MS,
|
|
26
28
|
MAX_EXTRACT_SETTLE_TIMEOUT_MS,
|
|
27
29
|
MAX_ARTIFACT_BYTES,
|
|
28
30
|
MAX_HAR_ENTRIES,
|
|
31
|
+
MAX_INPUT_HOLD_MS,
|
|
29
32
|
MAX_INTERCEPT_BODY_BYTES,
|
|
30
33
|
MAX_INTERCEPT_HEADER_BYTES,
|
|
31
34
|
MAX_INTERCEPT_HEADERS,
|
|
32
35
|
MAX_INTERCEPT_HEADER_VALUE_BYTES,
|
|
33
36
|
MAX_INTERCEPT_URL_PATTERN_LENGTH,
|
|
37
|
+
MAX_PERFORM_STEPS,
|
|
38
|
+
MAX_PERFORM_TIMEOUT_MS,
|
|
34
39
|
MAX_SENSITIVE_VALUE_BYTES,
|
|
40
|
+
MAX_TOUCH_POINTS,
|
|
35
41
|
} from './defaults.js';
|
|
36
42
|
import { BridgeError, ERROR_CODES, getErrorRecovery } from './errors.js';
|
|
37
43
|
import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry.js';
|
|
@@ -96,6 +102,8 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
96
102
|
/** @typedef {import('./types.js').NormalizedNetworkParams} NormalizedNetworkParams */
|
|
97
103
|
/** @typedef {import('./types.js').NormalizedPageTextParams} NormalizedPageTextParams */
|
|
98
104
|
/** @typedef {import('./types.js').NormalizedPatchOperation} NormalizedPatchOperation */
|
|
105
|
+
/** @typedef {import('./types.js').NormalizedPerformParams} NormalizedPerformParams */
|
|
106
|
+
/** @typedef {import('./types.js').NormalizedPerformStep} NormalizedPerformStep */
|
|
99
107
|
/** @typedef {import('./types.js').NormalizedSelectAction} NormalizedSelectAction */
|
|
100
108
|
/** @typedef {import('./types.js').NormalizedScreenshotParams} NormalizedScreenshotParams */
|
|
101
109
|
/** @typedef {import('./types.js').NormalizedStorageParams} NormalizedStorageParams */
|
|
@@ -103,11 +111,17 @@ import { BRIDGE_METHODS, METHOD_SET, createBridgeMethodGroups } from './registry
|
|
|
103
111
|
/** @typedef {import('./types.js').NormalizedStyleQuery} NormalizedStyleQuery */
|
|
104
112
|
/** @typedef {import('./types.js').NormalizedTabCloseParams} NormalizedTabCloseParams */
|
|
105
113
|
/** @typedef {import('./types.js').NormalizedTabCreateParams} NormalizedTabCreateParams */
|
|
114
|
+
/** @typedef {import('./types.js').NormalizedTouchParams} NormalizedTouchParams */
|
|
115
|
+
/** @typedef {import('./types.js').NormalizedTouchPoint} NormalizedTouchPoint */
|
|
116
|
+
/** @typedef {import('./types.js').NormalizedTouchPosition} NormalizedTouchPosition */
|
|
106
117
|
/** @typedef {import('./types.js').NormalizedViewportAction} NormalizedViewportAction */
|
|
107
118
|
/** @typedef {import('./types.js').NormalizedViewportResizeParams} NormalizedViewportResizeParams */
|
|
108
119
|
/** @typedef {import('./types.js').NormalizedWaitForLoadStateParams} NormalizedWaitForLoadStateParams */
|
|
109
120
|
/** @typedef {import('./types.js').NormalizedWaitForParams} NormalizedWaitForParams */
|
|
110
121
|
/** @typedef {import('./types.js').PageTextParams} PageTextParams */
|
|
122
|
+
/** @typedef {import('./types.js').PerformParams} PerformParams */
|
|
123
|
+
/** @typedef {import('./types.js').PerformStepMethod} PerformStepMethod */
|
|
124
|
+
/** @typedef {import('./types.js').TouchParams} TouchParams */
|
|
111
125
|
/** @typedef {import('./types.js').PatchOperationParams} PatchOperationParams */
|
|
112
126
|
/** @typedef {import('./types.js').SelectActionParams} SelectActionParams */
|
|
113
127
|
/** @typedef {import('./types.js').ScreenshotParams} ScreenshotParams */
|
|
@@ -401,6 +415,11 @@ export function validateBridgeRequest(request) {
|
|
|
401
415
|
source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
|
|
402
416
|
mcp_era: normalizeMcpEra(meta.source, meta.mcp_era),
|
|
403
417
|
automatic_retry: normalizeAutomaticRetryMeta(meta.source, meta.automatic_retry),
|
|
418
|
+
agent_session:
|
|
419
|
+
typeof meta.agent_session === 'string' &&
|
|
420
|
+
/^[A-Za-z0-9_.:-]{1,128}$/.test(meta.agent_session.trim())
|
|
421
|
+
? meta.agent_session.trim()
|
|
422
|
+
: undefined,
|
|
404
423
|
},
|
|
405
424
|
};
|
|
406
425
|
}
|
|
@@ -442,13 +461,14 @@ function normalizeAutomaticRetryMeta(source, value) {
|
|
|
442
461
|
|
|
443
462
|
/**
|
|
444
463
|
* Normalize request params early so malformed payloads fail at the shared
|
|
445
|
-
* protocol boundary before daemon or extension dispatch.
|
|
464
|
+
* protocol boundary before daemon or extension dispatch. Exported so later
|
|
465
|
+
* stages re-normalize with the same per-method options (auto-wait, observe).
|
|
446
466
|
*
|
|
447
467
|
* @param {BridgeMethod} method
|
|
448
468
|
* @param {Record<string, unknown>} params
|
|
449
469
|
* @returns {Record<string, unknown>}
|
|
450
470
|
*/
|
|
451
|
-
function normalizeRequestParams(method, params) {
|
|
471
|
+
export function normalizeRequestParams(method, params) {
|
|
452
472
|
switch (method) {
|
|
453
473
|
case 'access.request':
|
|
454
474
|
return normalizeAccessRequestParams(params);
|
|
@@ -512,7 +532,7 @@ function normalizeRequestParams(method, params) {
|
|
|
512
532
|
case 'input.type':
|
|
513
533
|
case 'input.fill':
|
|
514
534
|
case 'input.press_key':
|
|
515
|
-
return normalizeInputAction(params);
|
|
535
|
+
return withInputOptions(method, normalizeInputAction(params), params);
|
|
516
536
|
case 'cdp.dispatch_key_event':
|
|
517
537
|
return normalizeCdpDispatchKeyEventParams(params);
|
|
518
538
|
case 'cdp.get_box_model':
|
|
@@ -521,13 +541,17 @@ function normalizeRequestParams(method, params) {
|
|
|
521
541
|
case 'cdp.get_dom_snapshot':
|
|
522
542
|
return normalizeCdpDomSnapshotParams(params);
|
|
523
543
|
case 'input.set_checked':
|
|
524
|
-
return normalizeCheckedAction(params);
|
|
544
|
+
return withInputOptions(method, normalizeCheckedAction(params), params);
|
|
525
545
|
case 'input.select_option':
|
|
526
|
-
return normalizeSelectAction(params);
|
|
546
|
+
return withInputOptions(method, normalizeSelectAction(params), params);
|
|
527
547
|
case 'input.hover':
|
|
528
|
-
return normalizeHoverParams(params);
|
|
548
|
+
return withInputOptions(method, normalizeHoverParams(params), params);
|
|
529
549
|
case 'input.drag':
|
|
530
|
-
return normalizeDragParams(params);
|
|
550
|
+
return withInputOptions(method, normalizeDragParams(params), params);
|
|
551
|
+
case 'input.touch':
|
|
552
|
+
return normalizeTouchParams(params);
|
|
553
|
+
case 'input.perform':
|
|
554
|
+
return normalizeInputPerformParams(params);
|
|
531
555
|
case 'screenshot.capture_region':
|
|
532
556
|
return { ...params, ...normalizeScreenshotParams(params) };
|
|
533
557
|
case 'screenshot.capture_element':
|
|
@@ -604,11 +628,37 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
604
628
|
}
|
|
605
629
|
case 'dom.wait_for':
|
|
606
630
|
return clampInt(params.timeoutMs, 100, 30_000, 5_000);
|
|
631
|
+
case 'input.perform':
|
|
632
|
+
return normalizeInputPerformParams(params).timeoutMs;
|
|
633
|
+
case 'input.click':
|
|
634
|
+
case 'input.press_key':
|
|
635
|
+
case 'input.focus':
|
|
636
|
+
case 'input.type':
|
|
637
|
+
case 'input.fill':
|
|
638
|
+
case 'input.set_checked':
|
|
639
|
+
case 'input.select_option':
|
|
640
|
+
case 'input.hover':
|
|
641
|
+
case 'input.drag': {
|
|
642
|
+
const normalized =
|
|
643
|
+
/** @type {{ timeoutMs?: number, observe?: { settleMs: number } | null }} */ (
|
|
644
|
+
normalizeRequestParams(method, params)
|
|
645
|
+
);
|
|
646
|
+
const holdMs = normalizeHoldMs(params.holdMs, 0);
|
|
647
|
+
const waitMs = normalized.timeoutMs ?? 0;
|
|
648
|
+
const settleMs = normalized.observe?.settleMs ?? 0;
|
|
649
|
+
const total = holdMs + waitMs + settleMs;
|
|
650
|
+
return total > 0 ? total + INPUT_HOLD_TIMEOUT_MARGIN_MS : null;
|
|
651
|
+
}
|
|
652
|
+
case 'input.touch':
|
|
653
|
+
return normalizeHoldMs(params.holdMs, DEFAULT_TOUCH_HOLD_MS) + INPUT_HOLD_TIMEOUT_MARGIN_MS;
|
|
607
654
|
default:
|
|
608
655
|
return null;
|
|
609
656
|
}
|
|
610
657
|
}
|
|
611
658
|
|
|
659
|
+
/** Resolution and dispatch allowance added around an input hold (ms). */
|
|
660
|
+
const INPUT_HOLD_TIMEOUT_MARGIN_MS = 5_000;
|
|
661
|
+
|
|
612
662
|
/**
|
|
613
663
|
* @param {DomQueryParams} [params={}]
|
|
614
664
|
* @returns {NormalizedDomQuery}
|
|
@@ -724,15 +774,107 @@ export function normalizeStyleQuery(params = {}) {
|
|
|
724
774
|
};
|
|
725
775
|
}
|
|
726
776
|
|
|
777
|
+
const TARGET_LOCATOR_KEYS = /** @type {const} */ ([
|
|
778
|
+
'role',
|
|
779
|
+
'name',
|
|
780
|
+
'text',
|
|
781
|
+
'label',
|
|
782
|
+
'placeholder',
|
|
783
|
+
'testId',
|
|
784
|
+
]);
|
|
785
|
+
const MAX_LOCATOR_VALUE_LENGTH = 500;
|
|
786
|
+
|
|
727
787
|
/**
|
|
728
|
-
*
|
|
788
|
+
* Normalize an input target. Besides `elementRef` and `selector`, a target can
|
|
789
|
+
* be a semantic locator (`role` + `name`, `text`, `label`, `placeholder`,
|
|
790
|
+
* `testId`) so one action call can find and act on an element. `selector`
|
|
791
|
+
* then narrows locator candidates; `nth` picks one of several matches.
|
|
792
|
+
*
|
|
793
|
+
* @param {Record<string, unknown> | null | undefined} target
|
|
729
794
|
* @returns {import('./types.js').InputTarget}
|
|
730
795
|
*/
|
|
731
796
|
function normalizeTarget(target) {
|
|
732
|
-
|
|
797
|
+
/** @type {import('./types.js').InputTarget} */
|
|
798
|
+
const normalized = {
|
|
733
799
|
elementRef: typeof target?.elementRef === 'string' ? target.elementRef : undefined,
|
|
734
800
|
selector: typeof target?.selector === 'string' ? target.selector : undefined,
|
|
735
801
|
};
|
|
802
|
+
if (!target) return normalized;
|
|
803
|
+
for (const key of TARGET_LOCATOR_KEYS) {
|
|
804
|
+
const value = target[key];
|
|
805
|
+
if (typeof value !== 'string' || !value.trim()) continue;
|
|
806
|
+
if (value.length > MAX_LOCATOR_VALUE_LENGTH) {
|
|
807
|
+
throw new BridgeError(
|
|
808
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
809
|
+
`target.${key} must not exceed ${MAX_LOCATOR_VALUE_LENGTH} characters.`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
normalized[key] = value.trim();
|
|
813
|
+
}
|
|
814
|
+
if (target.exact === true) normalized.exact = true;
|
|
815
|
+
if (target.nth !== undefined && target.nth !== null) {
|
|
816
|
+
const nth = Number(target.nth);
|
|
817
|
+
if (!Number.isInteger(nth) || nth < 0 || nth > 49) {
|
|
818
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'target.nth must be an integer 0-49.');
|
|
819
|
+
}
|
|
820
|
+
normalized.nth = nth;
|
|
821
|
+
}
|
|
822
|
+
return normalized;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Default bounded wait for a target to exist and become actionable (ms). */
|
|
826
|
+
export const DEFAULT_INPUT_WAIT_MS = 2_500;
|
|
827
|
+
export const MAX_INPUT_WAIT_MS = 15_000;
|
|
828
|
+
/** Default and maximum post-action settle window used by `observe` (ms). */
|
|
829
|
+
export const DEFAULT_OBSERVE_SETTLE_MS = 500;
|
|
830
|
+
export const MAX_OBSERVE_SETTLE_MS = 5_000;
|
|
831
|
+
|
|
832
|
+
/** Actions that report their observed effects unless `observe: false`. */
|
|
833
|
+
const OBSERVE_BY_DEFAULT_METHODS = new Set([
|
|
834
|
+
'input.click',
|
|
835
|
+
'input.press_key',
|
|
836
|
+
'input.select_option',
|
|
837
|
+
'input.set_checked',
|
|
838
|
+
]);
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* @param {string} method
|
|
842
|
+
* @param {unknown} value
|
|
843
|
+
* @param {{ submit?: boolean }} normalized
|
|
844
|
+
* @returns {{ settleMs: number } | null}
|
|
845
|
+
*/
|
|
846
|
+
function normalizeObserveOption(method, value, normalized) {
|
|
847
|
+
if (value === false || value === null) return null;
|
|
848
|
+
if (value && typeof value === 'object') {
|
|
849
|
+
const settleMs = /** @type {{ settleMs?: unknown }} */ (value).settleMs;
|
|
850
|
+
return {
|
|
851
|
+
settleMs: clampInt(settleMs, 0, MAX_OBSERVE_SETTLE_MS, DEFAULT_OBSERVE_SETTLE_MS),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const observeByDefault =
|
|
855
|
+
OBSERVE_BY_DEFAULT_METHODS.has(method) ||
|
|
856
|
+
((method === 'input.type' || method === 'input.fill') && normalized.submit === true);
|
|
857
|
+
return value === true || observeByDefault ? { settleMs: DEFAULT_OBSERVE_SETTLE_MS } : null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Add the shared auto-wait and observation options to normalized input params.
|
|
862
|
+
*
|
|
863
|
+
* @template {Record<string, unknown>} T
|
|
864
|
+
* @param {string} method
|
|
865
|
+
* @param {T} normalized
|
|
866
|
+
* @param {Record<string, unknown>} raw
|
|
867
|
+
* @returns {T & { timeoutMs: number, observe: { settleMs: number } | null }}
|
|
868
|
+
*/
|
|
869
|
+
function withInputOptions(method, normalized, raw) {
|
|
870
|
+
return {
|
|
871
|
+
...normalized,
|
|
872
|
+
timeoutMs:
|
|
873
|
+
typeof raw.timeoutMs === 'number' && Number.isFinite(raw.timeoutMs)
|
|
874
|
+
? Math.round(clampNumber(raw.timeoutMs, 0, MAX_INPUT_WAIT_MS, DEFAULT_INPUT_WAIT_MS))
|
|
875
|
+
: DEFAULT_INPUT_WAIT_MS,
|
|
876
|
+
observe: normalizeObserveOption(method, raw.observe, normalized),
|
|
877
|
+
};
|
|
736
878
|
}
|
|
737
879
|
|
|
738
880
|
/**
|
|
@@ -740,10 +882,10 @@ function normalizeTarget(target) {
|
|
|
740
882
|
* @returns {import('./types.js').InputExecutionMode}
|
|
741
883
|
*/
|
|
742
884
|
function normalizeInputExecutionMode(value) {
|
|
743
|
-
if (value !== undefined && value !== 'dom' && value !== 'cdp') {
|
|
744
|
-
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'executionMode must be
|
|
885
|
+
if (value !== undefined && value !== 'dom' && value !== 'cdp' && value !== 'auto') {
|
|
886
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'executionMode must be dom, cdp, or auto.');
|
|
745
887
|
}
|
|
746
|
-
return value === 'cdp'
|
|
888
|
+
return value === 'cdp' || value === 'auto' ? value : 'dom';
|
|
747
889
|
}
|
|
748
890
|
|
|
749
891
|
/**
|
|
@@ -774,11 +916,209 @@ export function normalizeInputAction(params = {}) {
|
|
|
774
916
|
modifiers: Array.isArray(params.modifiers)
|
|
775
917
|
? params.modifiers.filter((modifier) => typeof modifier === 'string' && modifier.trim())
|
|
776
918
|
: [],
|
|
919
|
+
holdMs: normalizeHoldMs(params.holdMs, 0),
|
|
777
920
|
executionMode: normalizeInputExecutionMode(params.executionMode),
|
|
778
921
|
recoverStale: params.recoverStale === true,
|
|
779
922
|
};
|
|
780
923
|
}
|
|
781
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Normalize a press-and-hold duration, honoring an explicit zero.
|
|
927
|
+
*
|
|
928
|
+
* @param {unknown} value
|
|
929
|
+
* @param {number} fallback
|
|
930
|
+
* @returns {number}
|
|
931
|
+
*/
|
|
932
|
+
function normalizeHoldMs(value, fallback) {
|
|
933
|
+
return Math.round(clampNumber(value, 0, MAX_INPUT_HOLD_MS, fallback));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* @param {unknown} value
|
|
938
|
+
* @param {string} label
|
|
939
|
+
* @returns {NormalizedTouchPosition}
|
|
940
|
+
*/
|
|
941
|
+
function normalizeTouchPosition(value, label) {
|
|
942
|
+
if (!isPlainRecord(value)) {
|
|
943
|
+
throw new BridgeError(
|
|
944
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
945
|
+
`${label} must be an object with either target or numeric x and y.`
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
const hasTarget = isPlainRecord(value.target);
|
|
949
|
+
const hasPoint =
|
|
950
|
+
typeof value.x === 'number' &&
|
|
951
|
+
Number.isFinite(value.x) &&
|
|
952
|
+
typeof value.y === 'number' &&
|
|
953
|
+
Number.isFinite(value.y);
|
|
954
|
+
if (hasTarget === hasPoint) {
|
|
955
|
+
throw new BridgeError(
|
|
956
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
957
|
+
`${label} needs exactly one of target or numeric x and y.`
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (hasTarget) {
|
|
961
|
+
return {
|
|
962
|
+
target: normalizeTarget(
|
|
963
|
+
/** @type {{ elementRef?: string, selector?: string }} */ (value.target)
|
|
964
|
+
),
|
|
965
|
+
x: null,
|
|
966
|
+
y: null,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
return { target: null, x: /** @type {number} */ (value.x), y: /** @type {number} */ (value.y) };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Normalize a simultaneous touch gesture: one to MAX_TOUCH_POINTS fingers,
|
|
974
|
+
* each placed on a target or viewport point and optionally moved to an end
|
|
975
|
+
* position while all fingers stay down for holdMs.
|
|
976
|
+
*
|
|
977
|
+
* @param {TouchParams} [params={}]
|
|
978
|
+
* @returns {NormalizedTouchParams}
|
|
979
|
+
*/
|
|
980
|
+
export function normalizeTouchParams(params = {}) {
|
|
981
|
+
const rawPoints = params.points;
|
|
982
|
+
if (!Array.isArray(rawPoints) || rawPoints.length < 1 || rawPoints.length > MAX_TOUCH_POINTS) {
|
|
983
|
+
throw new BridgeError(
|
|
984
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
985
|
+
`points must be an array of 1 to ${MAX_TOUCH_POINTS} touch points.`
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
/** @type {NormalizedTouchPoint[]} */
|
|
989
|
+
const points = rawPoints.map((point, index) => {
|
|
990
|
+
const start = normalizeTouchPosition(point, `points[${index}]`);
|
|
991
|
+
const rawEnd = isPlainRecord(point) ? point.to : undefined;
|
|
992
|
+
return {
|
|
993
|
+
...start,
|
|
994
|
+
to: rawEnd == null ? null : normalizeTouchPosition(rawEnd, `points[${index}].to`),
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
return {
|
|
998
|
+
points,
|
|
999
|
+
holdMs: normalizeHoldMs(params.holdMs, DEFAULT_TOUCH_HOLD_MS),
|
|
1000
|
+
moveSteps: clampInt(params.moveSteps, 1, 60, 10),
|
|
1001
|
+
executionMode: normalizeInputExecutionMode(params.executionMode),
|
|
1002
|
+
recoverStale: params.recoverStale === true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** Methods that may appear as input.perform steps. */
|
|
1007
|
+
const PERFORM_STEP_METHODS = new Set([
|
|
1008
|
+
'input.click',
|
|
1009
|
+
'input.focus',
|
|
1010
|
+
'input.type',
|
|
1011
|
+
'input.fill',
|
|
1012
|
+
'input.press_key',
|
|
1013
|
+
'input.set_checked',
|
|
1014
|
+
'input.select_option',
|
|
1015
|
+
'input.hover',
|
|
1016
|
+
'input.drag',
|
|
1017
|
+
'input.touch',
|
|
1018
|
+
'input.scroll_into_view',
|
|
1019
|
+
'viewport.scroll',
|
|
1020
|
+
'dom.wait_for',
|
|
1021
|
+
'page.wait_for_load_state',
|
|
1022
|
+
]);
|
|
1023
|
+
|
|
1024
|
+
/** Step methods that accept executionMode=cdp. */
|
|
1025
|
+
const CDP_CAPABLE_STEP_METHODS = new Set([
|
|
1026
|
+
'input.click',
|
|
1027
|
+
'input.hover',
|
|
1028
|
+
'input.drag',
|
|
1029
|
+
'input.type',
|
|
1030
|
+
'input.fill',
|
|
1031
|
+
'input.press_key',
|
|
1032
|
+
'input.touch',
|
|
1033
|
+
]);
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* @param {unknown} value
|
|
1037
|
+
* @param {string} label
|
|
1038
|
+
* @param {number} maximum
|
|
1039
|
+
* @returns {number | null}
|
|
1040
|
+
*/
|
|
1041
|
+
function normalizeStepTiming(value, label, maximum) {
|
|
1042
|
+
if (value == null) return null;
|
|
1043
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > maximum) {
|
|
1044
|
+
throw new BridgeError(
|
|
1045
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1046
|
+
`${label} must be a number between 0 and the sequence timeoutMs (${maximum}).`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
return Math.round(value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Normalize an ordered, timed input sequence. Each step reuses an existing
|
|
1054
|
+
* bridge method and its own normalizer; the sequence adds only scheduling.
|
|
1055
|
+
*
|
|
1056
|
+
* @param {PerformParams} [params={}]
|
|
1057
|
+
* @returns {NormalizedPerformParams}
|
|
1058
|
+
*/
|
|
1059
|
+
export function normalizeInputPerformParams(params = {}) {
|
|
1060
|
+
const timeoutMs = clampInt(
|
|
1061
|
+
params.timeoutMs,
|
|
1062
|
+
100,
|
|
1063
|
+
MAX_PERFORM_TIMEOUT_MS,
|
|
1064
|
+
DEFAULT_PERFORM_TIMEOUT_MS
|
|
1065
|
+
);
|
|
1066
|
+
const executionMode =
|
|
1067
|
+
params.executionMode === undefined ? null : normalizeInputExecutionMode(params.executionMode);
|
|
1068
|
+
const rawSteps = params.steps;
|
|
1069
|
+
if (!Array.isArray(rawSteps) || rawSteps.length < 1 || rawSteps.length > MAX_PERFORM_STEPS) {
|
|
1070
|
+
throw new BridgeError(
|
|
1071
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1072
|
+
`steps must be an array of 1 to ${MAX_PERFORM_STEPS} steps.`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
/** @type {NormalizedPerformStep[]} */
|
|
1076
|
+
const steps = rawSteps.map((step, index) => {
|
|
1077
|
+
if (!isPlainRecord(step)) {
|
|
1078
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, `steps[${index}] must be an object.`);
|
|
1079
|
+
}
|
|
1080
|
+
const method = step.method;
|
|
1081
|
+
if (typeof method !== 'string' || !PERFORM_STEP_METHODS.has(method)) {
|
|
1082
|
+
throw new BridgeError(
|
|
1083
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1084
|
+
`steps[${index}].method must be one of ${[...PERFORM_STEP_METHODS].join(', ')}.`
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (step.params !== undefined && !isPlainRecord(step.params)) {
|
|
1088
|
+
throw new BridgeError(
|
|
1089
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1090
|
+
`steps[${index}].params must be an object.`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
const rawParams = /** @type {Record<string, unknown>} */ (step.params ?? {});
|
|
1094
|
+
const stepParams =
|
|
1095
|
+
executionMode && CDP_CAPABLE_STEP_METHODS.has(method) && rawParams.executionMode === undefined
|
|
1096
|
+
? { ...rawParams, executionMode }
|
|
1097
|
+
: rawParams;
|
|
1098
|
+
const delayMs = normalizeStepTiming(step.delayMs, `steps[${index}].delayMs`, timeoutMs);
|
|
1099
|
+
const atMs = normalizeStepTiming(step.atMs, `steps[${index}].atMs`, timeoutMs);
|
|
1100
|
+
if (delayMs && atMs !== null) {
|
|
1101
|
+
throw new BridgeError(
|
|
1102
|
+
ERROR_CODES.INVALID_REQUEST,
|
|
1103
|
+
`steps[${index}] accepts either delayMs or atMs, not both.`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const stepMethod = /** @type {PerformStepMethod} */ (method);
|
|
1107
|
+
const normalizedStepParams = normalizeRequestParams(stepMethod, stepParams);
|
|
1108
|
+
// Timed sequences keep their schedule: steps observe effects only on request.
|
|
1109
|
+
if (rawParams.observe === undefined && 'observe' in normalizedStepParams) {
|
|
1110
|
+
normalizedStepParams.observe = null;
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
method: stepMethod,
|
|
1114
|
+
params: normalizedStepParams,
|
|
1115
|
+
delayMs: delayMs ?? 0,
|
|
1116
|
+
atMs,
|
|
1117
|
+
};
|
|
1118
|
+
});
|
|
1119
|
+
return { steps, timeoutMs, continueOnError: params.continueOnError === true };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
782
1122
|
/**
|
|
783
1123
|
* @param {CdpDispatchKeyEventParams} [params={}]
|
|
784
1124
|
* @returns {NormalizedCdpDispatchKeyEventParams}
|
|
@@ -1119,6 +1459,7 @@ export function normalizeFindByTextParams(params = {}) {
|
|
|
1119
1459
|
exact: Boolean(params.exact),
|
|
1120
1460
|
selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '*',
|
|
1121
1461
|
maxResults: clampInt(params.maxResults, 1, 50, 10),
|
|
1462
|
+
includeHidden: params.includeHidden === true,
|
|
1122
1463
|
};
|
|
1123
1464
|
}
|
|
1124
1465
|
|
|
@@ -1130,8 +1471,10 @@ export function normalizeFindByRoleParams(params = {}) {
|
|
|
1130
1471
|
return {
|
|
1131
1472
|
role: typeof params.role === 'string' ? params.role : '',
|
|
1132
1473
|
name: typeof params.name === 'string' ? params.name : '',
|
|
1474
|
+
exact: Boolean(params.exact),
|
|
1133
1475
|
selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '*',
|
|
1134
1476
|
maxResults: clampInt(params.maxResults, 1, 50, 10),
|
|
1477
|
+
includeHidden: params.includeHidden === true,
|
|
1135
1478
|
};
|
|
1136
1479
|
}
|
|
1137
1480
|
|
|
@@ -1381,6 +1724,14 @@ export function normalizeTabCloseParams(params = {}) {
|
|
|
1381
1724
|
* @returns {NormalizedAccessibilityTreeParams}
|
|
1382
1725
|
*/
|
|
1383
1726
|
export function normalizeAccessibilityTreeParams(params = {}) {
|
|
1727
|
+
const source = params.source ?? 'cdp';
|
|
1728
|
+
if (source !== 'cdp' && source !== 'dom') {
|
|
1729
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'source must be cdp or dom.');
|
|
1730
|
+
}
|
|
1731
|
+
const format = params.format ?? (source === 'dom' ? 'outline' : 'tree');
|
|
1732
|
+
if (format !== 'tree' && format !== 'outline') {
|
|
1733
|
+
throw new BridgeError(ERROR_CODES.INVALID_REQUEST, 'format must be tree or outline.');
|
|
1734
|
+
}
|
|
1384
1735
|
return {
|
|
1385
1736
|
selector:
|
|
1386
1737
|
typeof params.selector === 'string' && params.selector.trim() ? params.selector.trim() : null,
|
|
@@ -1388,6 +1739,9 @@ export function normalizeAccessibilityTreeParams(params = {}) {
|
|
|
1388
1739
|
maxNodes: clampInt(params.maxNodes, 10, 5000, DEFAULT_A11Y_MAX_NODES),
|
|
1389
1740
|
compact: params.compact === true,
|
|
1390
1741
|
interactiveOnly: params.interactiveOnly === true,
|
|
1742
|
+
// Omitted at defaults so existing CDP callers see an unchanged shape.
|
|
1743
|
+
...(source === 'dom' ? { source } : {}),
|
|
1744
|
+
...(format === 'outline' ? { format } : {}),
|
|
1391
1745
|
};
|
|
1392
1746
|
}
|
|
1393
1747
|
|
|
@@ -140,11 +140,13 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
140
140
|
'dom.get_text': 'Read bounded text for one element.',
|
|
141
141
|
'dom.get_attributes': 'Read selected attributes for one element.',
|
|
142
142
|
'dom.wait_for': 'Wait for a selector or text condition in the DOM.',
|
|
143
|
-
'dom.find_by_text':
|
|
144
|
-
|
|
143
|
+
'dom.find_by_text':
|
|
144
|
+
'Find the innermost visible elements by text, including open/closed shadow DOM.',
|
|
145
|
+
'dom.find_by_role':
|
|
146
|
+
'Find visible elements by ARIA role and accessible name (labels, aria-*, alt, content).',
|
|
145
147
|
'dom.get_html': 'Read inner or outer HTML for one element.',
|
|
146
148
|
'dom.get_accessibility_tree':
|
|
147
|
-
'Read
|
|
149
|
+
'Read an accessibility tree; source=dom returns a compact actionable outline with elementRefs and no debugger.',
|
|
148
150
|
'layout.get_box_model': 'Read the box model for one element.',
|
|
149
151
|
'layout.hit_test': 'Resolve the topmost element at a viewport point.',
|
|
150
152
|
'styles.get_computed':
|
|
@@ -153,16 +155,22 @@ const BRIDGE_METHOD_DESCRIPTIONS = Object.freeze({
|
|
|
153
155
|
'Read element classes and inline style context (not stylesheet cascade data).',
|
|
154
156
|
'viewport.scroll': 'Scroll the viewport or a scrollable element.',
|
|
155
157
|
'viewport.resize': 'Resize or reset the tab viewport.',
|
|
156
|
-
'input.click':
|
|
158
|
+
'input.click':
|
|
159
|
+
'Actionability-check and click an element through DOM or optional CDP input; holdMs keeps the button pressed.',
|
|
157
160
|
'input.focus': 'Actionability-check and focus an element through DOM input.',
|
|
158
161
|
'input.type': 'Actionability-check and type through DOM or optional CDP text input.',
|
|
159
162
|
'input.fill':
|
|
160
163
|
'Fill an editable target using a DOM strategy or optional CDP text input; verify afterward.',
|
|
161
|
-
'input.press_key':
|
|
164
|
+
'input.press_key':
|
|
165
|
+
'Send a key press to the page or an element through DOM or optional CDP input; holdMs keeps the key down.',
|
|
162
166
|
'input.set_checked': 'Set checkbox or radio checked state.',
|
|
163
167
|
'input.select_option': 'Select options in a select element.',
|
|
164
168
|
'input.hover': 'Actionability-check and hover through DOM or optional CDP pointer input.',
|
|
165
169
|
'input.drag': 'Actionability-check and drag through DOM or optional CDP pointer input.',
|
|
170
|
+
'input.touch':
|
|
171
|
+
'Press one or more touch points at the same time (chords, multi-finger taps, swipes, pinches) through DOM or optional CDP touch input.',
|
|
172
|
+
'input.perform':
|
|
173
|
+
'Run an ordered, timed sequence of input steps inside the browser in one call; use for rhythm, games, gestures, and multi-step flows.',
|
|
166
174
|
'input.scroll_into_view': 'Scroll an element into the visible viewport.',
|
|
167
175
|
'screenshot.capture_region': 'Capture a screenshot of a viewport region.',
|
|
168
176
|
'screenshot.capture_element': 'Capture a screenshot of one element.',
|
|
@@ -516,7 +524,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
516
524
|
'dom.find_by_text',
|
|
517
525
|
'inspect',
|
|
518
526
|
true,
|
|
519
|
-
['text', 'exact', 'selector', 'maxResults'],
|
|
527
|
+
['text', 'exact', 'selector', 'maxResults', 'includeHidden'],
|
|
520
528
|
'low',
|
|
521
529
|
METHOD_POLICIES.domRead
|
|
522
530
|
),
|
|
@@ -524,7 +532,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
524
532
|
'dom.find_by_role',
|
|
525
533
|
'inspect',
|
|
526
534
|
true,
|
|
527
|
-
['role', 'name', 'selector', 'maxResults'],
|
|
535
|
+
['role', 'name', 'exact', 'selector', 'maxResults', 'includeHidden'],
|
|
528
536
|
'low',
|
|
529
537
|
METHOD_POLICIES.domRead
|
|
530
538
|
),
|
|
@@ -540,7 +548,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
540
548
|
'dom.get_accessibility_tree',
|
|
541
549
|
'inspect',
|
|
542
550
|
true,
|
|
543
|
-
['selector', 'maxNodes', 'maxDepth', 'compact', 'interactiveOnly'],
|
|
551
|
+
['selector', 'maxNodes', 'maxDepth', 'compact', 'interactiveOnly', 'source', 'format'],
|
|
544
552
|
'moderate',
|
|
545
553
|
METHOD_POLICIES.domReadDebugger
|
|
546
554
|
),
|
|
@@ -600,7 +608,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
600
608
|
'input.click',
|
|
601
609
|
'interact',
|
|
602
610
|
true,
|
|
603
|
-
['target', 'button', 'clickCount', 'modifiers', 'executionMode', 'recoverStale'],
|
|
611
|
+
['target', 'button', 'clickCount', 'modifiers', 'holdMs', 'executionMode', 'recoverStale'],
|
|
604
612
|
'low',
|
|
605
613
|
METHOD_POLICIES.automationInput
|
|
606
614
|
),
|
|
@@ -632,7 +640,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
632
640
|
'input.press_key',
|
|
633
641
|
'interact',
|
|
634
642
|
true,
|
|
635
|
-
['target', 'key', 'modifiers', 'executionMode', 'recoverStale'],
|
|
643
|
+
['target', 'key', 'modifiers', 'holdMs', 'executionMode', 'recoverStale'],
|
|
636
644
|
'low',
|
|
637
645
|
METHOD_POLICIES.automationInput
|
|
638
646
|
),
|
|
@@ -668,6 +676,22 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
668
676
|
'moderate',
|
|
669
677
|
METHOD_POLICIES.automationInput
|
|
670
678
|
),
|
|
679
|
+
'input.touch': createRegistryEntry(
|
|
680
|
+
'input.touch',
|
|
681
|
+
'interact',
|
|
682
|
+
true,
|
|
683
|
+
['points', 'holdMs', 'moveSteps', 'executionMode', 'recoverStale'],
|
|
684
|
+
'moderate',
|
|
685
|
+
METHOD_POLICIES.automationInput
|
|
686
|
+
),
|
|
687
|
+
'input.perform': createRegistryEntry(
|
|
688
|
+
'input.perform',
|
|
689
|
+
'interact',
|
|
690
|
+
true,
|
|
691
|
+
['steps', 'executionMode', 'timeoutMs', 'continueOnError'],
|
|
692
|
+
'moderate',
|
|
693
|
+
METHOD_POLICIES.automationInput
|
|
694
|
+
),
|
|
671
695
|
'input.scroll_into_view': createRegistryEntry(
|
|
672
696
|
'input.scroll_into_view',
|
|
673
697
|
'interact',
|