browser-debugger-cli 0.13.0 → 0.15.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/.claude/skills/bdg/SKILL.md +101 -187
- package/README.md +4 -4
- package/dist/commands/cdp.js +1 -0
- package/dist/commands/cleanup.js +3 -0
- package/dist/commands/console.js +5 -1
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +20 -20
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +8 -5
- package/dist/commands/dom/formInteraction.js +1 -1
- package/dist/commands/dom/get.js +25 -7
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/index.js +7 -2
- package/dist/commands/dom/query.d.ts +2 -1
- package/dist/commands/dom/query.js +5 -3
- package/dist/commands/dom/screenshot.js +1 -0
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +4 -4
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/network/list.js +46 -3
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +60 -42
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.js +2 -2
- package/dist/commands/shared/dataFetcher.d.ts +4 -2
- package/dist/commands/shared/dataFetcher.js +11 -3
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +15 -3
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +1 -1
- package/dist/connection/launcher.js +1 -2
- package/dist/constants.d.ts +31 -5
- package/dist/constants.js +37 -5
- package/dist/daemon/SessionController.js +2 -0
- package/dist/daemon/launcher.d.ts +17 -3
- package/dist/daemon/launcher.js +37 -7
- package/dist/daemon/session/Session.d.ts +2 -1
- package/dist/daemon/session/Session.js +10 -2
- package/dist/daemon/session/TelemetryStore.d.ts +7 -0
- package/dist/daemon/session/TelemetryStore.js +6 -0
- package/dist/daemon/session/commandRegistry.js +25 -7
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +1 -0
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +8742 -8315
- package/dist/errors/messages.d.ts +31 -0
- package/dist/errors/messages.js +96 -6
- package/dist/index.js +1129 -548
- package/dist/ipc/client.d.ts +6 -1
- package/dist/ipc/client.js +11 -2
- package/dist/ipc/protocol/commands.d.ts +8 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
- package/dist/ipc/session/types.d.ts +5 -1
- package/dist/ipc/transport/index.d.ts +6 -0
- package/dist/ipc/transport/index.js +16 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/elementGeometry.d.ts +23 -0
- package/dist/runtime/dom/elementGeometry.js +17 -15
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +15 -5
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +40 -26
- package/dist/runtime/dom/inspectModel.d.ts +3 -3
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +1 -2
- package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
- package/dist/runtime/dom/reactEventHelpers.js +9 -2
- package/dist/runtime/dom/targetNode.d.ts +10 -6
- package/dist/runtime/dom/targetNode.js +15 -8
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +38 -3
- package/dist/session/paths.js +154 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +83 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +11 -3
- package/dist/telemetry/har/sanitize.d.ts +24 -0
- package/dist/telemetry/har/sanitize.js +138 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
- package/dist/telemetry/har/sanitizeBody.js +168 -0
- package/dist/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/types.d.ts +26 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/console/chronological.js +4 -4
- package/dist/ui/formatters/console/follow.d.ts +4 -2
- package/dist/ui/formatters/console/follow.js +6 -3
- package/dist/ui/formatters/console/json.d.ts +3 -6
- package/dist/ui/formatters/console/json.js +9 -13
- package/dist/ui/formatters/console/shared.d.ts +17 -2
- package/dist/ui/formatters/console/shared.js +17 -0
- package/dist/ui/formatters/console/summarize.d.ts +2 -2
- package/dist/ui/formatters/console/summarize.js +22 -7
- package/dist/ui/formatters/console.d.ts +1 -1
- package/dist/ui/formatters/console.js +1 -5
- package/dist/ui/formatters/details.js +1 -1
- package/dist/ui/formatters/dom.d.ts +13 -4
- package/dist/ui/formatters/dom.js +25 -7
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -2
- package/dist/ui/formatters/preview.d.ts +4 -1
- package/dist/ui/formatters/preview.js +55 -13
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/messages/chrome.d.ts +34 -7
- package/dist/ui/messages/chrome.js +81 -15
- package/dist/ui/messages/commands.d.ts +29 -8
- package/dist/ui/messages/commands.js +36 -8
- package/dist/ui/messages/networkMessages.d.ts +50 -0
- package/dist/ui/messages/networkMessages.js +66 -0
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
package/dist/ipc/client.d.ts
CHANGED
|
@@ -208,8 +208,13 @@ export declare function callCDP(method: string, params?: Record<string, unknown>
|
|
|
208
208
|
export declare function callBdgScript(method: string, params?: Record<string, unknown>): Promise<ClientResponse<'cdp_call'>>;
|
|
209
209
|
/**
|
|
210
210
|
* Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
|
|
211
|
+
*
|
|
212
|
+
* @param script - JavaScript expression
|
|
213
|
+
* @param frame - Iframe to evaluate in
|
|
214
|
+
* @param full - `--full`: copy an object or array result with every entry
|
|
215
|
+
* @returns The daemon's response
|
|
211
216
|
*/
|
|
212
|
-
export declare function domEval(script: string, frame?: string): Promise<ClientResponse<'dom_eval'>>;
|
|
217
|
+
export declare function domEval(script: string, frame?: string, full?: boolean): Promise<ClientResponse<'dom_eval'>>;
|
|
213
218
|
/**
|
|
214
219
|
* List the page's iframes via the daemon.
|
|
215
220
|
*/
|
package/dist/ipc/client.js
CHANGED
|
@@ -292,9 +292,18 @@ export function callBdgScript(method, params) {
|
|
|
292
292
|
}
|
|
293
293
|
/**
|
|
294
294
|
* Evaluate a JavaScript expression in the active page (or one of its iframes) via the daemon.
|
|
295
|
+
*
|
|
296
|
+
* @param script - JavaScript expression
|
|
297
|
+
* @param frame - Iframe to evaluate in
|
|
298
|
+
* @param full - `--full`: copy an object or array result with every entry
|
|
299
|
+
* @returns The daemon's response
|
|
295
300
|
*/
|
|
296
|
-
export async function domEval(script, frame) {
|
|
297
|
-
return sendCommand('dom_eval', {
|
|
301
|
+
export async function domEval(script, frame, full) {
|
|
302
|
+
return sendCommand('dom_eval', {
|
|
303
|
+
script,
|
|
304
|
+
...(frame !== undefined && { frame }),
|
|
305
|
+
...(full && { full }),
|
|
306
|
+
});
|
|
298
307
|
}
|
|
299
308
|
/**
|
|
300
309
|
* List the page's iframes via the daemon.
|
|
@@ -74,6 +74,10 @@ export interface SessionPeekData {
|
|
|
74
74
|
totalConsole: number;
|
|
75
75
|
/** Console messages dropped at the limit (the oldest; indices start after them) */
|
|
76
76
|
droppedConsole?: number;
|
|
77
|
+
/** Finished network requests dropped at the request cap (the oldest) */
|
|
78
|
+
droppedNetwork?: number;
|
|
79
|
+
/** Response bodies evicted at the total body budget (the oldest) */
|
|
80
|
+
evictedNetworkBodies?: number;
|
|
77
81
|
/** Whether there are more network items available. */
|
|
78
82
|
hasMoreNetwork?: boolean;
|
|
79
83
|
/** Whether there are more console items available. */
|
|
@@ -183,6 +187,8 @@ export interface DomEvalCommand {
|
|
|
183
187
|
script: string;
|
|
184
188
|
/** Iframe to evaluate in: index, name/id attribute, or URL substring */
|
|
185
189
|
frame?: string;
|
|
190
|
+
/** `--full`: copy an object or array result with every entry */
|
|
191
|
+
full?: boolean;
|
|
186
192
|
}
|
|
187
193
|
export interface DomEvalData {
|
|
188
194
|
/** JSON value, or a readable description for values JSON cannot represent */
|
|
@@ -191,6 +197,8 @@ export interface DomEvalData {
|
|
|
191
197
|
type: string;
|
|
192
198
|
/** Object subtype (node, date, array, ...) */
|
|
193
199
|
subtype?: string;
|
|
200
|
+
/** Elements of an array result in the page (`value` holds at most 1000 unless `full`) */
|
|
201
|
+
length?: number;
|
|
194
202
|
/** URL of the iframe the script ran in (with `frame`; empty when it has none) */
|
|
195
203
|
frame?: string;
|
|
196
204
|
/** Set when the page replaced built-ins bdg's copy of the result uses */
|
|
@@ -447,7 +447,10 @@ export interface InspectResult {
|
|
|
447
447
|
rules?: InspectRule[];
|
|
448
448
|
/** `--why <property>`: one entry, or one per longhand of a shorthand whose sides differ */
|
|
449
449
|
why?: InspectWhy[];
|
|
450
|
-
/**
|
|
451
|
-
|
|
450
|
+
/**
|
|
451
|
+
* The cascade was not read: Chrome took longer than the time allowed, failed,
|
|
452
|
+
* or (hints only) was not asked because it was too slow earlier on this page
|
|
453
|
+
*/
|
|
454
|
+
cascade?: 'timeout' | 'failed' | 'skipped';
|
|
452
455
|
}
|
|
453
456
|
//# sourceMappingURL=inspectTypes.d.ts.map
|
|
@@ -8,8 +8,12 @@ import type { ColorScheme, ViewportSize } from '../../types.js';
|
|
|
8
8
|
* Session activity metrics.
|
|
9
9
|
*/
|
|
10
10
|
export interface SessionActivity {
|
|
11
|
-
/**
|
|
11
|
+
/** Network requests kept (finished ones; the newest at the cap). */
|
|
12
12
|
networkRequestsCaptured: number;
|
|
13
|
+
/** Oldest finished requests dropped at the request cap (left out when none). */
|
|
14
|
+
networkRequestsDropped?: number;
|
|
15
|
+
/** Oldest response bodies evicted at the total body budget (left out when none). */
|
|
16
|
+
networkBodiesEvicted?: number;
|
|
13
17
|
/** Total console messages captured. */
|
|
14
18
|
consoleMessagesCaptured: number;
|
|
15
19
|
/** Timestamp of last network request. */
|
|
@@ -18,6 +18,12 @@ type WithTypeAndSession = {
|
|
|
18
18
|
* @param timeoutMs - How long to wait for the response (default: IPC timeout)
|
|
19
19
|
* @param socketPath - Daemon socket (default: the selected session's)
|
|
20
20
|
* @returns The daemon's response
|
|
21
|
+
* @throws CommandError (103) before connecting when the socket's session
|
|
22
|
+
* directory cannot be trusted (see {@link secureSessionDir}): a socket
|
|
23
|
+
* planted there by another user would receive the request. The check runs
|
|
24
|
+
* just before connecting, by path; replacing the socket in that window
|
|
25
|
+
* needs write access to a directory of the chain, which the check has
|
|
26
|
+
* just found only the user has
|
|
21
27
|
*/
|
|
22
28
|
export declare function sendRequest<TRequest extends WithTypeAndSession, TResponse extends WithTypeAndSession>(request: TRequest, requestName: string, expectedType?: string, timeoutMs?: number, socketPath?: string): Promise<TResponse>;
|
|
23
29
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles Unix domain socket communication with JSONL protocol.
|
|
5
5
|
*/
|
|
6
|
+
import * as path from 'path';
|
|
6
7
|
import { getIPCRequestTimeout } from '../../constants.js';
|
|
7
|
-
import {
|
|
8
|
+
import { CommandError } from '../../errors/index.js';
|
|
9
|
+
import { untrustedSessionDirError } from '../../errors/messages.js';
|
|
10
|
+
import { getDaemonSocketPath, secureSessionDir } from '../../session/paths.js';
|
|
8
11
|
import { createLogger } from '../../ui/logging/index.js';
|
|
12
|
+
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
9
13
|
import { formatConnectionError, formatEarlyCloseError, formatParseError, formatTimeoutError, } from './errors.js';
|
|
10
14
|
import { JSONLBuffer, parseJSONLFrame, toJSONLFrame } from './jsonl.js';
|
|
11
15
|
import { createSocket } from './socket.js';
|
|
@@ -22,8 +26,19 @@ const log = createLogger('client');
|
|
|
22
26
|
* @param timeoutMs - How long to wait for the response (default: IPC timeout)
|
|
23
27
|
* @param socketPath - Daemon socket (default: the selected session's)
|
|
24
28
|
* @returns The daemon's response
|
|
29
|
+
* @throws CommandError (103) before connecting when the socket's session
|
|
30
|
+
* directory cannot be trusted (see {@link secureSessionDir}): a socket
|
|
31
|
+
* planted there by another user would receive the request. The check runs
|
|
32
|
+
* just before connecting, by path; replacing the socket in that window
|
|
33
|
+
* needs write access to a directory of the chain, which the check has
|
|
34
|
+
* just found only the user has
|
|
25
35
|
*/
|
|
26
36
|
export async function sendRequest(request, requestName, expectedType, timeoutMs = getIPCRequestTimeout(), socketPath = getDaemonSocketPath()) {
|
|
37
|
+
const untrusted = secureSessionDir(path.dirname(socketPath));
|
|
38
|
+
if (untrusted) {
|
|
39
|
+
const err = untrustedSessionDirError(untrusted);
|
|
40
|
+
throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SESSION_FILE_ERROR);
|
|
41
|
+
}
|
|
27
42
|
return new Promise((resolve, reject) => {
|
|
28
43
|
const buffer = new JSONLBuffer();
|
|
29
44
|
let resolved = false;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bdg command tree: root command, its options and every registered command.
|
|
3
|
+
*/
|
|
4
|
+
import { Command, type OutputConfiguration } from 'commander';
|
|
5
|
+
/**
|
|
6
|
+
* Build the bdg program with every command registered, without parsing
|
|
7
|
+
* anything. Commander errors throw instead of exiting; the output
|
|
8
|
+
* configuration is set before commands are registered so they inherit it.
|
|
9
|
+
*
|
|
10
|
+
* @param output - Where Commander writes help and errors
|
|
11
|
+
* @returns Root command
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildProgram(output?: OutputConfiguration): Command;
|
|
14
|
+
//# sourceMappingURL=program.d.ts.map
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bdg command tree: root command, its options and every registered command.
|
|
3
|
+
*/
|
|
4
|
+
import { Command, Option } from 'commander';
|
|
5
|
+
import { commandRegistry } from './commands.js';
|
|
6
|
+
import { VERSION } from './utils/version.js';
|
|
7
|
+
const CLI_NAME = 'bdg';
|
|
8
|
+
const CLI_DESCRIPTION = 'Browser telemetry via Chrome DevTools Protocol';
|
|
9
|
+
const SESSION_OPTION_FLAGS = '--session <name>';
|
|
10
|
+
const SESSION_OPTION_DESCRIPTION = 'Use a named session (own daemon, Chrome and port) instead of the default one; env: BDG_SESSION';
|
|
11
|
+
/**
|
|
12
|
+
* Make `--debug`, `-q` and `--session` accepted after any subcommand (program
|
|
13
|
+
* options are positional).
|
|
14
|
+
*
|
|
15
|
+
* @param command - Command whose subcommands get the hidden global options
|
|
16
|
+
*/
|
|
17
|
+
function addGlobalOptions(command) {
|
|
18
|
+
for (const sub of command.commands) {
|
|
19
|
+
if (!sub.options.some((option) => option.long === '--debug')) {
|
|
20
|
+
sub.addOption(new Option('--debug', 'Enable debug logging').hideHelp());
|
|
21
|
+
}
|
|
22
|
+
if (!sub.options.some((option) => option.long === '--quiet')) {
|
|
23
|
+
sub.addOption(new Option('-q, --quiet', 'Hide tips and hints').hideHelp());
|
|
24
|
+
}
|
|
25
|
+
if (!sub.options.some((option) => option.long === '--session')) {
|
|
26
|
+
sub.addOption(new Option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION).hideHelp());
|
|
27
|
+
}
|
|
28
|
+
addGlobalOptions(sub);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the bdg program with every command registered, without parsing
|
|
33
|
+
* anything. Commander errors throw instead of exiting; the output
|
|
34
|
+
* configuration is set before commands are registered so they inherit it.
|
|
35
|
+
*
|
|
36
|
+
* @param output - Where Commander writes help and errors
|
|
37
|
+
* @returns Root command
|
|
38
|
+
*/
|
|
39
|
+
export function buildProgram(output = {}) {
|
|
40
|
+
const program = new Command()
|
|
41
|
+
.name(CLI_NAME)
|
|
42
|
+
.description(CLI_DESCRIPTION)
|
|
43
|
+
.version(VERSION)
|
|
44
|
+
.option('--debug', 'Enable debug logging (verbose output)')
|
|
45
|
+
.option(SESSION_OPTION_FLAGS, SESSION_OPTION_DESCRIPTION)
|
|
46
|
+
.enablePositionalOptions()
|
|
47
|
+
.exitOverride()
|
|
48
|
+
.configureOutput(output);
|
|
49
|
+
commandRegistry.forEach((register) => register(program));
|
|
50
|
+
addGlobalOptions(program);
|
|
51
|
+
return program;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=program.js.map
|
|
@@ -119,6 +119,29 @@ export declare const FRAME_OFFSET_JS = "(frame) => {\n const rect = frame.getBo
|
|
|
119
119
|
* its bounding box is the box itself, scaled.
|
|
120
120
|
*/
|
|
121
121
|
export declare const SCALES_ONLY_JS = "(style) => {\n if (style.rotate && style.rotate !== 'none') return false;\n if (!style.transform || style.transform === 'none') return true;\n const matrix = /^matrix\\(([^)]*)\\)$/.exec(style.transform);\n if (!matrix) return false;\n const values = matrix[1].split(',').map(parseFloat);\n return Math.abs(values[1]) < 1e-6 && Math.abs(values[2]) < 1e-6;\n}";
|
|
122
|
+
/**
|
|
123
|
+
* Page-side clip of a node by its ancestors in the flat tree (through the
|
|
124
|
+
* slots it is shown in and open shadow roots): the padding boxes
|
|
125
|
+
* ({@link CLIP_BOX_JS}) of those that cut off overflowing content and hold
|
|
126
|
+
* the node in their containing-block chain. An absolutely positioned node
|
|
127
|
+
* skips static ancestors (that are not transformed) up to its containing
|
|
128
|
+
* block, a fixed one is not clipped at all unless a transformed (or
|
|
129
|
+
* filtered, contained, …) ancestor holds it like an absolute one, and
|
|
130
|
+
* inline ancestors and `display: contents` ones have no box to clip with.
|
|
131
|
+
* The root element is left out (its overflow belongs to the viewport), and
|
|
132
|
+
* so is the body's overflow unless the root element's overflow is not
|
|
133
|
+
* `visible` (then the body keeps its own overflow and, e.g. as the page's
|
|
134
|
+
* scroller, clips like any container).
|
|
135
|
+
* Returns the clip, whether overlay scrollbars of the containers show along
|
|
136
|
+
* its right and bottom edges ({@link OVERLAY_SCROLLBARS_JS}), the innermost
|
|
137
|
+
* ancestor cutting off part of `rect` (null when none does), why the
|
|
138
|
+
* innermost clipping ancestor with no area (a collapsed
|
|
139
|
+
* `height: 0; overflow: hidden` accordion) hides it, e.g.
|
|
140
|
+
* `clipped by div#acc: zero height` (null when none has), and whether the
|
|
141
|
+
* node is fixed to the viewport (it or a container in its containing-block
|
|
142
|
+
* chain is `position: fixed`; `fixedBy` is that node).
|
|
143
|
+
*/
|
|
144
|
+
export declare const ANCESTOR_CLIP_JS: string;
|
|
122
145
|
/** Page-side test of a `clip-path` that cuts everything away: `inset()` with percentages leaving no area. */
|
|
123
146
|
export declare const CLIP_PATH_CUTS_ALL_JS = "(clipPath) => {\n const inset = /^inset\\(([^)]*)\\)/.exec(clipPath || '');\n const values = inset ? inset[1].split(' round ')[0].trim().split(/\\s+/) : [];\n if (values.length === 0 || values.some((v) => !/%$/.test(v))) return false;\n const [top, right = top, bottom = top, left = right] = values.map(parseFloat);\n return top + bottom >= 100 || left + right >= 100;\n}";
|
|
124
147
|
/**
|
|
@@ -152,28 +152,30 @@ const HOLDS_FIXED_JS = `(style) =>
|
|
|
152
152
|
/transform|filter|perspective/.test(style.willChange) || /paint|layout|strict|content/.test(style.contain)`;
|
|
153
153
|
/**
|
|
154
154
|
* Page-side test of whether a `position: fixed` node is fixed to its
|
|
155
|
-
* document's viewport (no ancestor holds it,
|
|
155
|
+
* document's viewport (no ancestor in the flat tree holds it,
|
|
156
|
+
* {@link HOLDS_FIXED_JS}).
|
|
156
157
|
*/
|
|
157
158
|
const FIXED_TO_VIEWPORT_JS = `(n) => {
|
|
158
159
|
const holdsFixed = ${HOLDS_FIXED_JS};
|
|
159
|
-
const parentOf =
|
|
160
|
+
const parentOf = ${FLAT_PARENT_JS};
|
|
160
161
|
for (let p = parentOf(n); p && p !== n.ownerDocument.documentElement; p = parentOf(p)) {
|
|
161
162
|
if (holdsFixed(p.ownerDocument.defaultView.getComputedStyle(p))) return false;
|
|
162
163
|
}
|
|
163
164
|
return true;
|
|
164
165
|
}`;
|
|
165
166
|
/**
|
|
166
|
-
* Page-side clip of a node by its ancestors
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* transformed) up to its containing
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
167
|
+
* Page-side clip of a node by its ancestors in the flat tree (through the
|
|
168
|
+
* slots it is shown in and open shadow roots): the padding boxes
|
|
169
|
+
* ({@link CLIP_BOX_JS}) of those that cut off overflowing content and hold
|
|
170
|
+
* the node in their containing-block chain. An absolutely positioned node
|
|
171
|
+
* skips static ancestors (that are not transformed) up to its containing
|
|
172
|
+
* block, a fixed one is not clipped at all unless a transformed (or
|
|
173
|
+
* filtered, contained, …) ancestor holds it like an absolute one, and
|
|
174
|
+
* inline ancestors and `display: contents` ones have no box to clip with.
|
|
175
|
+
* The root element is left out (its overflow belongs to the viewport), and
|
|
176
|
+
* so is the body's overflow unless the root element's overflow is not
|
|
177
|
+
* `visible` (then the body keeps its own overflow and, e.g. as the page's
|
|
178
|
+
* scroller, clips like any container).
|
|
177
179
|
* Returns the clip, whether overlay scrollbars of the containers show along
|
|
178
180
|
* its right and bottom edges ({@link OVERLAY_SCROLLBARS_JS}), the innermost
|
|
179
181
|
* ancestor cutting off part of `rect` (null when none does), why the
|
|
@@ -183,14 +185,14 @@ const FIXED_TO_VIEWPORT_JS = `(n) => {
|
|
|
183
185
|
* node is fixed to the viewport (it or a container in its containing-block
|
|
184
186
|
* chain is `position: fixed`; `fixedBy` is that node).
|
|
185
187
|
*/
|
|
186
|
-
const ANCESTOR_CLIP_JS = `(node, rect, describe) => {
|
|
188
|
+
export const ANCESTOR_CLIP_JS = `(node, rect, describe) => {
|
|
187
189
|
const clipBox = ${CLIP_BOX_JS};
|
|
188
190
|
const overlayScrollbars = ${OVERLAY_SCROLLBARS_JS};
|
|
189
191
|
const holdsFixed = ${HOLDS_FIXED_JS};
|
|
190
192
|
const fixedToViewport = ${FIXED_TO_VIEWPORT_JS};
|
|
191
193
|
const reasons = ${JSON.stringify(LAYOUT_REASONS)};
|
|
192
194
|
const styleOf = (n) => n.ownerDocument.defaultView.getComputedStyle(n);
|
|
193
|
-
const parentOf =
|
|
195
|
+
const parentOf = ${FLAT_PARENT_JS};
|
|
194
196
|
const doc = node.ownerDocument;
|
|
195
197
|
const rootStyle = styleOf(doc.documentElement);
|
|
196
198
|
const bodyClips = rootStyle.overflowX !== 'visible' || rootStyle.overflowY !== 'visible';
|
|
@@ -44,12 +44,14 @@ export declare const COMPOSED_JS = "(node) => {\n const composes = (n) => Boole
|
|
|
44
44
|
* `visibility: hidden` are left out, elements that are not inline are set
|
|
45
45
|
* apart by line breaks, as is a `<br>`. Fields and editable regions inside
|
|
46
46
|
* it (inputs, textareas, selects, a contenteditable editor) are skipped, so
|
|
47
|
-
* what a user typed is never read
|
|
48
|
-
*
|
|
47
|
+
* what a user typed is never read, unless `fields` is set: then selects
|
|
48
|
+
* (all their options) and editable regions are read as `innerText` reads
|
|
49
|
+
* them (inputs and textareas it leaves out too). Raw text keeps
|
|
50
|
+
* `innerText`'s collapsed whitespace. The text is cut at `limit` characters, and a
|
|
49
51
|
* part whose text alone passes the limit is read from its text nodes
|
|
50
52
|
* (`textContent`) instead of `innerText`, which would lay out all of it.
|
|
51
53
|
*/
|
|
52
|
-
export declare const FLAT_TEXT_JS = "(el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea
|
|
54
|
+
export declare const FLAT_TEXT_JS = "(el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n}";
|
|
53
55
|
/**
|
|
54
56
|
* Page-side text of an element as a user sees it: `innerText` for a rendered
|
|
55
57
|
* element (CSS-hidden parts left out, inline elements not split apart), none
|
|
@@ -65,9 +67,16 @@ export declare const FLAT_TEXT_JS = "(el, limit) => {\n const composed = (node)
|
|
|
65
67
|
* a whole page's text, unless `full` is set. Decorations are left out
|
|
66
68
|
* ({@link WITHOUT_DECORATIONS_JS}).
|
|
67
69
|
*/
|
|
68
|
-
export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea
|
|
70
|
+
export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
|
|
69
71
|
/** Shown instead of a secret field value (the same for every length) */
|
|
70
72
|
export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
|
|
73
|
+
/**
|
|
74
|
+
* Regular expression source (case-insensitive) matching names of fields that
|
|
75
|
+
* hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
|
|
76
|
+
* `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
|
|
77
|
+
* {@link SENSITIVE_FIELD_JS} and HAR sanitization.
|
|
78
|
+
*/
|
|
79
|
+
export declare const SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc";
|
|
71
80
|
/**
|
|
72
81
|
* Page-side check whether a form control holds a secret whose value must
|
|
73
82
|
* never leave the page: a password field (live type or type attribute), a
|
|
@@ -84,12 +84,14 @@ export const COMPOSED_JS = `(node) => {
|
|
|
84
84
|
* `visibility: hidden` are left out, elements that are not inline are set
|
|
85
85
|
* apart by line breaks, as is a `<br>`. Fields and editable regions inside
|
|
86
86
|
* it (inputs, textareas, selects, a contenteditable editor) are skipped, so
|
|
87
|
-
* what a user typed is never read
|
|
88
|
-
*
|
|
87
|
+
* what a user typed is never read, unless `fields` is set: then selects
|
|
88
|
+
* (all their options) and editable regions are read as `innerText` reads
|
|
89
|
+
* them (inputs and textareas it leaves out too). Raw text keeps
|
|
90
|
+
* `innerText`'s collapsed whitespace. The text is cut at `limit` characters, and a
|
|
89
91
|
* part whose text alone passes the limit is read from its text nodes
|
|
90
92
|
* (`textContent`) instead of `innerText`, which would lay out all of it.
|
|
91
93
|
*/
|
|
92
|
-
export const FLAT_TEXT_JS = `(el, limit) => {
|
|
94
|
+
export const FLAT_TEXT_JS = `(el, limit, fields) => {
|
|
93
95
|
const composed = ${COMPOSED_JS};
|
|
94
96
|
const view = el.ownerDocument.defaultView;
|
|
95
97
|
let text = '';
|
|
@@ -99,7 +101,8 @@ export const FLAT_TEXT_JS = `(el, limit) => {
|
|
|
99
101
|
if (text.length >= limit) return;
|
|
100
102
|
if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');
|
|
101
103
|
if (node.nodeType !== 1) return;
|
|
102
|
-
if (/^(input|textarea
|
|
104
|
+
if (/^(input|textarea)$/.test(node.localName)) return;
|
|
105
|
+
if (!fields && (node.localName === 'select' || node.isContentEditable)) return;
|
|
103
106
|
const style = view.getComputedStyle(node);
|
|
104
107
|
if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;
|
|
105
108
|
if (node.localName === 'br') text += '\\n';
|
|
@@ -154,6 +157,13 @@ export const ELEMENT_TEXT_JS = `(el, full) => {
|
|
|
154
157
|
}`;
|
|
155
158
|
/** Shown instead of a secret field value (the same for every length) */
|
|
156
159
|
export const MASKED_VALUE = '••••';
|
|
160
|
+
/**
|
|
161
|
+
* Regular expression source (case-insensitive) matching names of fields that
|
|
162
|
+
* hold a password, one-time code or card code: `password`, `passwd`, `pwd`,
|
|
163
|
+
* `passcode`, `otp`, `cvv`, `cvc`. Shared by the page-side
|
|
164
|
+
* {@link SENSITIVE_FIELD_JS} and HAR sanitization.
|
|
165
|
+
*/
|
|
166
|
+
export const SENSITIVE_NAME_SOURCE = 'passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc';
|
|
157
167
|
/**
|
|
158
168
|
* Page-side check whether a form control holds a secret whose value must
|
|
159
169
|
* never leave the page: a password field (live type or type attribute), a
|
|
@@ -168,7 +178,7 @@ export const SENSITIVE_FIELD_JS = `(el) => {
|
|
|
168
178
|
if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
|
|
169
179
|
if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
|
|
170
180
|
const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
|
|
171
|
-
if (/
|
|
181
|
+
if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
|
|
172
182
|
try {
|
|
173
183
|
const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
|
|
174
184
|
return Boolean(security) && security !== 'none';
|
|
@@ -26,6 +26,8 @@ export interface EvalResult {
|
|
|
26
26
|
type: string;
|
|
27
27
|
/** Object subtype (`node`, `date`, `map`, `array`, ...) */
|
|
28
28
|
subtype?: string;
|
|
29
|
+
/** Elements of an array result in the page (its copy holds at most 1000 unless `full`) */
|
|
30
|
+
length?: number;
|
|
29
31
|
/** Set when the page replaced built-ins bdg's copy of the result uses, so the browser copied it */
|
|
30
32
|
warning?: string;
|
|
31
33
|
}
|
|
@@ -36,11 +38,27 @@ export interface EvalResult {
|
|
|
36
38
|
* dates ISO strings, maps and sets entries, errors their message, BigInts
|
|
37
39
|
* `12n`, functions `function name()`, and cycles `[Circular]` (an object
|
|
38
40
|
* shared by two properties is copied twice). Works for objects of iframes
|
|
39
|
-
* (other realms); lists and objects are cut after 1000 entries
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* (other realms); lists and objects are cut after 1000 entries and levels
|
|
42
|
+
* below the 20th become `[…]` (with `full`: no entries are cut, levels
|
|
43
|
+
* below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
|
|
44
|
+
* page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
|
|
45
|
+
* left them alone.
|
|
46
|
+
*
|
|
47
|
+
* @param full - `--full`: copy every entry
|
|
48
|
+
* @returns Function declaration for Runtime.callFunctionOn
|
|
49
|
+
*/
|
|
50
|
+
export declare function jsonSafeCopyFunction(full?: boolean): string;
|
|
51
|
+
/**
|
|
52
|
+
* Elements of an array result (also a node list or typed array), read from
|
|
53
|
+
* its description (`Array(20000)`, `NodeList(5)`), since its copy holds
|
|
54
|
+
* at most 1000 unless `--full`.
|
|
55
|
+
*
|
|
56
|
+
* @param remote - Remote object returned by Runtime.evaluate
|
|
57
|
+
* @returns `length`, when the result is an array
|
|
42
58
|
*/
|
|
43
|
-
export declare
|
|
59
|
+
export declare function arrayLength(remote: Protocol.Runtime.RemoteObject): {
|
|
60
|
+
length?: number;
|
|
61
|
+
};
|
|
44
62
|
/** What a busy target is called in messages: the page, or an iframe (`--frame`) */
|
|
45
63
|
export type BusyScope = 'page' | 'frame';
|
|
46
64
|
/** When {@link withBusyPageRecovery} checks the target, and what it calls it */
|
|
@@ -100,6 +118,8 @@ export interface EvalTarget {
|
|
|
100
118
|
recovery?: CDPSender;
|
|
101
119
|
/** Execution context of a frame (`ExecutionContextDescription.uniqueId`) */
|
|
102
120
|
uniqueContextId?: string;
|
|
121
|
+
/** `--full`: copy the result with every entry */
|
|
122
|
+
full?: boolean;
|
|
103
123
|
}
|
|
104
124
|
/**
|
|
105
125
|
* Whether the page still answers. A page that does not answer in time is
|
|
@@ -156,6 +156,10 @@ const DESCRIBED_SUBTYPES = new Set([
|
|
|
156
156
|
'iterator',
|
|
157
157
|
'generator',
|
|
158
158
|
]);
|
|
159
|
+
/** Entries per list or object, and levels, of a copied eval result */
|
|
160
|
+
const COPY_LIMITS = { maxItems: 1000, maxDepth: 20 };
|
|
161
|
+
/** Limits of a copied eval result with `--full`: every entry, levels deep enough for real data */
|
|
162
|
+
const FULL_COPY_LIMITS = { maxItems: Infinity, maxDepth: 100 };
|
|
159
163
|
/** Object subtypes shown by their short description (`button#submit`, `ArrayBuffer(8)`). */
|
|
160
164
|
const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
|
|
161
165
|
/**
|
|
@@ -165,12 +169,19 @@ const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
|
|
|
165
169
|
* dates ISO strings, maps and sets entries, errors their message, BigInts
|
|
166
170
|
* `12n`, functions `function name()`, and cycles `[Circular]` (an object
|
|
167
171
|
* shared by two properties is copied twice). Works for objects of iframes
|
|
168
|
-
* (other realms); lists and objects are cut after 1000 entries
|
|
169
|
-
*
|
|
170
|
-
*
|
|
172
|
+
* (other realms); lists and objects are cut after 1000 entries and levels
|
|
173
|
+
* below the 20th become `[…]` (with `full`: no entries are cut, levels
|
|
174
|
+
* below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
|
|
175
|
+
* page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
|
|
176
|
+
* left them alone.
|
|
177
|
+
*
|
|
178
|
+
* @param full - `--full`: copy every entry
|
|
179
|
+
* @returns Function declaration for Runtime.callFunctionOn
|
|
171
180
|
*/
|
|
172
|
-
export
|
|
173
|
-
|
|
181
|
+
export function jsonSafeCopyFunction(full = false) {
|
|
182
|
+
const limits = full ? FULL_COPY_LIMITS : COPY_LIMITS;
|
|
183
|
+
return `function () {
|
|
184
|
+
const MAX_ITEMS = ${limits.maxItems};
|
|
174
185
|
const ancestors = new Set();
|
|
175
186
|
const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);
|
|
176
187
|
const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
|
|
@@ -209,7 +220,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
|
|
|
209
220
|
const copy = (value, depth) => {
|
|
210
221
|
if (value === null || typeof value !== 'object') return leaf(value);
|
|
211
222
|
if (ancestors.has(value)) return '[Circular]';
|
|
212
|
-
if (depth >
|
|
223
|
+
if (depth > ${limits.maxDepth}) return '[…]';
|
|
213
224
|
const next = (item) => copy(item, depth + 1);
|
|
214
225
|
const type = kind(value);
|
|
215
226
|
if (isNode(value)) return describeNode(value);
|
|
@@ -236,6 +247,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
|
|
|
236
247
|
};
|
|
237
248
|
return copy(this, 0);
|
|
238
249
|
}`;
|
|
250
|
+
}
|
|
239
251
|
/** Built-ins {@link JSON_SAFE_COPY_FUNCTION} uses (it runs in the page's world, next to the result) */
|
|
240
252
|
const COPY_BUILTINS = [
|
|
241
253
|
'Object.keys',
|
|
@@ -284,9 +296,10 @@ function isTerminated(details) {
|
|
|
284
296
|
*
|
|
285
297
|
* @param cdp - CDP connection
|
|
286
298
|
* @param remote - Remote object returned by Runtime.evaluate
|
|
299
|
+
* @param full - `--full`: copy every entry ({@link jsonSafeCopyFunction})
|
|
287
300
|
* @returns Value and type
|
|
288
301
|
*/
|
|
289
|
-
async function toEvalResult(cdp, remote) {
|
|
302
|
+
async function toEvalResult(cdp, remote, full) {
|
|
290
303
|
const kind = { type: remote.type, ...(remote.subtype && { subtype: remote.subtype }) };
|
|
291
304
|
if (remote.unserializableValue !== undefined)
|
|
292
305
|
return { value: remote.unserializableValue, ...kind };
|
|
@@ -301,12 +314,27 @@ async function toEvalResult(cdp, remote) {
|
|
|
301
314
|
return { value: formatRemoteObject(remote), ...kind };
|
|
302
315
|
}
|
|
303
316
|
const replaced = await findReplacedBuiltins(cdp, COPY_BUILTINS, remote.objectId);
|
|
304
|
-
const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ?
|
|
317
|
+
const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? jsonSafeCopyFunction(full) : SELF_FUNCTION);
|
|
305
318
|
const value = copy ? copy.value : formatRemoteObject(remote);
|
|
319
|
+
const copied = { value, ...kind, ...arrayLength(remote) };
|
|
306
320
|
if (replaced.length === 0)
|
|
307
|
-
return
|
|
321
|
+
return copied;
|
|
308
322
|
const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
|
|
309
|
-
return {
|
|
323
|
+
return { ...copied, warning };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Elements of an array result (also a node list or typed array), read from
|
|
327
|
+
* its description (`Array(20000)`, `NodeList(5)`), since its copy holds
|
|
328
|
+
* at most 1000 unless `--full`.
|
|
329
|
+
*
|
|
330
|
+
* @param remote - Remote object returned by Runtime.evaluate
|
|
331
|
+
* @returns `length`, when the result is an array
|
|
332
|
+
*/
|
|
333
|
+
export function arrayLength(remote) {
|
|
334
|
+
if (remote.subtype !== 'array' && remote.subtype !== 'typedarray')
|
|
335
|
+
return {};
|
|
336
|
+
const match = /\((\d+)\)$/.exec(remote.description ?? '');
|
|
337
|
+
return match ? { length: Number(match[1]) } : {};
|
|
310
338
|
}
|
|
311
339
|
/**
|
|
312
340
|
* Copy a result by value: what a function called on it returns, which the
|
|
@@ -314,7 +342,7 @@ async function toEvalResult(cdp, remote) {
|
|
|
314
342
|
*
|
|
315
343
|
* @param cdp - CDP connection
|
|
316
344
|
* @param objectId - The result
|
|
317
|
-
* @param functionDeclaration - {@link
|
|
345
|
+
* @param functionDeclaration - {@link jsonSafeCopyFunction}'s, or
|
|
318
346
|
* {@link SELF_FUNCTION} for the browser's plain copy (it fails on cycles
|
|
319
347
|
* and BigInts)
|
|
320
348
|
* @returns The copy, or undefined when it failed
|
|
@@ -560,7 +588,7 @@ export async function evaluateScript(cdp, script, target = {}) {
|
|
|
560
588
|
try {
|
|
561
589
|
const response = await withDeadline(executeScript(session, script, evaluateOptions(target)), EVAL_TIMEOUT_MS + TERMINATION_GRACE_MS, () => evaluationTimeoutError(target.recovery ?? session, scope));
|
|
562
590
|
const settled = await withDeadline(settlePromise(session, response.result, script), EVAL_TIMEOUT_MS, promiseTimeout);
|
|
563
|
-
return await toEvalResult(session, settled);
|
|
591
|
+
return await toEvalResult(session, settled, target.full ?? false);
|
|
564
592
|
}
|
|
565
593
|
catch (error) {
|
|
566
594
|
throw scope === 'page' && isContextLostError(error)
|
|
@@ -66,6 +66,13 @@ export declare function mapPoint(mapping: FrameMapping, point: LayoutPoint): Lay
|
|
|
66
66
|
* @returns Box in the top-level viewport
|
|
67
67
|
*/
|
|
68
68
|
export declare function mapBox(mapping: FrameMapping, box: LayoutBox): LayoutBox;
|
|
69
|
+
/**
|
|
70
|
+
* Page-side choice of the box to measure: the element's, or its document's
|
|
71
|
+
* root element when the element has no size (hidden, collapsed).
|
|
72
|
+
*/
|
|
73
|
+
export declare const REFERENCE_NODE_FUNCTION = "function () {\n const r = this.getBoundingClientRect();\n return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;\n}";
|
|
74
|
+
/** Page-side `getBoundingClientRect()` as a plain box */
|
|
75
|
+
export declare const CLIENT_RECT_FUNCTION = "function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }";
|
|
69
76
|
/**
|
|
70
77
|
* How the element's frame maps into the top-level viewport.
|
|
71
78
|
*
|
|
@@ -150,12 +150,12 @@ export function mapBox(mapping, box) {
|
|
|
150
150
|
* Page-side choice of the box to measure: the element's, or its document's
|
|
151
151
|
* root element when the element has no size (hidden, collapsed).
|
|
152
152
|
*/
|
|
153
|
-
const REFERENCE_NODE_FUNCTION = `function () {
|
|
153
|
+
export const REFERENCE_NODE_FUNCTION = `function () {
|
|
154
154
|
const r = this.getBoundingClientRect();
|
|
155
155
|
return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;
|
|
156
156
|
}`;
|
|
157
157
|
/** Page-side `getBoundingClientRect()` as a plain box */
|
|
158
|
-
const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
|
|
158
|
+
export const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
|
|
159
159
|
/**
|
|
160
160
|
* Measure a box of the element's frame both in the frame and through CDP.
|
|
161
161
|
*
|