browser-debugger-cli 0.9.0 → 0.10.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/README.md +4 -1
- package/dist/commands/dom/a11y.js +2 -1
- package/dist/commands/dom/formInteraction.js +56 -25
- package/dist/commands/dom/helpers/keyAttributes.d.ts +20 -0
- package/dist/commands/dom/helpers/keyAttributes.js +54 -0
- package/dist/commands/dom/helpers/query.d.ts +1 -1
- package/dist/commands/dom/helpers/query.js +66 -19
- package/dist/commands/dom/helpers/runElementCommand.js +4 -3
- package/dist/commands/dom/helpers/screenshot.js +85 -12
- package/dist/commands/dom/index.d.ts +1 -0
- package/dist/commands/dom/index.js +8 -3
- package/dist/commands/dom/inspect.d.ts +15 -0
- package/dist/commands/dom/inspect.js +82 -0
- package/dist/commands/dom/layout.js +2 -2
- package/dist/commands/dom/listeners.js +2 -2
- package/dist/commands/dom/semanticUtils.d.ts +14 -1
- package/dist/commands/dom/semanticUtils.js +44 -3
- package/dist/commands/network/list.js +13 -2
- package/dist/commands/optionBehaviors.js +48 -6
- package/dist/commands/page.d.ts +1 -1
- package/dist/commands/page.js +62 -3
- package/dist/commands/shared/commonOptions.d.ts +4 -0
- package/dist/commands/shared/commonOptions.js +9 -0
- package/dist/commands/shared/optionTypes.d.ts +21 -0
- package/dist/commands/shared/startHelpers.d.ts +66 -0
- package/dist/commands/shared/startHelpers.js +91 -10
- package/dist/commands/shared/validation.d.ts +11 -0
- package/dist/commands/shared/validation.js +16 -0
- package/dist/daemon/launcher.d.ts +8 -1
- package/dist/daemon/launcher.js +3 -1
- package/dist/daemon/session/Session.d.ts +7 -0
- package/dist/daemon/session/Session.js +23 -1
- package/dist/daemon/session/commandRegistry.d.ts +14 -1
- package/dist/daemon/session/commandRegistry.js +65 -9
- package/dist/daemon/session/interactions.d.ts +18 -5
- package/dist/daemon/session/interactions.js +22 -12
- package/dist/daemon.js +3565 -329
- package/dist/errors/messages.d.ts +69 -0
- package/dist/errors/messages.js +102 -1
- package/dist/index.js +2416 -1320
- package/dist/ipc/client.d.ts +9 -0
- package/dist/ipc/client.js +13 -0
- package/dist/ipc/protocol/commands.d.ts +56 -1
- package/dist/ipc/protocol/commands.js +2 -0
- package/dist/ipc/protocol/domTypes.d.ts +35 -2
- package/dist/ipc/protocol/inspectTypes.d.ts +388 -0
- package/dist/ipc/protocol/inspectTypes.js +10 -0
- package/dist/runtime/dom/actionEffects.d.ts +94 -15
- package/dist/runtime/dom/actionEffects.js +173 -27
- package/dist/runtime/dom/actionEffectsScripts.d.ts +52 -14
- package/dist/runtime/dom/actionEffectsScripts.js +224 -32
- package/dist/runtime/dom/elementInfo.d.ts +26 -0
- package/dist/runtime/dom/elementInfo.js +65 -0
- package/dist/runtime/dom/eventListeners.js +14 -4
- package/dist/runtime/dom/formFillHelpers/fill.d.ts +3 -4
- package/dist/runtime/dom/formFillHelpers/fill.js +77 -28
- package/dist/runtime/dom/frameSelection.d.ts +11 -0
- package/dist/runtime/dom/frameSelection.js +20 -1
- package/dist/runtime/dom/frames.d.ts +38 -5
- package/dist/runtime/dom/frames.js +136 -21
- package/dist/runtime/dom/inspect.d.ts +28 -0
- package/dist/runtime/dom/inspect.js +557 -0
- package/dist/runtime/dom/inspectAllStyles.d.ts +62 -0
- package/dist/runtime/dom/inspectAllStyles.js +385 -0
- package/dist/runtime/dom/inspectCascade.d.ts +94 -0
- package/dist/runtime/dom/inspectCascade.js +371 -0
- package/dist/runtime/dom/inspectCascadeModel.d.ts +39 -0
- package/dist/runtime/dom/inspectCascadeModel.js +232 -0
- package/dist/runtime/dom/inspectHints.d.ts +62 -0
- package/dist/runtime/dom/inspectHints.js +305 -0
- package/dist/runtime/dom/inspectLayoutModel.d.ts +87 -0
- package/dist/runtime/dom/inspectLayoutModel.js +346 -0
- package/dist/runtime/dom/inspectModel.d.ts +74 -0
- package/dist/runtime/dom/inspectModel.js +184 -0
- package/dist/runtime/dom/inspectPaintModel.d.ts +157 -0
- package/dist/runtime/dom/inspectPaintModel.js +461 -0
- package/dist/runtime/dom/inspectRules.d.ts +37 -0
- package/dist/runtime/dom/inspectRules.js +101 -0
- package/dist/runtime/dom/inspectScripts.d.ts +132 -0
- package/dist/runtime/dom/inspectScripts.js +263 -0
- package/dist/runtime/dom/inspectTree.d.ts +40 -0
- package/dist/runtime/dom/inspectTree.js +134 -0
- package/dist/runtime/dom/inspectVariables.d.ts +33 -0
- package/dist/runtime/dom/inspectVariables.js +94 -0
- package/dist/runtime/dom/inspectWhyModel.d.ts +20 -0
- package/dist/runtime/dom/inspectWhyModel.js +134 -0
- package/dist/runtime/dom/layout.d.ts +5 -1
- package/dist/runtime/dom/layout.js +10 -3
- package/dist/runtime/dom/listenerPageScripts.d.ts +11 -5
- package/dist/runtime/dom/listenerPageScripts.js +95 -9
- package/dist/runtime/dom/listenerSummary.d.ts +4 -0
- package/dist/runtime/dom/listenerSummary.js +26 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +5 -0
- package/dist/runtime/dom/reactEventHelpers.js +12 -4
- package/dist/runtime/page/emulation.d.ts +20 -0
- package/dist/runtime/page/emulation.js +37 -0
- package/dist/telemetry/a11y.d.ts +10 -0
- package/dist/telemetry/a11y.js +78 -1
- package/dist/telemetry/console.d.ts +1 -0
- package/dist/telemetry/console.js +100 -5
- package/dist/telemetry/network.js +3 -1
- package/dist/types.d.ts +32 -0
- package/dist/ui/formatters/details.d.ts +8 -0
- package/dist/ui/formatters/details.js +59 -3
- package/dist/ui/formatters/dom.d.ts +2 -1
- package/dist/ui/formatters/dom.js +25 -9
- package/dist/ui/formatters/inspect.d.ts +39 -0
- package/dist/ui/formatters/inspect.js +596 -0
- package/dist/ui/formatters/keyAttributes.d.ts +19 -0
- package/dist/ui/formatters/keyAttributes.js +84 -0
- package/dist/ui/formatters/layout.js +2 -2
- package/dist/ui/formatters/networkHeaders.d.ts +13 -0
- package/dist/ui/formatters/networkHeaders.js +23 -3
- package/dist/ui/formatters/networkList.d.ts +29 -1
- package/dist/ui/formatters/networkList.js +86 -20
- package/dist/ui/formatters/status.js +1 -1
- package/dist/ui/formatting.d.ts +9 -0
- package/dist/ui/formatting.js +6 -3
- package/dist/ui/messages/commands.d.ts +123 -7
- package/dist/ui/messages/commands.js +181 -10
- package/dist/ui/messages/networkMessages.d.ts +14 -0
- package/dist/ui/messages/networkMessages.js +18 -0
- package/dist/ui/messages/session.d.ts +14 -0
- package/dist/ui/messages/session.js +20 -0
- package/dist/utils/async.d.ts +9 -0
- package/dist/utils/async.js +17 -0
- package/dist/utils/color.d.ts +84 -0
- package/dist/utils/color.js +376 -0
- package/dist/utils/cssValues.d.ts +109 -0
- package/dist/utils/cssValues.js +236 -0
- package/dist/utils/selectorFilters.d.ts +12 -0
- package/dist/utils/selectorFilters.js +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,6 +59,9 @@ bdg eval "document.title" # Run JavaScript in the page (--frame for ifr
|
|
|
59
59
|
bdg network list --preset errors # Network requests, console: bdg console
|
|
60
60
|
bdg dom listeners "#save" # Which event listeners run for an element
|
|
61
61
|
bdg dom layout "#save" # Where it is, whether it is visible or covered
|
|
62
|
+
bdg dom inspect "#save" # What it looks like (Figma-like styles), no screenshot
|
|
63
|
+
bdg dom inspect "#save" --why color # Which CSS rule sets a value, and what it beats
|
|
64
|
+
bdg page emulate --viewport 900x700 # Responsive check mid-session (or --color-scheme)
|
|
62
65
|
bdg dom wait "#result" --visible # Wait for an element instead of sleeping
|
|
63
66
|
bdg example.com --session agent2 --viewport 1280x800 # A second, independent session
|
|
64
67
|
bdg stop # End session
|
|
@@ -66,7 +69,7 @@ bdg stop # End session
|
|
|
66
69
|
|
|
67
70
|
## Current State
|
|
68
71
|
|
|
69
|
-
**Raw CDP access is complete.** Every protocol method works now. High-level commands cover the common work: page navigation, DOM queries and interaction (click, fill, hover, keys, forms, shadow DOM and iframes), accessibility tree, screenshots, network requests and HAR export, console messages and event listeners. Actions report what they changed (navigation, new messages, requests, or no visible effect), and several named sessions can run side by side. See the [CLI reference](docs/CLI_REFERENCE.md) for every command, and `bdg --help --json` for the machine-readable version.
|
|
72
|
+
**Raw CDP access is complete.** Every protocol method works now. High-level commands cover the common work: page navigation, DOM queries and interaction (click, fill, hover, keys, forms, shadow DOM and iframes), accessibility tree, screenshots, element styles with the CSS cascade (`dom inspect`: what an element looks like, which rule sets each value, and declarations that have no effect), network requests and HAR export, console messages and event listeners. Actions report what they changed (navigation, new messages, requests, or no visible effect), and several named sessions can run side by side. See the [CLI reference](docs/CLI_REFERENCE.md) for every command, and `bdg --help --json` for the machine-readable version.
|
|
70
73
|
|
|
71
74
|
## Agent Discovery Pattern
|
|
72
75
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { DomElementResolver } from './DomElementResolver.js';
|
|
12
12
|
import { getDomContext, resolveBackendNodeIds } from './helpers/index.js';
|
|
13
|
+
import { withSecretMasked } from './semanticUtils.js';
|
|
13
14
|
import { runCommand, runJsonCommand } from '../shared/CommandRunner.js';
|
|
14
15
|
import { jsonOption } from '../shared/commonOptions.js';
|
|
15
16
|
import { integerOption } from '../shared/validation.js';
|
|
@@ -164,7 +165,7 @@ async function handleA11yDescribe(selectorOrIndex, options) {
|
|
|
164
165
|
if (domNodeId) {
|
|
165
166
|
domContext = await getDomContext({ backendNodeId: domNodeId });
|
|
166
167
|
}
|
|
167
|
-
return { node, domContext };
|
|
168
|
+
return { node: withSecretMasked(node, domContext), domContext };
|
|
168
169
|
}
|
|
169
170
|
if (options.json) {
|
|
170
171
|
await runJsonCommand(fetchA11yNodeData);
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { InvalidArgumentError } from 'commander';
|
|
10
10
|
import { runElementCommand } from './helpers/runElementCommand.js';
|
|
11
11
|
import { runCommand } from '../shared/CommandRunner.js';
|
|
12
|
-
import { jsonOption } from '../shared/commonOptions.js';
|
|
12
|
+
import { jsonOption, SELECTOR_OR_INDEX_ARGUMENT } from '../shared/commonOptions.js';
|
|
13
13
|
import { integerOption } from '../shared/validation.js';
|
|
14
14
|
import { CommandError } from '../../errors/index.js';
|
|
15
15
|
import { VIA_LABEL_SUFFIX, conflictingOptionsMessage, indexSourceText, internalError, scrollOptionsError, } from '../../errors/messages.js';
|
|
@@ -17,9 +17,11 @@ import { domClick, domFill, domPressKey, domScroll, domSubmit } from '../../ipc/
|
|
|
17
17
|
import { findUnknownModifiers } from '../../runtime/dom/keyMapping.js';
|
|
18
18
|
import { formatTriggeredRequestLines, formatTriggeredRequestsTitle, } from '../../ui/formatters/triggeredRequests.js';
|
|
19
19
|
import { OutputFormatter } from '../../ui/formatting.js';
|
|
20
|
-
import { CLICK_RESULT_WAIT_HELP, POINTER_ACTION_DONE, actionStatusLine, dialogConsoleText, newMessageText, pageNavigationText, } from '../../ui/messages/commands.js';
|
|
20
|
+
import { CLICK_RESULT_WAIT_HELP, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, actionStatusLine, dialogConsoleText, newMessageText, pageNavigationText, shownElementText, stillChangingNote, } from '../../ui/messages/commands.js';
|
|
21
21
|
import { sessionCommand } from '../../ui/messages/sessionCommand.js';
|
|
22
22
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
23
|
+
/** Help of `--strict` on click and hover */
|
|
24
|
+
const STRICT_OPTION_HELP = 'Fail (exit 90) instead of using DOM events when a real mouse cannot reach the element (covered, hidden, zero-size)';
|
|
23
25
|
/**
|
|
24
26
|
* Commander parser for `--modifiers`: rejects unknown names instead of
|
|
25
27
|
* silently pressing the bare key.
|
|
@@ -54,7 +56,7 @@ export function registerFormInteractionCommands(program) {
|
|
|
54
56
|
domCommand
|
|
55
57
|
.command('fill')
|
|
56
58
|
.description('Fill a form field with a value (React-compatible, waits for stability)')
|
|
57
|
-
.argument('<selectorOrIndex>',
|
|
59
|
+
.argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
|
|
58
60
|
.argument('<value>', 'Value to fill (file inputs: paths separated by commas, "" clears)')
|
|
59
61
|
.option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
|
|
60
62
|
.option('--no-blur', 'Do not blur after filling (keeps focus on element)')
|
|
@@ -80,10 +82,11 @@ export function registerFormInteractionCommands(program) {
|
|
|
80
82
|
domCommand
|
|
81
83
|
.command('click')
|
|
82
84
|
.description('Click an element and wait for stability (accepts selector or index)')
|
|
83
|
-
.argument('<selectorOrIndex>',
|
|
85
|
+
.argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
|
|
84
86
|
.option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
|
|
85
87
|
.option('--double', 'Double-click')
|
|
86
88
|
.option('--right', 'Right-click (opens the context menu)')
|
|
89
|
+
.option('--strict', STRICT_OPTION_HELP)
|
|
87
90
|
.option('--no-wait', 'Skip waiting for network stability after click')
|
|
88
91
|
.addOption(jsonOption())
|
|
89
92
|
.addHelpText('after', CLICK_RESULT_WAIT_HELP)
|
|
@@ -94,8 +97,9 @@ export function registerFormInteractionCommands(program) {
|
|
|
94
97
|
domCommand
|
|
95
98
|
.command('hover')
|
|
96
99
|
.description('Move the mouse over an element (shows hover menus and tooltips)')
|
|
97
|
-
.argument('<selectorOrIndex>',
|
|
100
|
+
.argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
|
|
98
101
|
.option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
|
|
102
|
+
.option('--strict', STRICT_OPTION_HELP)
|
|
99
103
|
.option('--no-wait', 'Skip waiting for network stability after hovering')
|
|
100
104
|
.addOption(jsonOption())
|
|
101
105
|
.action(async (selectorOrIndex, options) => {
|
|
@@ -104,7 +108,7 @@ export function registerFormInteractionCommands(program) {
|
|
|
104
108
|
domCommand
|
|
105
109
|
.command('submit')
|
|
106
110
|
.description('Submit a form by clicking submit button and waiting for completion')
|
|
107
|
-
.argument('<selectorOrIndex>',
|
|
111
|
+
.argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
|
|
108
112
|
.option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
|
|
109
113
|
.option('--wait-navigation', 'Wait for page navigation after submit')
|
|
110
114
|
.option('--wait-network <ms>', 'Wait for network idle after submit (milliseconds)', integerOption(0), 1000)
|
|
@@ -132,7 +136,7 @@ export function registerFormInteractionCommands(program) {
|
|
|
132
136
|
domCommand
|
|
133
137
|
.command('pressKey')
|
|
134
138
|
.description('Press a key on an element (for Enter-to-submit, keyboard navigation)')
|
|
135
|
-
.argument('<selectorOrIndex>',
|
|
139
|
+
.argument('<selectorOrIndex>', SELECTOR_OR_INDEX_ARGUMENT)
|
|
136
140
|
.argument('<key>', 'Key to press (Enter, Tab, Escape, Space, ArrowUp, etc.)')
|
|
137
141
|
.option('--index <n>', 'Element index if selector matches multiple (0-based)', integerOption(0))
|
|
138
142
|
.option('--times <n>', 'Press key multiple times (default: 1)', integerOption(1, 1000))
|
|
@@ -270,6 +274,7 @@ async function runPointerCommand(selectorOrIndex, options, action) {
|
|
|
270
274
|
...target,
|
|
271
275
|
wait: options.wait !== false,
|
|
272
276
|
...(action !== 'click' && { action }),
|
|
277
|
+
...(options.strict && { strict: true }),
|
|
273
278
|
}),
|
|
274
279
|
call: domClick,
|
|
275
280
|
command: action === 'hover' ? 'hover' : 'click',
|
|
@@ -279,34 +284,40 @@ async function runPointerCommand(selectorOrIndex, options, action) {
|
|
|
279
284
|
}
|
|
280
285
|
/**
|
|
281
286
|
* Build an action's output: the status line ("✓ Element Clicked",
|
|
282
|
-
* "⚠ Element Clicked (with warnings)" with the warning right below it,
|
|
283
|
-
* "⚠ Element Clicked (
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
+
* "⚠ Element Clicked (with warnings)" with the warning right below it,
|
|
288
|
+
* "⚠ Element Clicked (page still changing)" with what it was still working
|
|
289
|
+
* on, or "⚠ Element Clicked (no visible effect: …)"), the details, what
|
|
290
|
+
* changed on the page (`Page:` navigation, `New text:` messages, `Shown:`
|
|
291
|
+
* elements), then the network requests it triggered and the dialogs it
|
|
292
|
+
* caused. No request list is shown when there were none (JSON has an empty
|
|
293
|
+
* `triggeredRequests` then).
|
|
287
294
|
*
|
|
288
295
|
* @param done - What was done, e.g. "Element Clicked"
|
|
289
296
|
* @param details - Label/value rows
|
|
290
297
|
* @param result - Action result
|
|
291
|
-
* @param
|
|
298
|
+
* @param options - Width of the labels; what the action is called in notes (e.g. "click")
|
|
292
299
|
* @returns Output being built (more can be appended)
|
|
293
300
|
*/
|
|
294
|
-
function formatActionOutput(done, details, result,
|
|
301
|
+
function formatActionOutput(done, details, result, options = {}) {
|
|
302
|
+
const keyWidth = options.keyWidth ?? 15;
|
|
295
303
|
const fmt = new OutputFormatter();
|
|
296
|
-
|
|
304
|
+
const stillChanging = result.settled === false && result.pending !== undefined;
|
|
305
|
+
fmt.text(actionStatusLine(done, {
|
|
306
|
+
warned: result.warning !== undefined,
|
|
307
|
+
noEffect: result.effect === 'none',
|
|
308
|
+
stillChanging,
|
|
309
|
+
}));
|
|
297
310
|
if (result.warning)
|
|
298
311
|
fmt.text(`⚠ Warning: ${result.warning}`);
|
|
312
|
+
if (stillChanging && result.pending) {
|
|
313
|
+
fmt.text(`⚠ ${stillChangingNote(options.action ?? 'action', result.pending)}`);
|
|
314
|
+
}
|
|
299
315
|
fmt.blank();
|
|
300
316
|
fmt.keyValueList(details, keyWidth);
|
|
301
317
|
if (result.navigation)
|
|
302
318
|
fmt.keyValue('Page', pageNavigationText(result.navigation), keyWidth);
|
|
303
|
-
(result.messages ?? []).
|
|
304
|
-
|
|
305
|
-
if (index === 0)
|
|
306
|
-
fmt.keyValue('New text', text, keyWidth);
|
|
307
|
-
else
|
|
308
|
-
fmt.text(' '.repeat(keyWidth) + text);
|
|
309
|
-
});
|
|
319
|
+
listRows(fmt, 'New text', (result.messages ?? []).map(newMessageText), keyWidth);
|
|
320
|
+
listRows(fmt, 'Shown', (result.shown ?? []).map(shownElementText), keyWidth);
|
|
310
321
|
const omitted = result.triggeredRequestsOmitted;
|
|
311
322
|
const requests = formatTriggeredRequestLines(result.triggeredRequests ?? [], omitted);
|
|
312
323
|
if (requests.length > 0) {
|
|
@@ -320,6 +331,23 @@ function formatActionOutput(done, details, result, keyWidth = 15) {
|
|
|
320
331
|
}
|
|
321
332
|
return fmt;
|
|
322
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Rows of a list under one label: the label on the first row, the rest
|
|
336
|
+
* indented below it.
|
|
337
|
+
*
|
|
338
|
+
* @param fmt - Output being built
|
|
339
|
+
* @param label - Label, e.g. "New text"
|
|
340
|
+
* @param texts - One text per row
|
|
341
|
+
* @param keyWidth - Width of the labels
|
|
342
|
+
*/
|
|
343
|
+
function listRows(fmt, label, texts, keyWidth) {
|
|
344
|
+
texts.forEach((text, index) => {
|
|
345
|
+
if (index === 0)
|
|
346
|
+
fmt.keyValue(label, text, keyWidth);
|
|
347
|
+
else
|
|
348
|
+
fmt.text(' '.repeat(keyWidth) + text);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
323
351
|
/**
|
|
324
352
|
* Row naming the element an action hit, e.g.
|
|
325
353
|
* `Element: input.toggle in div.view "Write report"` (just its tag when the
|
|
@@ -372,7 +400,7 @@ function formatClickOutput(result) {
|
|
|
372
400
|
...selectorRows(result),
|
|
373
401
|
elementRow(result),
|
|
374
402
|
['Method', result.method === 'dom' ? 'DOM events' : 'mouse events'],
|
|
375
|
-
], result).build();
|
|
403
|
+
], result, { action: POINTER_ACTION_NOUN[result.action ?? 'click'] }).build();
|
|
376
404
|
}
|
|
377
405
|
/**
|
|
378
406
|
* Format submit command output for human-readable display.
|
|
@@ -391,7 +419,10 @@ function formatSubmitOutput(result) {
|
|
|
391
419
|
details.push(['Navigation', 'yes']);
|
|
392
420
|
if (result.waitTimeMs !== undefined)
|
|
393
421
|
details.push(['Wait Time', `${result.waitTimeMs}ms`]);
|
|
394
|
-
const fmt = formatActionOutput('Form Submitted', details, result,
|
|
422
|
+
const fmt = formatActionOutput('Form Submitted', details, result, {
|
|
423
|
+
keyWidth: 20,
|
|
424
|
+
action: 'submit',
|
|
425
|
+
});
|
|
395
426
|
fmt.hints('Next steps:', [
|
|
396
427
|
`${sessionCommand('bdg network list --last 10').padEnd(32)} Check network requests`,
|
|
397
428
|
`${sessionCommand('bdg console --last 5').padEnd(32)} Check console messages`,
|
|
@@ -412,7 +443,7 @@ function formatPressKeyOutput(result) {
|
|
|
412
443
|
details.push(['Times', result.times.toString()]);
|
|
413
444
|
if (result.modifiers?.length)
|
|
414
445
|
details.push(['Modifiers', result.modifiers.join('+')]);
|
|
415
|
-
return formatActionOutput('Key Pressed', details, result).build();
|
|
446
|
+
return formatActionOutput('Key Pressed', details, result, { action: 'key press' }).build();
|
|
416
447
|
}
|
|
417
448
|
/**
|
|
418
449
|
* Format scroll command output for human-readable display.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attributes that identify an element by its type (an image's `src`, a
|
|
3
|
+
* link's `href`, a field's name and value), shown by `dom query`, `dom get`
|
|
4
|
+
* and `dom a11y describe`.
|
|
5
|
+
*/
|
|
6
|
+
import type { ElementState, KeyAttributes } from '../../../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* The key attributes of an element: the identifying attributes of its type
|
|
9
|
+
* that are set (empty ones left out) and the live state of a form control
|
|
10
|
+
* (type, current value, checked, selected options). Values arrive masked
|
|
11
|
+
* from the page (`ELEMENT_STATE_JS`); a hidden input's value is never read,
|
|
12
|
+
* and a sensitive field's value is masked here again in case it was not.
|
|
13
|
+
*
|
|
14
|
+
* @param tag - Lower-case tag name
|
|
15
|
+
* @param attributes - Element attributes
|
|
16
|
+
* @param state - Live state read in the page (`ELEMENT_STATE_JS`)
|
|
17
|
+
* @returns Key attributes, or undefined for an element type without any
|
|
18
|
+
*/
|
|
19
|
+
export declare function keyAttributes(tag: string, attributes: Record<string, string>, state?: ElementState): KeyAttributes | undefined;
|
|
20
|
+
//# sourceMappingURL=keyAttributes.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The attributes that identify an element by its type (an image's `src`, a
|
|
3
|
+
* link's `href`, a field's name and value), shown by `dom query`, `dom get`
|
|
4
|
+
* and `dom a11y describe`.
|
|
5
|
+
*/
|
|
6
|
+
import { MASKED_VALUE } from '../../../runtime/dom/elementInfo.js';
|
|
7
|
+
/** Attributes read for each element type (live state is added for form controls) */
|
|
8
|
+
const ATTRIBUTES_BY_TAG = {
|
|
9
|
+
img: ['src', 'alt'],
|
|
10
|
+
a: ['href'],
|
|
11
|
+
input: ['name', 'placeholder'],
|
|
12
|
+
textarea: ['name', 'placeholder'],
|
|
13
|
+
button: ['name'],
|
|
14
|
+
select: ['name'],
|
|
15
|
+
iframe: ['src'],
|
|
16
|
+
form: ['action', 'method'],
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The key attributes of an element: the identifying attributes of its type
|
|
20
|
+
* that are set (empty ones left out) and the live state of a form control
|
|
21
|
+
* (type, current value, checked, selected options). Values arrive masked
|
|
22
|
+
* from the page (`ELEMENT_STATE_JS`); a hidden input's value is never read,
|
|
23
|
+
* and a sensitive field's value is masked here again in case it was not.
|
|
24
|
+
*
|
|
25
|
+
* @param tag - Lower-case tag name
|
|
26
|
+
* @param attributes - Element attributes
|
|
27
|
+
* @param state - Live state read in the page (`ELEMENT_STATE_JS`)
|
|
28
|
+
* @returns Key attributes, or undefined for an element type without any
|
|
29
|
+
*/
|
|
30
|
+
export function keyAttributes(tag, attributes, state = {}) {
|
|
31
|
+
const names = ATTRIBUTES_BY_TAG[tag];
|
|
32
|
+
if (!names)
|
|
33
|
+
return undefined;
|
|
34
|
+
const result = {};
|
|
35
|
+
const type = state.type ?? attributes['type'];
|
|
36
|
+
if (type)
|
|
37
|
+
result['type'] = type;
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const value = attributes[name];
|
|
40
|
+
if (value)
|
|
41
|
+
result[name] = value;
|
|
42
|
+
}
|
|
43
|
+
if (type === 'hidden')
|
|
44
|
+
return result;
|
|
45
|
+
const secret = state.sensitive === true || type === 'password';
|
|
46
|
+
if (state.value)
|
|
47
|
+
result['value'] = secret ? MASKED_VALUE : state.value;
|
|
48
|
+
if (state.checked !== undefined)
|
|
49
|
+
result['checked'] = state.checked;
|
|
50
|
+
if (state.selected)
|
|
51
|
+
result['selected'] = secret ? MASKED_VALUE : state.selected;
|
|
52
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=keyAttributes.js.map
|
|
@@ -46,7 +46,7 @@ export declare function documentReadyState(): Promise<string | undefined>;
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function queryDOMElements(selector: string): Promise<DomQueryResult>;
|
|
48
48
|
/**
|
|
49
|
-
* Get DOM context (tag, classes, text preview) for a node: a one-line
|
|
49
|
+
* Get DOM context (tag, classes, key attributes, text preview) for a node: a one-line
|
|
50
50
|
* preview, and up to {@link ELEMENT_TEXT_LENGTH} characters of text when it
|
|
51
51
|
* is longer (all of it with `full`).
|
|
52
52
|
*
|
|
@@ -9,18 +9,19 @@
|
|
|
9
9
|
* bdg invocations. Selectors are matched in the page, including open shadow
|
|
10
10
|
* roots and same-origin iframes, like a user sees the page.
|
|
11
11
|
*/
|
|
12
|
+
import { keyAttributes } from './keyAttributes.js';
|
|
12
13
|
import { CommandError } from '../../../errors/index.js';
|
|
13
14
|
import { noNodesFoundError, indexOutOfRangeError, eitherArgumentRequiredError, invalidSelectorError, nodeIdNotFoundError, operationFailedError, similarSelectorsLine, staleNodeError, } from '../../../errors/messages.js';
|
|
14
15
|
import { callCDP } from '../../../ipc/client.js';
|
|
15
16
|
import { ELEMENT_GEOMETRY_JS, VIEWPORT_SIZE_JS, classifyViewportPosition, } from '../../../runtime/dom/elementGeometry.js';
|
|
16
|
-
import { ELEMENT_CONTEXT_JS, ELEMENT_TEXT_JS, ELEMENT_TEXT_LENGTH, textPreview, } from '../../../runtime/dom/elementInfo.js';
|
|
17
|
+
import { ELEMENT_CONTEXT_JS, ELEMENT_STATE_JS, ELEMENT_TEXT_JS, ELEMENT_TEXT_LENGTH, textPreview, } from '../../../runtime/dom/elementInfo.js';
|
|
17
18
|
import { DEEP_QUERY_JS, UNSEARCHED_CONTENT_JS, pageNamesJS, selectorArgsJS, } from '../../../runtime/dom/targetNode.js';
|
|
18
19
|
import { createLogger } from '../../../ui/logging/index.js';
|
|
19
20
|
import { sessionCommand } from '../../../ui/messages/sessionCommand.js';
|
|
20
21
|
import { ConcurrencyLimiter } from '../../../utils/concurrency.js';
|
|
21
22
|
import { getErrorMessage } from '../../../utils/errors.js';
|
|
22
23
|
import { EXIT_CODES } from '../../../utils/exitCodes.js';
|
|
23
|
-
import { parseSelectorFilters, withoutVisibleFilters } from '../../../utils/selectorFilters.js';
|
|
24
|
+
import { leadingCompounds, parseSelectorFilters, withoutVisibleFilters, } from '../../../utils/selectorFilters.js';
|
|
24
25
|
import { findSimilarNames, parseSingleNameSelector } from '../../../utils/suggestions.js';
|
|
25
26
|
const log = createLogger('dom');
|
|
26
27
|
/** Maximum concurrent CDP calls to avoid overwhelming the connection. */
|
|
@@ -113,6 +114,34 @@ async function withSelection(selector, use) {
|
|
|
113
114
|
export async function noMatchesError(selector) {
|
|
114
115
|
return noNodesFoundError(selector, await noMatchContext(selector));
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Page-side index of the first leading compound that matches a shadow host
|
|
119
|
+
* (an element with an open shadow root, anywhere a selector reaches) while
|
|
120
|
+
* the selector after it finds something: then the selector only failed by
|
|
121
|
+
* crossing into the shadow root.
|
|
122
|
+
*
|
|
123
|
+
* @param compounds - Leading compounds of the selector
|
|
124
|
+
* @returns Expression evaluating to the index, or -1
|
|
125
|
+
*/
|
|
126
|
+
function shadowHostJS(compounds) {
|
|
127
|
+
const plain = compounds
|
|
128
|
+
.slice(0, MAX_HOST_COMPOUNDS)
|
|
129
|
+
.map((entry) => [withoutFilters(entry.compound), withoutFilters(entry.rest)]);
|
|
130
|
+
return `${JSON.stringify(plain)}.findIndex(([host, rest]) => { try { const deep = ${DEEP_QUERY_JS}; return deep(host, null).some((el) => el.shadowRoot) && deep(rest, null).length > 0; } catch (e) { return false; } })`;
|
|
131
|
+
}
|
|
132
|
+
/** Leading compounds checked for a shadow host (each check walks the page) */
|
|
133
|
+
const MAX_HOST_COMPOUNDS = 4;
|
|
134
|
+
/**
|
|
135
|
+
* A selector without bdg's filters (`:visible`, `:has-text()`, `:text-is()`),
|
|
136
|
+
* which plain CSS does not know.
|
|
137
|
+
*
|
|
138
|
+
* @param selector - Selector
|
|
139
|
+
* @returns Plain CSS
|
|
140
|
+
*/
|
|
141
|
+
function withoutFilters(selector) {
|
|
142
|
+
return (selector.replace(/:visible\b|:(has-text|text-is)\((?:"[^"]*"|'[^']*'|[^)"'])*\)/g, '').trim() ||
|
|
143
|
+
'*');
|
|
144
|
+
}
|
|
116
145
|
/**
|
|
117
146
|
* What the page says about a selector that matched nothing, in one
|
|
118
147
|
* evaluation: whether it is still loading, how many elements match with the
|
|
@@ -131,9 +160,11 @@ export async function noMatchContext(selector) {
|
|
|
131
160
|
? `(() => { try { return (${DEEP_QUERY_JS})(${JSON.stringify(selector)}, ${JSON.stringify(unfiltered)}).length; } catch (e) { return 0; } })()`
|
|
132
161
|
: '0';
|
|
133
162
|
const names = single ? pageNamesJS(single.kind) : '[]';
|
|
163
|
+
const compounds = leadingCompounds(selector);
|
|
164
|
+
const shadowHost = compounds.length > 0 ? shadowHostJS(compounds) : '-1';
|
|
134
165
|
try {
|
|
135
166
|
const evaluated = await callCDP('Runtime.evaluate', {
|
|
136
|
-
expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names} })`,
|
|
167
|
+
expression: `({ hidden: ${hidden}, readyState: document.readyState, unsearched: ${UNSEARCHED_CONTENT_JS}, names: ${names}, shadowHost: ${shadowHost} })`,
|
|
137
168
|
returnByValue: true,
|
|
138
169
|
});
|
|
139
170
|
const { result } = (evaluated.data?.result ?? {});
|
|
@@ -151,6 +182,8 @@ export async function noMatchContext(selector) {
|
|
|
151
182
|
},
|
|
152
183
|
}),
|
|
153
184
|
...(similar && { similar }),
|
|
185
|
+
...(typeof value.shadowHost === 'number' &&
|
|
186
|
+
compounds[value.shadowHost] && { shadowHost: compounds[value.shadowHost] }),
|
|
154
187
|
};
|
|
155
188
|
}
|
|
156
189
|
catch (error) {
|
|
@@ -182,7 +215,8 @@ export async function documentReadyState() {
|
|
|
182
215
|
const VIEWPORT_HINT_LIMIT = 100;
|
|
183
216
|
/**
|
|
184
217
|
* Where each element of a page-side array lives (an iframe and/or a shadow
|
|
185
|
-
* root), its text
|
|
218
|
+
* root), its text, its form control state ({@link ELEMENT_STATE_JS}) and, for
|
|
219
|
+
* the first {@link VIEWPORT_HINT_LIMIT}, its position
|
|
186
220
|
* relative to the viewport, plus the viewport size. An element that cannot be
|
|
187
221
|
* read gets empty details instead of failing the whole query.
|
|
188
222
|
*/
|
|
@@ -190,9 +224,10 @@ const ELEMENT_DETAILS_FUNCTION = `function () {
|
|
|
190
224
|
const contextOf = ${ELEMENT_CONTEXT_JS};
|
|
191
225
|
const textOf = ${ELEMENT_TEXT_JS};
|
|
192
226
|
const geometryOf = ${ELEMENT_GEOMETRY_JS};
|
|
227
|
+
const stateOf = ${ELEMENT_STATE_JS};
|
|
193
228
|
const read = (el, index) => {
|
|
194
229
|
try {
|
|
195
|
-
return { context: contextOf(el), text: textOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
|
|
230
|
+
return { context: contextOf(el), text: textOf(el), state: stateOf(el), geometry: index < ${VIEWPORT_HINT_LIMIT} ? geometryOf(el) : null };
|
|
196
231
|
} catch (e) {
|
|
197
232
|
return {};
|
|
198
233
|
}
|
|
@@ -219,6 +254,7 @@ async function elementsWithDetails(arrayObjectId) {
|
|
|
219
254
|
backendNodeId,
|
|
220
255
|
context: details?.context ?? '',
|
|
221
256
|
text: details?.text ?? '',
|
|
257
|
+
state: details?.state ?? {},
|
|
222
258
|
...viewportHint(details?.geometry, viewport),
|
|
223
259
|
},
|
|
224
260
|
];
|
|
@@ -334,18 +370,21 @@ export async function queryDOMElements(selector) {
|
|
|
334
370
|
log.debug(`Querying ${elements.length} elements with selector: ${selector}`);
|
|
335
371
|
}
|
|
336
372
|
const nodes = await mapConcurrently(elements, async (element, index) => {
|
|
337
|
-
const { backendNodeId, context, text, inViewport, clippedBy } = element;
|
|
373
|
+
const { backendNodeId, context, text, state, inViewport, clippedBy } = element;
|
|
338
374
|
const desc = await describeNode({ backendNodeId });
|
|
339
375
|
if (!desc)
|
|
340
376
|
return { index, nodeId: 0 };
|
|
341
377
|
const attributes = unpackAttributes(desc.attributes);
|
|
342
378
|
const classes = attributes['class']?.split(/\s+/).filter(Boolean);
|
|
343
379
|
const preview = textPreview(text);
|
|
380
|
+
const tag = desc.nodeName.toLowerCase();
|
|
381
|
+
const keys = keyAttributes(tag, attributes, state);
|
|
344
382
|
return {
|
|
345
383
|
index,
|
|
346
384
|
nodeId: desc.backendNodeId,
|
|
347
|
-
tag
|
|
385
|
+
tag,
|
|
348
386
|
...identifyingAttributes(attributes, desc.nodeName),
|
|
387
|
+
...(keys && { attributes: keys }),
|
|
349
388
|
...(classes && { classes }),
|
|
350
389
|
...(preview && { preview }),
|
|
351
390
|
...(context && { context }),
|
|
@@ -372,36 +411,39 @@ function identifyingAttributes(attributes, nodeName) {
|
|
|
372
411
|
};
|
|
373
412
|
}
|
|
374
413
|
/**
|
|
375
|
-
* The text of one element as the page renders it ({@link ELEMENT_TEXT_JS})
|
|
414
|
+
* The text of one element as the page renders it ({@link ELEMENT_TEXT_JS})
|
|
415
|
+
* and its form control state ({@link ELEMENT_STATE_JS}).
|
|
376
416
|
*
|
|
377
417
|
* @param ref - Node reference
|
|
378
418
|
* @param full - Read all of a large container's text, not just its start
|
|
379
|
-
* @returns Element text,
|
|
419
|
+
* @returns Element text and state, empty when the node cannot be read
|
|
380
420
|
*/
|
|
381
|
-
async function
|
|
421
|
+
async function elementTextAndState(ref, full) {
|
|
382
422
|
const objectGroup = `bdg-text-${process.pid}-${++queryCount}`;
|
|
383
423
|
const resolved = await callCDP('DOM.resolveNode', { ...ref, objectGroup });
|
|
384
424
|
const objectId = resolved.data?.result?.object
|
|
385
425
|
.objectId;
|
|
386
426
|
if (!objectId)
|
|
387
|
-
return '';
|
|
427
|
+
return { text: '', state: {} };
|
|
388
428
|
try {
|
|
389
429
|
const response = await callCDP('Runtime.callFunctionOn', {
|
|
390
430
|
objectId,
|
|
391
|
-
functionDeclaration: `function (full) { return (${ELEMENT_TEXT_JS})(this, full); }`,
|
|
431
|
+
functionDeclaration: `function (full) { return { text: (${ELEMENT_TEXT_JS})(this, full), state: (${ELEMENT_STATE_JS})(this) }; }`,
|
|
392
432
|
arguments: [{ value: full }],
|
|
393
433
|
returnByValue: true,
|
|
394
434
|
});
|
|
395
|
-
const value = response.data?.result?.result
|
|
396
|
-
|
|
397
|
-
|
|
435
|
+
const value = response.data?.result?.result?.value;
|
|
436
|
+
return {
|
|
437
|
+
text: typeof value?.text === 'string' ? value.text : '',
|
|
438
|
+
state: value?.state ?? {},
|
|
439
|
+
};
|
|
398
440
|
}
|
|
399
441
|
finally {
|
|
400
442
|
await callCDP('Runtime.releaseObjectGroup', { objectGroup });
|
|
401
443
|
}
|
|
402
444
|
}
|
|
403
445
|
/**
|
|
404
|
-
* Get DOM context (tag, classes, text preview) for a node: a one-line
|
|
446
|
+
* Get DOM context (tag, classes, key attributes, text preview) for a node: a one-line
|
|
405
447
|
* preview, and up to {@link ELEMENT_TEXT_LENGTH} characters of text when it
|
|
406
448
|
* is longer (all of it with `full`).
|
|
407
449
|
*
|
|
@@ -417,13 +459,18 @@ export async function getDomContext(ref, options = {}) {
|
|
|
417
459
|
return null;
|
|
418
460
|
}
|
|
419
461
|
const full = options.full === true;
|
|
420
|
-
const
|
|
421
|
-
const
|
|
462
|
+
const attributes = unpackAttributes(desc.attributes);
|
|
463
|
+
const classes = attributes['class']?.split(/\s+/).filter(Boolean);
|
|
464
|
+
const { text, state } = await elementTextAndState(ref, full);
|
|
422
465
|
const preview = textPreview(text);
|
|
423
466
|
const longer = textPreview(text, full ? Number.POSITIVE_INFINITY : ELEMENT_TEXT_LENGTH);
|
|
467
|
+
const tag = desc.nodeName.toLowerCase();
|
|
468
|
+
const keys = keyAttributes(tag, attributes, state);
|
|
424
469
|
return {
|
|
425
|
-
tag
|
|
470
|
+
tag,
|
|
426
471
|
...(classes && classes.length > 0 && { classes }),
|
|
472
|
+
...(keys && { attributes: keys }),
|
|
473
|
+
...(state.sensitive && { sensitive: true }),
|
|
427
474
|
...(preview && { preview }),
|
|
428
475
|
...(longer !== preview && { text: longer }),
|
|
429
476
|
...(!preview && (await childElements(ref))),
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { DomElementResolver } from '../DomElementResolver.js';
|
|
8
8
|
import { noMatchContext } from './query.js';
|
|
9
|
-
import { otherIndexSourceNote, staleNodeError, unreachableElementsNote, withLoadingHint, } from '../../../errors/messages.js';
|
|
9
|
+
import { otherIndexSourceNote, staleNodeError, shadowBoundaryLine, unreachableElementsNote, withLoadingHint, } from '../../../errors/messages.js';
|
|
10
10
|
import { joinLines } from '../../../ui/formatting.js';
|
|
11
11
|
import { EXIT_CODES } from '../../../utils/exitCodes.js';
|
|
12
12
|
/**
|
|
@@ -110,7 +110,8 @@ function failedResultFailure(result, options) {
|
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Add what the page says to a "not found" failure (one page evaluation, on
|
|
113
|
-
* this failure path only, {@link noMatchContext}): similar ids or classes,
|
|
113
|
+
* this failure path only, {@link noMatchContext}): similar ids or classes, a
|
|
114
|
+
* shadow host the selector tries to cross,
|
|
114
115
|
* the places selectors do not search (for a page script that found nothing)
|
|
115
116
|
* and the still-loading hint while the page loads.
|
|
116
117
|
*
|
|
@@ -124,7 +125,7 @@ async function withNotFoundContext(failure, selector, searched) {
|
|
|
124
125
|
return failure;
|
|
125
126
|
const context = await noMatchContext(selector);
|
|
126
127
|
const note = searched ? unreachableElementsNote(selector, context.unsearched) : '';
|
|
127
|
-
const suggestion = withLoadingHint(joinLines(context.similar, failure.errorContext?.suggestion, note ? note : undefined), context.readyState, selector);
|
|
128
|
+
const suggestion = withLoadingHint(joinLines(context.similar, context.shadowHost && shadowBoundaryLine(context.shadowHost), failure.errorContext?.suggestion, note ? note : undefined), context.readyState, selector);
|
|
128
129
|
return suggestion ? { ...failure, errorContext: { suggestion } } : failure;
|
|
129
130
|
}
|
|
130
131
|
//# sourceMappingURL=runElementCommand.js.map
|