@browserbridge/bbx 1.11.5 → 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 +3 -1
- 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 +17 -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 +49 -2
- package/packages/protocol/src/protocol.js +146 -15
- package/packages/protocol/src/registry.js +8 -6
- package/packages/protocol/src/summary.js +184 -32
- package/packages/protocol/src/types.ts +58 -7
- package/skills/browser-bridge/SKILL.md +18 -12
- package/skills/browser-bridge/references/interaction.md +12 -16
- package/skills/browser-bridge/references/protocol.md +16 -6
- package/skills/browser-bridge/references/token-efficiency.md +12 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserbridge/bbx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-tools",
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
"format": "oxfmt .",
|
|
57
57
|
"format:check": "oxfmt --check .",
|
|
58
58
|
"fixture:browser": "node tests/fixtures/browser-reliability/server.mjs",
|
|
59
|
+
"fixture:interaction": "node tests/fixtures/agent-interaction/server.mjs",
|
|
60
|
+
"bench:interaction": "node tests/fixtures/agent-interaction/bench.mjs",
|
|
59
61
|
"test:runtime": "node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
|
|
60
62
|
"test": "c8 node --import tsx --test packages/*/test/*.test.ts packages/integration-tests/*.test.ts tests/fixtures/*/*.test.ts",
|
|
61
63
|
"coverage": "c8 report --reporter=text --reporter=text-summary",
|
|
@@ -139,6 +139,18 @@ async function writeSocketLine(socket, line, signal) {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Agent session that keeps tab routing sticky across separate CLI processes.
|
|
144
|
+
* Set `BBX_SESSION` to give one agent its own working tab; unset callers share
|
|
145
|
+
* the extension's default session.
|
|
146
|
+
*
|
|
147
|
+
* @returns {string | null}
|
|
148
|
+
*/
|
|
149
|
+
export function getDefaultAgentSession() {
|
|
150
|
+
const value = process.env.BBX_SESSION?.trim();
|
|
151
|
+
return value ? value : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
export class BridgeClient extends EventEmitter {
|
|
143
155
|
/**
|
|
144
156
|
* @param {BridgeClientOptions} [options={}]
|
|
@@ -156,6 +168,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
156
168
|
exitProcessOnNpmUpdate = false,
|
|
157
169
|
updateCompatibleNpmPackageFn = updateCompatibleNpmPackage,
|
|
158
170
|
authToken = undefined,
|
|
171
|
+
agentSession = getDefaultAgentSession(),
|
|
159
172
|
} = {}) {
|
|
160
173
|
super();
|
|
161
174
|
this.transport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
|
|
@@ -171,6 +184,7 @@ export class BridgeClient extends EventEmitter {
|
|
|
171
184
|
this.exitProcessOnNpmUpdate = exitProcessOnNpmUpdate;
|
|
172
185
|
this.updateCompatibleNpmPackageFn = updateCompatibleNpmPackageFn;
|
|
173
186
|
this.authToken = authToken;
|
|
187
|
+
this.agentSession = agentSession;
|
|
174
188
|
this.socket = null;
|
|
175
189
|
this.connected = false;
|
|
176
190
|
this.protocolCompatibility = null;
|
|
@@ -488,7 +502,10 @@ export class BridgeClient extends EventEmitter {
|
|
|
488
502
|
method,
|
|
489
503
|
params,
|
|
490
504
|
tabId,
|
|
491
|
-
meta
|
|
505
|
+
meta:
|
|
506
|
+
this.agentSession && meta.agent_session === undefined
|
|
507
|
+
? { ...meta, agent_session: this.agentSession }
|
|
508
|
+
: meta,
|
|
492
509
|
});
|
|
493
510
|
|
|
494
511
|
if (method === 'page.extract_content') {
|
|
@@ -124,6 +124,8 @@ export interface BridgeClientOptions {
|
|
|
124
124
|
supportedVersions: readonly string[];
|
|
125
125
|
}) => Promise<NpmUpdateResult>;
|
|
126
126
|
authToken?: string | null;
|
|
127
|
+
/** Stable agent session id used for sticky working-tab routing. */
|
|
128
|
+
agentSession?: string | null;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
export interface ShortcutCommand {
|
|
@@ -326,10 +326,17 @@ export const INPUT_ACTION_METHODS = {
|
|
|
326
326
|
*
|
|
327
327
|
* @param {unknown} elementRef
|
|
328
328
|
* @param {unknown} selector
|
|
329
|
+
* @param {unknown} [locator]
|
|
329
330
|
* @returns {import('../../protocol/src/types.js').InputTarget | null}
|
|
330
331
|
*/
|
|
331
|
-
function createInputTarget(elementRef, selector) {
|
|
332
|
+
function createInputTarget(elementRef, selector, locator) {
|
|
332
333
|
if (typeof elementRef === 'string' && elementRef) return { elementRef };
|
|
334
|
+
if (locator && typeof locator === 'object' && Object.keys(locator).length) {
|
|
335
|
+
return {
|
|
336
|
+
...(typeof selector === 'string' && selector ? { selector } : {}),
|
|
337
|
+
.../** @type {Record<string, unknown>} */ (locator),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
333
340
|
if (typeof selector === 'string' && selector) return { selector };
|
|
334
341
|
return null;
|
|
335
342
|
}
|
|
@@ -337,18 +344,20 @@ function createInputTarget(elementRef, selector) {
|
|
|
337
344
|
/**
|
|
338
345
|
* Avoid changing legacy handler payloads when new options are omitted.
|
|
339
346
|
*
|
|
340
|
-
* @param {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }} args
|
|
341
|
-
* @returns {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean }}
|
|
347
|
+
* @param {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean, timeoutMs?: number, observe?: boolean | { settleMs?: number } }} args
|
|
348
|
+
* @returns {{ executionMode?: 'dom' | 'cdp', recoverStale?: boolean, timeoutMs?: number, observe?: boolean | { settleMs?: number } }}
|
|
342
349
|
*/
|
|
343
350
|
function createInputOptions(args) {
|
|
344
351
|
return {
|
|
345
352
|
...(args.executionMode ? { executionMode: args.executionMode } : {}),
|
|
346
353
|
...(typeof args.recoverStale === 'boolean' ? { recoverStale: args.recoverStale } : {}),
|
|
354
|
+
...(typeof args.timeoutMs === 'number' ? { timeoutMs: args.timeoutMs } : {}),
|
|
355
|
+
...(args.observe !== undefined ? { observe: args.observe } : {}),
|
|
347
356
|
};
|
|
348
357
|
}
|
|
349
358
|
|
|
350
359
|
/**
|
|
351
|
-
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, holdMs?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', executionMode?: 'dom' | 'cdp', recoverStale?: boolean, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
360
|
+
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, holdMs?: number, text?: string, value?: string, mode?: 'auto' | 'setter' | 'keystrokes', executionMode?: 'dom' | 'cdp', recoverStale?: boolean, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep', locator?: Record<string, unknown>, timeoutMs?: number, observe?: boolean | { settleMs?: number } }} args
|
|
352
361
|
* @returns {Promise<ToolResult>}
|
|
353
362
|
*/
|
|
354
363
|
export async function handleInputTool(args) {
|
|
@@ -374,9 +383,9 @@ export async function handleInputTool(args) {
|
|
|
374
383
|
async (client) => {
|
|
375
384
|
const requestedTabId = typeof args.tabId === 'number' ? args.tabId : null;
|
|
376
385
|
const elementTarget = () => {
|
|
377
|
-
const target = createInputTarget(args.elementRef, args.selector);
|
|
386
|
+
const target = createInputTarget(args.elementRef, args.selector, args.locator);
|
|
378
387
|
if (!target) {
|
|
379
|
-
throw new BridgeError('INVALID_REQUEST', 'Provide
|
|
388
|
+
throw new BridgeError('INVALID_REQUEST', 'Provide elementRef, selector, or locator.');
|
|
380
389
|
}
|
|
381
390
|
return target;
|
|
382
391
|
};
|
|
@@ -457,7 +466,8 @@ export async function handleInputTool(args) {
|
|
|
457
466
|
return summarizeToolResponse(response, 'input.fill');
|
|
458
467
|
}
|
|
459
468
|
case 'press_key': {
|
|
460
|
-
const target =
|
|
469
|
+
const target =
|
|
470
|
+
args.elementRef || args.selector || args.locator ? elementTarget() : undefined;
|
|
461
471
|
const response = await requestBridgeWithRetry(
|
|
462
472
|
client,
|
|
463
473
|
'input.press_key',
|
|
@@ -87,6 +87,7 @@ export const DOM_ACTIONS = {
|
|
|
87
87
|
exact: a.exact,
|
|
88
88
|
selector: a.selector,
|
|
89
89
|
maxResults: a.maxResults,
|
|
90
|
+
...(a.includeHidden !== undefined ? { includeHidden: a.includeHidden } : {}),
|
|
90
91
|
}),
|
|
91
92
|
},
|
|
92
93
|
find_role: {
|
|
@@ -95,8 +96,10 @@ export const DOM_ACTIONS = {
|
|
|
95
96
|
params: (a) => ({
|
|
96
97
|
role: a.role,
|
|
97
98
|
name: a.name,
|
|
99
|
+
...(a.exact !== undefined ? { exact: a.exact } : {}),
|
|
98
100
|
selector: a.selector,
|
|
99
101
|
maxResults: a.maxResults,
|
|
102
|
+
...(a.includeHidden !== undefined ? { includeHidden: a.includeHidden } : {}),
|
|
100
103
|
}),
|
|
101
104
|
},
|
|
102
105
|
html: {
|
|
@@ -117,12 +120,14 @@ export const DOM_ACTIONS = {
|
|
|
117
120
|
maxDepth: a.maxDepth,
|
|
118
121
|
compact: a.compact,
|
|
119
122
|
interactiveOnly: a.interactiveOnly,
|
|
123
|
+
...(a.source !== undefined ? { source: a.source } : {}),
|
|
124
|
+
...(a.format !== undefined ? { format: a.format } : {}),
|
|
120
125
|
}),
|
|
121
126
|
},
|
|
122
127
|
};
|
|
123
128
|
|
|
124
129
|
/**
|
|
125
|
-
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
130
|
+
* @param {{ action: string, baselineId?: string, maxChanges?: number, selector?: string, elementRef?: string, withinRef?: string, maxNodes?: number, maxDepth?: number, compact?: boolean, interactiveOnly?: boolean, source?: 'cdp' | 'dom', format?: 'tree' | 'outline', includeHidden?: boolean, textBudget?: number, includeBbox?: boolean, attributeAllowlist?: string[], attributes?: string[], text?: string, exact?: boolean, maxResults?: number, role?: string, name?: string, state?: string, timeoutMs?: number, outer?: boolean, maxLength?: number, tabId?: number, destinationId?: string, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
126
131
|
* @returns {Promise<ToolResult>}
|
|
127
132
|
*/
|
|
128
133
|
export async function handleDomTool(args) {
|
|
@@ -47,6 +47,8 @@ import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-clie
|
|
|
47
47
|
|
|
48
48
|
export const REQUEST_SOURCE = 'mcp';
|
|
49
49
|
const MCP_CLIENT_ID = `mcp_${randomUUID()}`;
|
|
50
|
+
// One MCP server process serves one agent conversation, so it owns one working tab.
|
|
51
|
+
const MCP_AGENT_SESSION = process.env.BBX_SESSION?.trim() || MCP_CLIENT_ID;
|
|
50
52
|
/** @type {AsyncLocalStorage<{ era: import('../../protocol/src/types.js').McpProtocolEra, signal?: AbortSignal }>} */
|
|
51
53
|
const MCP_REQUEST_ERA = new AsyncLocalStorage();
|
|
52
54
|
|
|
@@ -346,6 +348,7 @@ export async function withMcpRequestClient(callback, options = {}) {
|
|
|
346
348
|
const client = await createBridgeClientForDestination(options.destinationId, {
|
|
347
349
|
checkProtocolOnConnect: false,
|
|
348
350
|
clientId: MCP_CLIENT_ID,
|
|
351
|
+
agentSession: MCP_AGENT_SESSION,
|
|
349
352
|
});
|
|
350
353
|
let completed = false;
|
|
351
354
|
try {
|
|
@@ -284,7 +284,7 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
284
284
|
{
|
|
285
285
|
title: 'Browser DOM',
|
|
286
286
|
description:
|
|
287
|
-
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For
|
|
287
|
+
'Query, describe, read, search, or wait for DOM elements. Reuse elementRef from prior results. For a page overview to act on, use accessibility_tree with source "dom": a compact outline (`- role "name" [ref]`) whose refs work directly as input targets, with no debugger. find_text/find_role search shadow DOM, use real accessible names (labels, aria-*), and skip hidden matches. For full-page text, use browser_page action "text".',
|
|
288
288
|
inputSchema: z.object({
|
|
289
289
|
action: z
|
|
290
290
|
.enum([
|
|
@@ -350,6 +350,16 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
350
350
|
.describe(
|
|
351
351
|
'Return semantically interactive AX nodes; actionable state is reported separately'
|
|
352
352
|
),
|
|
353
|
+
source: z
|
|
354
|
+
.enum(['dom', 'cdp'])
|
|
355
|
+
.optional()
|
|
356
|
+
.describe(
|
|
357
|
+
'accessibility_tree source: dom = actionable outline with refs, no debugger (recommended); cdp = Chrome AX tree (default)'
|
|
358
|
+
),
|
|
359
|
+
format: z
|
|
360
|
+
.enum(['outline', 'tree'])
|
|
361
|
+
.optional()
|
|
362
|
+
.describe('accessibility_tree format (outline is default for source dom)'),
|
|
353
363
|
textBudget: z
|
|
354
364
|
.number()
|
|
355
365
|
.int()
|
|
@@ -381,6 +391,10 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
381
391
|
.describe('Maximum search results (default: 10)'),
|
|
382
392
|
role: z.string().optional().describe('ARIA role to search for (for find_role action)'),
|
|
383
393
|
name: z.string().optional().describe('Accessible name to match with role'),
|
|
394
|
+
includeHidden: z
|
|
395
|
+
.boolean()
|
|
396
|
+
.optional()
|
|
397
|
+
.describe('Include hidden matches in find_text/find_role (default: false; counted)'),
|
|
384
398
|
state: z
|
|
385
399
|
.enum(['attached', 'detached', 'visible', 'hidden'])
|
|
386
400
|
.optional()
|
|
@@ -658,7 +672,7 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
658
672
|
{
|
|
659
673
|
title: 'Browser Input',
|
|
660
674
|
description:
|
|
661
|
-
'Dispatch browser input.
|
|
675
|
+
'Dispatch browser input. Target by elementRef, selector, or a semantic locator ({role,name} / text / label / placeholder / testId) in one call. Targeted actions auto-wait for the element to become actionable (timeoutMs) and click/press_key/set_checked/select_option/submits return observed effects (URL change, dialogs, alerts, focus, DOM churn, target state), so a follow-up read is usually unnecessary. cdp_press_key and scroll_into_view use separate contracts.',
|
|
662
676
|
inputSchema: z.object({
|
|
663
677
|
action: z
|
|
664
678
|
.enum([
|
|
@@ -769,6 +783,39 @@ export function createBridgeMcpServer(options = {}) {
|
|
|
769
783
|
.describe('Drag destination selector (alternative to destinationElementRef)'),
|
|
770
784
|
offsetX: z.number().optional().describe('Drag drop offset X (default: 0)'),
|
|
771
785
|
offsetY: z.number().optional().describe('Drag drop offset Y (default: 0)'),
|
|
786
|
+
locator: z
|
|
787
|
+
.object({
|
|
788
|
+
role: z.string().max(500).optional().describe('ARIA role, e.g. button, textbox, link'),
|
|
789
|
+
name: z.string().max(500).optional().describe('Accessible name (labels, aria-*, text)'),
|
|
790
|
+
text: z.string().max(500).optional().describe('Visible text of the element'),
|
|
791
|
+
label: z.string().max(500).optional().describe('Form control label text'),
|
|
792
|
+
placeholder: z.string().max(500).optional(),
|
|
793
|
+
testId: z.string().max(500).optional().describe('data-testid / data-test / data-qa'),
|
|
794
|
+
exact: z.boolean().optional().describe('Exact instead of substring match'),
|
|
795
|
+
nth: z.number().int().min(0).max(49).optional().describe('Pick the nth visible match'),
|
|
796
|
+
})
|
|
797
|
+
.optional()
|
|
798
|
+
.describe(
|
|
799
|
+
'Find the target semantically in the same call (instead of elementRef/selector); selector narrows candidates'
|
|
800
|
+
),
|
|
801
|
+
timeoutMs: z
|
|
802
|
+
.number()
|
|
803
|
+
.int()
|
|
804
|
+
.min(0)
|
|
805
|
+
.max(15000)
|
|
806
|
+
.optional()
|
|
807
|
+
.describe(
|
|
808
|
+
'Wait for the target to appear and become actionable (default: 2500; 0 = no wait)'
|
|
809
|
+
),
|
|
810
|
+
observe: z
|
|
811
|
+
.union([
|
|
812
|
+
z.boolean(),
|
|
813
|
+
z.object({ settleMs: z.number().int().min(0).max(5000).optional() }),
|
|
814
|
+
])
|
|
815
|
+
.optional()
|
|
816
|
+
.describe(
|
|
817
|
+
'Report effects (URL, dialogs, alerts, focus, DOM churn, target state) after the action; default on for click, press_key, set_checked, select_option, and submits'
|
|
818
|
+
),
|
|
772
819
|
}),
|
|
773
820
|
},
|
|
774
821
|
handleInputTool
|
|
@@ -415,6 +415,11 @@ export function validateBridgeRequest(request) {
|
|
|
415
415
|
source: meta.source === 'cli' || meta.source === 'mcp' ? meta.source : undefined,
|
|
416
416
|
mcp_era: normalizeMcpEra(meta.source, meta.mcp_era),
|
|
417
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,
|
|
418
423
|
},
|
|
419
424
|
};
|
|
420
425
|
}
|
|
@@ -456,13 +461,14 @@ function normalizeAutomaticRetryMeta(source, value) {
|
|
|
456
461
|
|
|
457
462
|
/**
|
|
458
463
|
* Normalize request params early so malformed payloads fail at the shared
|
|
459
|
-
* 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).
|
|
460
466
|
*
|
|
461
467
|
* @param {BridgeMethod} method
|
|
462
468
|
* @param {Record<string, unknown>} params
|
|
463
469
|
* @returns {Record<string, unknown>}
|
|
464
470
|
*/
|
|
465
|
-
function normalizeRequestParams(method, params) {
|
|
471
|
+
export function normalizeRequestParams(method, params) {
|
|
466
472
|
switch (method) {
|
|
467
473
|
case 'access.request':
|
|
468
474
|
return normalizeAccessRequestParams(params);
|
|
@@ -526,7 +532,7 @@ function normalizeRequestParams(method, params) {
|
|
|
526
532
|
case 'input.type':
|
|
527
533
|
case 'input.fill':
|
|
528
534
|
case 'input.press_key':
|
|
529
|
-
return normalizeInputAction(params);
|
|
535
|
+
return withInputOptions(method, normalizeInputAction(params), params);
|
|
530
536
|
case 'cdp.dispatch_key_event':
|
|
531
537
|
return normalizeCdpDispatchKeyEventParams(params);
|
|
532
538
|
case 'cdp.get_box_model':
|
|
@@ -535,13 +541,13 @@ function normalizeRequestParams(method, params) {
|
|
|
535
541
|
case 'cdp.get_dom_snapshot':
|
|
536
542
|
return normalizeCdpDomSnapshotParams(params);
|
|
537
543
|
case 'input.set_checked':
|
|
538
|
-
return normalizeCheckedAction(params);
|
|
544
|
+
return withInputOptions(method, normalizeCheckedAction(params), params);
|
|
539
545
|
case 'input.select_option':
|
|
540
|
-
return normalizeSelectAction(params);
|
|
546
|
+
return withInputOptions(method, normalizeSelectAction(params), params);
|
|
541
547
|
case 'input.hover':
|
|
542
|
-
return normalizeHoverParams(params);
|
|
548
|
+
return withInputOptions(method, normalizeHoverParams(params), params);
|
|
543
549
|
case 'input.drag':
|
|
544
|
-
return normalizeDragParams(params);
|
|
550
|
+
return withInputOptions(method, normalizeDragParams(params), params);
|
|
545
551
|
case 'input.touch':
|
|
546
552
|
return normalizeTouchParams(params);
|
|
547
553
|
case 'input.perform':
|
|
@@ -625,9 +631,23 @@ export function getBridgeOperationTimeoutMs(method, params = {}) {
|
|
|
625
631
|
case 'input.perform':
|
|
626
632
|
return normalizeInputPerformParams(params).timeoutMs;
|
|
627
633
|
case 'input.click':
|
|
628
|
-
case 'input.press_key':
|
|
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
|
+
);
|
|
629
646
|
const holdMs = normalizeHoldMs(params.holdMs, 0);
|
|
630
|
-
|
|
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;
|
|
631
651
|
}
|
|
632
652
|
case 'input.touch':
|
|
633
653
|
return normalizeHoldMs(params.holdMs, DEFAULT_TOUCH_HOLD_MS) + INPUT_HOLD_TIMEOUT_MARGIN_MS;
|
|
@@ -754,15 +774,107 @@ export function normalizeStyleQuery(params = {}) {
|
|
|
754
774
|
};
|
|
755
775
|
}
|
|
756
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
|
+
|
|
757
787
|
/**
|
|
758
|
-
*
|
|
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
|
|
759
794
|
* @returns {import('./types.js').InputTarget}
|
|
760
795
|
*/
|
|
761
796
|
function normalizeTarget(target) {
|
|
762
|
-
|
|
797
|
+
/** @type {import('./types.js').InputTarget} */
|
|
798
|
+
const normalized = {
|
|
763
799
|
elementRef: typeof target?.elementRef === 'string' ? target.elementRef : undefined,
|
|
764
800
|
selector: typeof target?.selector === 'string' ? target.selector : undefined,
|
|
765
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
|
+
};
|
|
766
878
|
}
|
|
767
879
|
|
|
768
880
|
/**
|
|
@@ -770,10 +882,10 @@ function normalizeTarget(target) {
|
|
|
770
882
|
* @returns {import('./types.js').InputExecutionMode}
|
|
771
883
|
*/
|
|
772
884
|
function normalizeInputExecutionMode(value) {
|
|
773
|
-
if (value !== undefined && value !== 'dom' && value !== 'cdp') {
|
|
774
|
-
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.');
|
|
775
887
|
}
|
|
776
|
-
return value === 'cdp'
|
|
888
|
+
return value === 'cdp' || value === 'auto' ? value : 'dom';
|
|
777
889
|
}
|
|
778
890
|
|
|
779
891
|
/**
|
|
@@ -992,9 +1104,14 @@ export function normalizeInputPerformParams(params = {}) {
|
|
|
992
1104
|
);
|
|
993
1105
|
}
|
|
994
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
|
+
}
|
|
995
1112
|
return {
|
|
996
1113
|
method: stepMethod,
|
|
997
|
-
params:
|
|
1114
|
+
params: normalizedStepParams,
|
|
998
1115
|
delayMs: delayMs ?? 0,
|
|
999
1116
|
atMs,
|
|
1000
1117
|
};
|
|
@@ -1342,6 +1459,7 @@ export function normalizeFindByTextParams(params = {}) {
|
|
|
1342
1459
|
exact: Boolean(params.exact),
|
|
1343
1460
|
selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '*',
|
|
1344
1461
|
maxResults: clampInt(params.maxResults, 1, 50, 10),
|
|
1462
|
+
includeHidden: params.includeHidden === true,
|
|
1345
1463
|
};
|
|
1346
1464
|
}
|
|
1347
1465
|
|
|
@@ -1353,8 +1471,10 @@ export function normalizeFindByRoleParams(params = {}) {
|
|
|
1353
1471
|
return {
|
|
1354
1472
|
role: typeof params.role === 'string' ? params.role : '',
|
|
1355
1473
|
name: typeof params.name === 'string' ? params.name : '',
|
|
1474
|
+
exact: Boolean(params.exact),
|
|
1356
1475
|
selector: typeof params.selector === 'string' && params.selector.trim() ? params.selector : '*',
|
|
1357
1476
|
maxResults: clampInt(params.maxResults, 1, 50, 10),
|
|
1477
|
+
includeHidden: params.includeHidden === true,
|
|
1358
1478
|
};
|
|
1359
1479
|
}
|
|
1360
1480
|
|
|
@@ -1604,6 +1724,14 @@ export function normalizeTabCloseParams(params = {}) {
|
|
|
1604
1724
|
* @returns {NormalizedAccessibilityTreeParams}
|
|
1605
1725
|
*/
|
|
1606
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
|
+
}
|
|
1607
1735
|
return {
|
|
1608
1736
|
selector:
|
|
1609
1737
|
typeof params.selector === 'string' && params.selector.trim() ? params.selector.trim() : null,
|
|
@@ -1611,6 +1739,9 @@ export function normalizeAccessibilityTreeParams(params = {}) {
|
|
|
1611
1739
|
maxNodes: clampInt(params.maxNodes, 10, 5000, DEFAULT_A11Y_MAX_NODES),
|
|
1612
1740
|
compact: params.compact === true,
|
|
1613
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 } : {}),
|
|
1614
1745
|
};
|
|
1615
1746
|
}
|
|
1616
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':
|
|
@@ -522,7 +524,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
522
524
|
'dom.find_by_text',
|
|
523
525
|
'inspect',
|
|
524
526
|
true,
|
|
525
|
-
['text', 'exact', 'selector', 'maxResults'],
|
|
527
|
+
['text', 'exact', 'selector', 'maxResults', 'includeHidden'],
|
|
526
528
|
'low',
|
|
527
529
|
METHOD_POLICIES.domRead
|
|
528
530
|
),
|
|
@@ -530,7 +532,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
530
532
|
'dom.find_by_role',
|
|
531
533
|
'inspect',
|
|
532
534
|
true,
|
|
533
|
-
['role', 'name', 'selector', 'maxResults'],
|
|
535
|
+
['role', 'name', 'exact', 'selector', 'maxResults', 'includeHidden'],
|
|
534
536
|
'low',
|
|
535
537
|
METHOD_POLICIES.domRead
|
|
536
538
|
),
|
|
@@ -546,7 +548,7 @@ export const BRIDGE_METHOD_REGISTRY = Object.freeze({
|
|
|
546
548
|
'dom.get_accessibility_tree',
|
|
547
549
|
'inspect',
|
|
548
550
|
true,
|
|
549
|
-
['selector', 'maxNodes', 'maxDepth', 'compact', 'interactiveOnly'],
|
|
551
|
+
['selector', 'maxNodes', 'maxDepth', 'compact', 'interactiveOnly', 'source', 'format'],
|
|
550
552
|
'moderate',
|
|
551
553
|
METHOD_POLICIES.domReadDebugger
|
|
552
554
|
),
|