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
|
@@ -66,7 +66,11 @@ export interface HandlerDetails {
|
|
|
66
66
|
targetName?: string | undefined;
|
|
67
67
|
/** Handlers jQuery runs from this listener (set when it is jQuery's dispatcher) */
|
|
68
68
|
jquery?: ResolvedHandler[] | undefined;
|
|
69
|
+
/** The handler Preact runs from this listener (set when it is Preact's event proxy) */
|
|
70
|
+
preact?: FrameworkHandler | undefined;
|
|
69
71
|
}
|
|
72
|
+
/** A framework's handler found behind its dispatcher, of the dispatcher's event type */
|
|
73
|
+
export type FrameworkHandler = Omit<ResolvedHandler, 'type'>;
|
|
70
74
|
/** A handler registered through a framework, found behind its dispatcher */
|
|
71
75
|
export interface ResolvedHandler {
|
|
72
76
|
/** Event type it was registered for */
|
|
@@ -140,14 +140,16 @@ function toElementListener(listener, found, name) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
|
-
* Report entries for a
|
|
144
|
-
*
|
|
143
|
+
* Report entries for a framework's dispatcher: one per handler it runs for
|
|
144
|
+
* the element (jQuery's handlers, Preact's handler), with the dispatcher's
|
|
145
|
+
* type and flags.
|
|
145
146
|
*
|
|
146
|
-
* @param dispatcher - Entry built for
|
|
147
|
+
* @param dispatcher - Entry built for the framework's own listener
|
|
147
148
|
* @param handlers - Handlers behind it
|
|
149
|
+
* @param framework - The framework
|
|
148
150
|
* @returns Entries naming the real handlers
|
|
149
151
|
*/
|
|
150
|
-
function
|
|
152
|
+
function frameworkListeners(dispatcher, handlers, framework) {
|
|
151
153
|
return handlers.map((resolved) => ({
|
|
152
154
|
...dispatcher,
|
|
153
155
|
handler: {
|
|
@@ -157,10 +159,25 @@ function jqueryListeners(dispatcher, handlers) {
|
|
|
157
159
|
lineNumber: resolved.lineNumber,
|
|
158
160
|
columnNumber: resolved.columnNumber,
|
|
159
161
|
},
|
|
160
|
-
framework
|
|
162
|
+
framework,
|
|
161
163
|
...(resolved.selector && { delegateSelector: resolved.selector }),
|
|
162
164
|
}));
|
|
163
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Report entries for one listener: itself, or the handlers behind a
|
|
168
|
+
* framework's dispatcher.
|
|
169
|
+
*
|
|
170
|
+
* @param built - Entry built for the listener
|
|
171
|
+
* @param detail - What the page reported about its handler
|
|
172
|
+
* @returns Entries
|
|
173
|
+
*/
|
|
174
|
+
function resolveDispatcher(built, detail) {
|
|
175
|
+
if (detail.jquery)
|
|
176
|
+
return frameworkListeners(built, detail.jquery, 'jQuery');
|
|
177
|
+
if (detail.preact)
|
|
178
|
+
return frameworkListeners(built, [detail.preact], 'Preact');
|
|
179
|
+
return [built];
|
|
180
|
+
}
|
|
164
181
|
/** Chain position given to React parents outside the chain: after the last one inside */
|
|
165
182
|
const OUTSIDE_CHAIN_OFFSET = 0.5;
|
|
166
183
|
/**
|
|
@@ -319,11 +336,11 @@ export function collapseFrameworkRoots(placed) {
|
|
|
319
336
|
};
|
|
320
337
|
}
|
|
321
338
|
/**
|
|
322
|
-
* Report entries of every listener, jQuery dispatchers
|
|
323
|
-
* handlers they run for the element.
|
|
339
|
+
* Report entries of every listener, jQuery dispatchers and Preact proxies
|
|
340
|
+
* replaced by the handlers they run for the element.
|
|
324
341
|
*
|
|
325
342
|
* @param found - Listeners per chain entry, in chain order
|
|
326
|
-
* @param details - Per flattened listener: handler name, jQuery handlers
|
|
343
|
+
* @param details - Per flattened listener: handler name, jQuery or Preact handlers
|
|
327
344
|
* @returns Entries in chain order
|
|
328
345
|
*/
|
|
329
346
|
function placeListeners(found, details) {
|
|
@@ -331,7 +348,7 @@ function placeListeners(found, details) {
|
|
|
331
348
|
return flat.flatMap(({ entry, listener }, i) => {
|
|
332
349
|
const detail = details[i] ?? {};
|
|
333
350
|
const built = toElementListener(listener, entry, detail.name);
|
|
334
|
-
const listeners =
|
|
351
|
+
const listeners = resolveDispatcher(built, detail);
|
|
335
352
|
return listeners.map((item) => ({
|
|
336
353
|
position: entry.position,
|
|
337
354
|
entry: entry.entry,
|
|
@@ -86,6 +86,11 @@ export declare const FILL_READ_BACK_SCRIPT = "(() => {\n const check = window._
|
|
|
86
86
|
* When selector matches multiple elements without index, prioritizes visible ones.
|
|
87
87
|
* A `<label>` is clicked (and double-clicked) through its control when that
|
|
88
88
|
* is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
|
|
89
|
+
* A hover first notes what is hidden around the element
|
|
90
|
+
* ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
|
|
91
|
+
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
92
|
+
* reaches the element, and otherwise which element it landed on: a browser
|
|
93
|
+
* dialog or bubble can swallow input while the page looks normal.
|
|
89
94
|
*/
|
|
90
95
|
export declare const CLICK_ELEMENT_SCRIPT: string;
|
|
91
96
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* to properly trigger React's event system.
|
|
7
7
|
*/
|
|
8
8
|
import { FILL_REFUSALS, LABEL_WITHOUT_CONTROL, NAME_QUERY_PLACEHOLDER, VIA_LABEL_SUFFIX, } from '../../errors/messages.js';
|
|
9
|
+
import { REVEAL_SNAPSHOT_JS } from './actionEffectsScripts.js';
|
|
9
10
|
import { ELEMENT_DESCRIPTION_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
10
11
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS } from './targetNode.js';
|
|
11
12
|
/**
|
|
@@ -437,6 +438,11 @@ export const FILL_READ_BACK_SCRIPT = `(() => {
|
|
|
437
438
|
* When selector matches multiple elements without index, prioritizes visible ones.
|
|
438
439
|
* A `<label>` is clicked (and double-clicked) through its control when that
|
|
439
440
|
* is visible ({@link LABEL_CONTROL_JS}), reported as e.g. `input (via label)`.
|
|
441
|
+
* A hover first notes what is hidden around the element
|
|
442
|
+
* ({@link REVEAL_SNAPSHOT_JS}), so its result can say what it revealed. For
|
|
443
|
+
* a press, a probe (`window.__bdgPressProbe`) records whether the press
|
|
444
|
+
* reaches the element, and otherwise which element it landed on: a browser
|
|
445
|
+
* dialog or bubble can swallow input while the page looks normal.
|
|
440
446
|
*/
|
|
441
447
|
export const CLICK_ELEMENT_SCRIPT = `
|
|
442
448
|
(function(selector, parts, index, action) {
|
|
@@ -588,12 +594,14 @@ export const CLICK_ELEMENT_SCRIPT = `
|
|
|
588
594
|
else if (!hasSize) obstruction = 'zero-size';
|
|
589
595
|
else if (!hittable) obstruction = 'covered by another element' + coveredBy();
|
|
590
596
|
|
|
591
|
-
|
|
592
|
-
|
|
597
|
+
if (action === 'hover') (${REVEAL_SNAPSHOT_JS})(el);
|
|
598
|
+
|
|
593
599
|
if (hittable && action !== 'hover') {
|
|
594
|
-
const probe = { reached: false };
|
|
600
|
+
const probe = { reached: false, landedOn: null };
|
|
595
601
|
const markReached = (event) => {
|
|
596
|
-
|
|
602
|
+
const path = event.composedPath();
|
|
603
|
+
if (path.includes(el)) probe.reached = true;
|
|
604
|
+
else if (!probe.landedOn && path[0] && path[0].nodeType === 1) probe.landedOn = describe(path[0]);
|
|
597
605
|
};
|
|
598
606
|
['pointerdown', 'mousedown'].forEach((type) => view.addEventListener(type, markReached, true));
|
|
599
607
|
probe.stop = () => ['pointerdown', 'mousedown'].forEach((type) => view.removeEventListener(type, markReached, true));
|
|
@@ -32,6 +32,26 @@ export declare function applySessionEmulation(cdp: CDPConnection, emulation: {
|
|
|
32
32
|
viewport?: ViewportSize;
|
|
33
33
|
colorScheme?: ColorScheme;
|
|
34
34
|
}): Promise<void>;
|
|
35
|
+
/** Page emulation of a session: what `--viewport` and `--color-scheme` set */
|
|
36
|
+
export interface SessionEmulation {
|
|
37
|
+
viewport?: ViewportSize;
|
|
38
|
+
colorScheme?: ColorScheme;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Change the page emulation mid-session (`bdg page emulate`): set the
|
|
42
|
+
* viewport or the color scheme, or clear both (back to the browser window
|
|
43
|
+
* and the system setting).
|
|
44
|
+
*
|
|
45
|
+
* @param cdp - Session connection
|
|
46
|
+
* @param current - Emulation now in effect
|
|
47
|
+
* @param change - What to set, or `reset`
|
|
48
|
+
* @param record - Called after each change Chrome accepted (so a later
|
|
49
|
+
* failure leaves the recorded emulation true)
|
|
50
|
+
* @returns Emulation in effect afterwards
|
|
51
|
+
*/
|
|
52
|
+
export declare function emulatePage(cdp: CDPConnection, current: SessionEmulation, change: SessionEmulation & {
|
|
53
|
+
reset?: boolean;
|
|
54
|
+
}, record: (emulation: SessionEmulation) => void): Promise<SessionEmulation>;
|
|
35
55
|
/** What the page currently renders with, for `bdg status` */
|
|
36
56
|
export interface PageAppearance {
|
|
37
57
|
/** Layout viewport without scrollbars (as `dom layout` reports it) */
|
|
@@ -37,6 +37,43 @@ export async function applySessionEmulation(cdp, emulation) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Change the page emulation mid-session (`bdg page emulate`): set the
|
|
42
|
+
* viewport or the color scheme, or clear both (back to the browser window
|
|
43
|
+
* and the system setting).
|
|
44
|
+
*
|
|
45
|
+
* @param cdp - Session connection
|
|
46
|
+
* @param current - Emulation now in effect
|
|
47
|
+
* @param change - What to set, or `reset`
|
|
48
|
+
* @param record - Called after each change Chrome accepted (so a later
|
|
49
|
+
* failure leaves the recorded emulation true)
|
|
50
|
+
* @returns Emulation in effect afterwards
|
|
51
|
+
*/
|
|
52
|
+
export async function emulatePage(cdp, current, change, record) {
|
|
53
|
+
let state = current;
|
|
54
|
+
const step = async (send, next) => {
|
|
55
|
+
await send();
|
|
56
|
+
state = next;
|
|
57
|
+
record(state);
|
|
58
|
+
};
|
|
59
|
+
const { viewport: _viewport, colorScheme, ...rest } = state;
|
|
60
|
+
if (change.reset || change.viewport) {
|
|
61
|
+
await step(() => change.viewport
|
|
62
|
+
? cdp.send('Emulation.setDeviceMetricsOverride', viewportOverride(change.viewport))
|
|
63
|
+
: cdp.send('Emulation.clearDeviceMetricsOverride', {}), {
|
|
64
|
+
...rest,
|
|
65
|
+
...(colorScheme && { colorScheme }),
|
|
66
|
+
...(change.viewport && { viewport: change.viewport }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (change.reset || change.colorScheme) {
|
|
70
|
+
const { colorScheme: _scheme, ...withoutScheme } = state;
|
|
71
|
+
await step(() => cdp.send('Emulation.setEmulatedMedia', {
|
|
72
|
+
features: [{ name: 'prefers-color-scheme', value: change.colorScheme ?? '' }],
|
|
73
|
+
}), { ...withoutScheme, ...(change.colorScheme && { colorScheme: change.colorScheme }) });
|
|
74
|
+
}
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
40
77
|
/** How long `bdg status` waits for the page to report its appearance */
|
|
41
78
|
const APPEARANCE_TIMEOUT_MS = 1000;
|
|
42
79
|
/**
|
package/dist/telemetry/a11y.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export declare function buildTreeFromRawNodes(rawNodes: Protocol.Accessibility.A
|
|
|
24
24
|
* @throws Error if CDP Accessibility domain fails
|
|
25
25
|
*/
|
|
26
26
|
export declare function collectA11yTree(): Promise<A11yTree>;
|
|
27
|
+
/**
|
|
28
|
+
* The tree with the values of secret fields masked ({@link MASKED_VALUE},
|
|
29
|
+
* whatever their length), and the names and values of the nodes inside them
|
|
30
|
+
* left out (the text Chrome lists under a text field is its value).
|
|
31
|
+
*
|
|
32
|
+
* @param tree - Accessibility tree
|
|
33
|
+
* @param secretIds - Node ids of secret fields
|
|
34
|
+
* @returns The same tree (nodes replaced in place)
|
|
35
|
+
*/
|
|
36
|
+
export declare function maskSecretValues(tree: A11yTree, secretIds: ReadonlySet<string>): A11yTree;
|
|
27
37
|
/**
|
|
28
38
|
* Queries accessibility tree by pattern (role, name, description).
|
|
29
39
|
*
|
package/dist/telemetry/a11y.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { CommandError } from '../errors/index.js';
|
|
2
2
|
import { unknownQueryFieldError } from '../errors/messages.js';
|
|
3
3
|
import { callCDP } from '../ipc/client.js';
|
|
4
|
+
import { MASKED_VALUE, SENSITIVE_FIELD_JS } from '../runtime/dom/elementInfo.js';
|
|
4
5
|
import { childFrameIds } from '../runtime/dom/frameLayout.js';
|
|
6
|
+
import { ConcurrencyLimiter } from '../utils/concurrency.js';
|
|
5
7
|
import { EXIT_CODES } from '../utils/exitCodes.js';
|
|
6
8
|
import { levenshteinDistance } from '../utils/levenshtein.js';
|
|
7
9
|
/**
|
|
@@ -80,12 +82,87 @@ export async function collectA11yTree() {
|
|
|
80
82
|
if (!result?.nodes) {
|
|
81
83
|
throw new CommandError('Failed to get accessibility tree', { suggestion: 'CDP returned no nodes. The page may not be fully loaded.' }, EXIT_CODES.SOFTWARE_ERROR);
|
|
82
84
|
}
|
|
83
|
-
|
|
85
|
+
const tree = buildTreeFromRawNodes([
|
|
86
|
+
...result.nodes,
|
|
87
|
+
...(await collectFrameNodes(result.nodes)),
|
|
88
|
+
]);
|
|
89
|
+
return maskSecretValues(tree, await sensitiveNodeIds(tree));
|
|
84
90
|
}
|
|
85
91
|
finally {
|
|
86
92
|
await callCDP('Accessibility.disable', {});
|
|
87
93
|
}
|
|
88
94
|
}
|
|
95
|
+
/** Concurrent page checks for {@link sensitiveNodeIds} */
|
|
96
|
+
const SENSITIVE_CHECK_CONCURRENCY = 10;
|
|
97
|
+
/**
|
|
98
|
+
* The accessibility nodes whose value is a secret: nodes with a value whose
|
|
99
|
+
* element is a sensitive field ({@link SENSITIVE_FIELD_JS}: passwords, card
|
|
100
|
+
* fields, one-time codes). Only nodes with a value are checked, one page
|
|
101
|
+
* call each (a page has few).
|
|
102
|
+
*
|
|
103
|
+
* @param tree - Accessibility tree
|
|
104
|
+
* @returns Node ids of secret fields
|
|
105
|
+
*/
|
|
106
|
+
async function sensitiveNodeIds(tree) {
|
|
107
|
+
const candidates = [...tree.nodes.values()].filter((node) => node.value !== undefined && node.backendDOMNodeId !== undefined);
|
|
108
|
+
const limiter = new ConcurrencyLimiter(SENSITIVE_CHECK_CONCURRENCY);
|
|
109
|
+
const checked = await Promise.all(candidates.map((node) => limiter.run(async () => (await isSensitiveField(node.backendDOMNodeId ?? 0)) ? node.nodeId : null)));
|
|
110
|
+
return new Set(checked.filter((id) => id !== null));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether an element is a sensitive field. An element that cannot be read
|
|
114
|
+
* counts as one, so a value is never shown by mistake.
|
|
115
|
+
*
|
|
116
|
+
* @param backendNodeId - The element
|
|
117
|
+
* @returns True for secret fields (and unreadable elements)
|
|
118
|
+
*/
|
|
119
|
+
async function isSensitiveField(backendNodeId) {
|
|
120
|
+
const objectGroup = 'bdg-a11y-secret';
|
|
121
|
+
const resolved = await callCDP('DOM.resolveNode', { backendNodeId, objectGroup });
|
|
122
|
+
const objectId = resolved.data?.result?.object
|
|
123
|
+
?.objectId;
|
|
124
|
+
if (!objectId)
|
|
125
|
+
return true;
|
|
126
|
+
try {
|
|
127
|
+
const response = await callCDP('Runtime.callFunctionOn', {
|
|
128
|
+
objectId,
|
|
129
|
+
functionDeclaration: `function () { return this.nodeType !== 1 || (${SENSITIVE_FIELD_JS})(this); }`,
|
|
130
|
+
returnByValue: true,
|
|
131
|
+
});
|
|
132
|
+
const value = response.data?.result?.result
|
|
133
|
+
?.value;
|
|
134
|
+
return value !== false;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await callCDP('Runtime.releaseObjectGroup', { objectGroup });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The tree with the values of secret fields masked ({@link MASKED_VALUE},
|
|
142
|
+
* whatever their length), and the names and values of the nodes inside them
|
|
143
|
+
* left out (the text Chrome lists under a text field is its value).
|
|
144
|
+
*
|
|
145
|
+
* @param tree - Accessibility tree
|
|
146
|
+
* @param secretIds - Node ids of secret fields
|
|
147
|
+
* @returns The same tree (nodes replaced in place)
|
|
148
|
+
*/
|
|
149
|
+
export function maskSecretValues(tree, secretIds) {
|
|
150
|
+
const hide = (nodeId, inside) => {
|
|
151
|
+
const node = tree.nodes.get(nodeId);
|
|
152
|
+
if (!node)
|
|
153
|
+
return;
|
|
154
|
+
const { name: _name, value: _value, ...rest } = node;
|
|
155
|
+
const masked = inside
|
|
156
|
+
? rest
|
|
157
|
+
: { ...node, ...(node.value !== undefined && { value: MASKED_VALUE }) };
|
|
158
|
+
tree.nodes.set(nodeId, masked);
|
|
159
|
+
if (tree.root.nodeId === nodeId)
|
|
160
|
+
tree.root = masked;
|
|
161
|
+
node.childIds?.forEach((childId) => hide(childId, true));
|
|
162
|
+
};
|
|
163
|
+
secretIds.forEach((nodeId) => hide(nodeId, false));
|
|
164
|
+
return tree;
|
|
165
|
+
}
|
|
89
166
|
/**
|
|
90
167
|
* Accessibility nodes of the page's iframes, attached under their `Iframe`
|
|
91
168
|
* node (the page's own tree stops there). Frames in another process
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Console message collection via CDP Runtime and Log domains.
|
|
3
3
|
*
|
|
4
4
|
* Captures console.log, console.error, etc. and JavaScript exceptions
|
|
5
|
+
* (unhandled rejections removed again when a handler is attached later)
|
|
5
6
|
* with automatic nested object expansion, browser messages (failed loads,
|
|
6
7
|
* CORS, security, deprecations), and the same from cross-origin iframes and
|
|
7
8
|
* workers.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Console message collection via CDP Runtime and Log domains.
|
|
3
3
|
*
|
|
4
4
|
* Captures console.log, console.error, etc. and JavaScript exceptions
|
|
5
|
+
* (unhandled rejections removed again when a handler is attached later)
|
|
5
6
|
* with automatic nested object expansion, browser messages (failed loads,
|
|
6
7
|
* CORS, security, deprecations), and the same from cross-origin iframes and
|
|
7
8
|
* workers.
|
|
@@ -76,14 +77,17 @@ function createMessage(type, text, timestamp, args, context) {
|
|
|
76
77
|
/**
|
|
77
78
|
* Insert a message in timestamp order.
|
|
78
79
|
* Messages are kept sorted by timestamp to handle async expansion delays.
|
|
80
|
+
*
|
|
81
|
+
* @returns False when the message limit is reached (the message is dropped)
|
|
79
82
|
*/
|
|
80
83
|
function insertMessageByTimestamp(messages, message) {
|
|
81
84
|
if (messages.length >= MAX_CONSOLE_MESSAGES) {
|
|
82
85
|
log.debug(`Warning: Console message limit reached (${MAX_CONSOLE_MESSAGES})`);
|
|
83
|
-
return;
|
|
86
|
+
return false;
|
|
84
87
|
}
|
|
85
88
|
const insertIndex = findInsertIndex(messages, message.timestamp);
|
|
86
89
|
messages.splice(insertIndex, 0, message);
|
|
90
|
+
return true;
|
|
87
91
|
}
|
|
88
92
|
/**
|
|
89
93
|
* Find the correct insertion index to maintain timestamp order.
|
|
@@ -171,15 +175,93 @@ export function formatExceptionText(details) {
|
|
|
171
175
|
}
|
|
172
176
|
/**
|
|
173
177
|
* Handle an exception thrown event.
|
|
178
|
+
*
|
|
179
|
+
* @returns The message added, undefined when filtered out or over the limit
|
|
174
180
|
*/
|
|
175
181
|
function handleExceptionThrown(messages, params, context, includeAll) {
|
|
176
182
|
const exception = params.exceptionDetails;
|
|
177
183
|
const text = formatExceptionText(exception);
|
|
178
184
|
if (shouldExcludeConsoleMessage(text, 'error', includeAll)) {
|
|
179
|
-
return;
|
|
185
|
+
return undefined;
|
|
180
186
|
}
|
|
181
187
|
const message = createMessage('error', text, params.timestamp, undefined, context);
|
|
182
|
-
insertMessageByTimestamp(messages, message);
|
|
188
|
+
return insertMessageByTimestamp(messages, message) ? message : undefined;
|
|
189
|
+
}
|
|
190
|
+
/** CDP's text of an unhandled promise rejection (the only kind Chrome revokes) */
|
|
191
|
+
const UNHANDLED_REJECTION_TEXT = 'Uncaught (in promise)';
|
|
192
|
+
/**
|
|
193
|
+
* Unhandled promise rejections that may still be revoked: Chrome reports a
|
|
194
|
+
* rejection without a handler at the end of the task, and revokes the
|
|
195
|
+
* report (`Runtime.exceptionRevoked`) when a handler is attached later, as
|
|
196
|
+
* DevTools does by removing the message. `bdg dom eval` attaches its
|
|
197
|
+
* handler only after the evaluation returns, so this is what keeps
|
|
198
|
+
* `bdg dom eval 'Promise.reject(…)'` out of the console; a rejection
|
|
199
|
+
* nothing ever handles stays.
|
|
200
|
+
*/
|
|
201
|
+
class RevocableRejections {
|
|
202
|
+
/** Messages of rejections, by session and exception id, oldest first */
|
|
203
|
+
pending = new Map();
|
|
204
|
+
/**
|
|
205
|
+
* Remember a reported exception if it is an unhandled rejection. Past
|
|
206
|
+
* {@link MAX_CONSOLE_MESSAGES} rejections, the oldest is forgotten.
|
|
207
|
+
*
|
|
208
|
+
* @param details - Exception details
|
|
209
|
+
* @param message - Its console message
|
|
210
|
+
* @param sessionId - Session it was reported on (undefined: the page)
|
|
211
|
+
*/
|
|
212
|
+
track(details, message, sessionId) {
|
|
213
|
+
if (!details.text.startsWith(UNHANDLED_REJECTION_TEXT))
|
|
214
|
+
return;
|
|
215
|
+
this.pending.set(this.key(details.exceptionId, sessionId), message);
|
|
216
|
+
if (this.pending.size <= MAX_CONSOLE_MESSAGES)
|
|
217
|
+
return;
|
|
218
|
+
const [oldest] = this.pending.keys();
|
|
219
|
+
if (oldest !== undefined)
|
|
220
|
+
this.pending.delete(oldest);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Remove the message of a revoked rejection.
|
|
224
|
+
*
|
|
225
|
+
* @param messages - Console messages (updated)
|
|
226
|
+
* @param exceptionId - Revoked exception
|
|
227
|
+
* @param sessionId - Session it was revoked on (undefined: the page)
|
|
228
|
+
*/
|
|
229
|
+
revoke(messages, exceptionId, sessionId) {
|
|
230
|
+
const key = this.key(exceptionId, sessionId);
|
|
231
|
+
const message = this.pending.get(key);
|
|
232
|
+
this.pending.delete(key);
|
|
233
|
+
const index = message ? messages.indexOf(message) : -1;
|
|
234
|
+
if (index !== -1)
|
|
235
|
+
messages.splice(index, 1);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Forget the rejections of a session whose documents are gone (it
|
|
239
|
+
* detached, or its contexts were cleared by a navigation): they can no
|
|
240
|
+
* longer be revoked.
|
|
241
|
+
*
|
|
242
|
+
* @param sessionId - Session (undefined: the page)
|
|
243
|
+
*/
|
|
244
|
+
forgetSession(sessionId) {
|
|
245
|
+
const prefix = this.key('', sessionId);
|
|
246
|
+
for (const key of this.pending.keys()) {
|
|
247
|
+
if (key.startsWith(prefix))
|
|
248
|
+
this.pending.delete(key);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Forget every rejection (the collection stopped). */
|
|
252
|
+
clear() {
|
|
253
|
+
this.pending.clear();
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Map key of an exception: ids are counted per session.
|
|
257
|
+
*
|
|
258
|
+
* @param exceptionId - Exception id ('' for the session's prefix)
|
|
259
|
+
* @param sessionId - Session
|
|
260
|
+
* @returns Key
|
|
261
|
+
*/
|
|
262
|
+
key(exceptionId, sessionId) {
|
|
263
|
+
return `${sessionId ?? ''}:${exceptionId}`;
|
|
264
|
+
}
|
|
183
265
|
}
|
|
184
266
|
/**
|
|
185
267
|
* Handle a browser log entry (failed resource loads, CORS, security,
|
|
@@ -255,12 +337,24 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
|
|
|
255
337
|
};
|
|
256
338
|
handleConsoleAPICall(senderFor(cdp, sessionId), messages, params, context, includeAll);
|
|
257
339
|
});
|
|
258
|
-
|
|
340
|
+
const rejections = new RevocableRejections();
|
|
341
|
+
registry.registerTyped(typed, 'Runtime.exceptionThrown', (params, sessionId) => {
|
|
259
342
|
const context = {
|
|
260
343
|
navigationId: getCurrentNavigationId?.(),
|
|
261
344
|
stackTrace: convertStackTrace(params.exceptionDetails.stackTrace),
|
|
262
345
|
};
|
|
263
|
-
handleExceptionThrown(messages, params, context, includeAll);
|
|
346
|
+
const message = handleExceptionThrown(messages, params, context, includeAll);
|
|
347
|
+
if (message)
|
|
348
|
+
rejections.track(params.exceptionDetails, message, sessionId);
|
|
349
|
+
});
|
|
350
|
+
registry.registerTyped(typed, 'Runtime.exceptionRevoked', ({ exceptionId }, sessionId) => {
|
|
351
|
+
rejections.revoke(messages, exceptionId, sessionId);
|
|
352
|
+
});
|
|
353
|
+
registry.registerTyped(typed, 'Runtime.executionContextsCleared', (_params, sessionId) => {
|
|
354
|
+
rejections.forgetSession(sessionId);
|
|
355
|
+
});
|
|
356
|
+
registry.registerTyped(typed, 'Target.detachedFromTarget', ({ sessionId }) => {
|
|
357
|
+
rejections.forgetSession(sessionId);
|
|
264
358
|
});
|
|
265
359
|
registry.registerTyped(typed, 'Log.entryAdded', ({ entry }) => {
|
|
266
360
|
handleLogEntry(messages, entry, getCurrentNavigationId?.(), includeAll);
|
|
@@ -275,6 +369,7 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
|
|
|
275
369
|
const detachChildren = await startOptionalSources(cdp, typed);
|
|
276
370
|
return async () => {
|
|
277
371
|
registry.cleanup();
|
|
372
|
+
rejections.clear();
|
|
278
373
|
await detachChildren();
|
|
279
374
|
};
|
|
280
375
|
}
|
|
@@ -110,7 +110,7 @@ export function skippedBodyReason(body) {
|
|
|
110
110
|
* @param resourceType - Resource type reported with the response
|
|
111
111
|
*/
|
|
112
112
|
function applyResponse(request, response, resourceType) {
|
|
113
|
-
const { status, mimeType, headers, timing, remoteIPAddress, connectionId } = response;
|
|
113
|
+
const { status, mimeType, headers, timing, remoteIPAddress, remotePort, connectionId } = response;
|
|
114
114
|
request.status = status;
|
|
115
115
|
if (response.statusText)
|
|
116
116
|
request.statusText = response.statusText;
|
|
@@ -138,6 +138,8 @@ function applyResponse(request, response, resourceType) {
|
|
|
138
138
|
}
|
|
139
139
|
if (remoteIPAddress)
|
|
140
140
|
request.serverIPAddress = remoteIPAddress;
|
|
141
|
+
if (remotePort)
|
|
142
|
+
request.serverPort = remotePort;
|
|
141
143
|
if (connectionId !== undefined)
|
|
142
144
|
request.connection = String(connectionId);
|
|
143
145
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -137,7 +137,10 @@ export interface NetworkRequest {
|
|
|
137
137
|
redirectURL?: string;
|
|
138
138
|
encodedDataLength?: number;
|
|
139
139
|
decodedBodyLength?: number;
|
|
140
|
+
/** Address Chrome connected to: the server's, or a proxy's (CDP `remoteIPAddress`) */
|
|
140
141
|
serverIPAddress?: string;
|
|
142
|
+
/** Port Chrome connected to (CDP `remotePort`) */
|
|
143
|
+
serverPort?: number;
|
|
141
144
|
connection?: string;
|
|
142
145
|
/** Status text sent by the server (empty for HTTP/2) */
|
|
143
146
|
statusText?: string;
|
|
@@ -324,7 +327,34 @@ export interface DomContext {
|
|
|
324
327
|
children?: string[];
|
|
325
328
|
/** Number of child elements, for an element without text */
|
|
326
329
|
childCount?: number;
|
|
330
|
+
/** Attributes that identify it by its type (see {@link KeyAttributes}) */
|
|
331
|
+
attributes?: KeyAttributes;
|
|
332
|
+
/** A field holding a secret (password, card, one-time code): its value is shown masked */
|
|
333
|
+
sensitive?: boolean;
|
|
327
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Live state of a form control read in the page: an input's type and value
|
|
337
|
+
* (`checked` for checkboxes and radios), a textarea's value, the labels of a
|
|
338
|
+
* select's selected options, the type of a button in a form.
|
|
339
|
+
*/
|
|
340
|
+
export interface ElementState {
|
|
341
|
+
type?: string;
|
|
342
|
+
/** Masked in the page for sensitive fields; never read for hidden inputs */
|
|
343
|
+
value?: string;
|
|
344
|
+
checked?: boolean;
|
|
345
|
+
selected?: string;
|
|
346
|
+
/** A field holding a secret (its value and selected option are masked) */
|
|
347
|
+
sensitive?: boolean;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The attributes that identify an element by its type, with full values:
|
|
351
|
+
* img `src`, `alt`; a `href`; input `type`, `name`, `placeholder`, `value`
|
|
352
|
+
* (current value, masked for passwords; for checkboxes and radios their
|
|
353
|
+
* `value` attribute) and `checked`; textarea `name`,
|
|
354
|
+
* `placeholder`, `value`; button `type` (`submit` by default in a form), `name`; select `name`, `selected`
|
|
355
|
+
* (labels of the selected options); iframe `src`; form `action`, `method`.
|
|
356
|
+
*/
|
|
357
|
+
export type KeyAttributes = Record<string, string | boolean>;
|
|
328
358
|
/**
|
|
329
359
|
* Reference to a DOM node for CDP calls: a per-connection `nodeId` (valid only
|
|
330
360
|
* within one command) or a `backendNodeId` (valid while the node exists).
|
|
@@ -392,6 +422,8 @@ export interface DomQueryResult {
|
|
|
392
422
|
type?: string;
|
|
393
423
|
/** `value` attribute of an `<option>` */
|
|
394
424
|
value?: string;
|
|
425
|
+
/** Attributes that identify it by its type (see {@link KeyAttributes}) */
|
|
426
|
+
attributes?: KeyAttributes;
|
|
395
427
|
classes?: string[];
|
|
396
428
|
/** Text content preview (display only, never used for targeting) */
|
|
397
429
|
preview?: string;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { NetworkRequest, ConsoleMessage } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The address Chrome connected to, with its port, noting when it looks like
|
|
4
|
+
* a proxy on this machine ({@link looksLikeLocalProxy}).
|
|
5
|
+
*
|
|
6
|
+
* @param request - Request with `serverIPAddress`
|
|
7
|
+
* @returns e.g. `93.184.215.14:443`, `[2606:4700::1]:443`, `127.0.0.1:9000 (loopback; likely a local proxy)`
|
|
8
|
+
*/
|
|
9
|
+
export declare function remoteAddress(request: NetworkRequest): string;
|
|
2
10
|
/**
|
|
3
11
|
* Format network request details for human-readable output.
|
|
4
12
|
*
|