browser-debugger-cli 0.9.0 → 0.11.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 +268 -0
- package/README.md +15 -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/installSkill.d.ts +20 -0
- package/dist/commands/installSkill.js +87 -0
- 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/commands.js +3 -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 +85 -0
- package/dist/errors/messages.js +128 -1
- package/dist/index.js +2151 -960
- 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 +40 -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/installSkill.d.ts +11 -0
- package/dist/ui/formatters/installSkill.js +31 -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 +2 -1
|
@@ -17,14 +17,21 @@ import { getSessionName } from '../../session/paths.js';
|
|
|
17
17
|
import { OutputBuilder, buildSuccessResponse } from '../../ui/OutputBuilder.js';
|
|
18
18
|
import { escapeControlChars, joinLines } from '../../ui/formatting.js';
|
|
19
19
|
import { createLogger } from '../../ui/logging/index.js';
|
|
20
|
-
import { startNotices } from '../../ui/messages/session.js';
|
|
20
|
+
import { daemonStillExitingHint, daemonStillExitingSuggestion, startNotices, } from '../../ui/messages/session.js';
|
|
21
21
|
import { noActiveSessionMessage } from '../../ui/messages/sessionCommand.js';
|
|
22
|
-
import { delay } from '../../utils/async.js';
|
|
22
|
+
import { delay, waitUntil } from '../../utils/async.js';
|
|
23
23
|
import { getExitCodeForIPCError } from '../../utils/errorMapping.js';
|
|
24
24
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
25
25
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
26
26
|
import { filterDefined } from '../../utils/objects.js';
|
|
27
27
|
const log = createLogger('bdg');
|
|
28
|
+
/** How long a failed start waits for the daemons it spawned to exit */
|
|
29
|
+
export const SPAWNED_DAEMON_EXIT_WAIT_MS = 3000;
|
|
30
|
+
const DEFAULT_START_DEPS = {
|
|
31
|
+
launch: launchDaemon,
|
|
32
|
+
send: sendStartSessionRequest,
|
|
33
|
+
exitWaitMs: SPAWNED_DAEMON_EXIT_WAIT_MS,
|
|
34
|
+
};
|
|
28
35
|
/**
|
|
29
36
|
* Start a session via the daemon and report the result.
|
|
30
37
|
*
|
|
@@ -38,13 +45,69 @@ const log = createLogger('bdg');
|
|
|
38
45
|
export async function startSessionViaDaemon(url, options, telemetry) {
|
|
39
46
|
process.once('SIGINT', () => reportStartOutcome(interruptedOutcome('SIGINT'), options));
|
|
40
47
|
process.once('SIGTERM', () => reportStartOutcome(interruptedOutcome('SIGTERM'), options));
|
|
41
|
-
|
|
48
|
+
reportStartOutcome(await attemptStart(url, options, telemetry), options);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Start a session, retrying while the previous session shuts down. After a
|
|
52
|
+
* failure the daemon reported (or a dropped connection), it waits for every
|
|
53
|
+
* daemon the attempts spawned to exit ({@link afterSpawnedDaemonExit}).
|
|
54
|
+
*
|
|
55
|
+
* @param url - Target URL
|
|
56
|
+
* @param options - Session options
|
|
57
|
+
* @param telemetry - Telemetry types
|
|
58
|
+
* @param deps - How to reach the daemon (tests replace it)
|
|
59
|
+
* @returns Start outcome, ready to report
|
|
60
|
+
*/
|
|
61
|
+
export async function attemptStart(url, options, telemetry, deps = DEFAULT_START_DEPS) {
|
|
62
|
+
const spawned = [];
|
|
63
|
+
const attempt = async () => {
|
|
64
|
+
const outcome = await requestSession(url, options, telemetry, deps);
|
|
65
|
+
if (!outcome.ok && outcome.spawned)
|
|
66
|
+
spawned.push(outcome.spawned);
|
|
67
|
+
return outcome;
|
|
68
|
+
};
|
|
69
|
+
let outcome = await attempt();
|
|
42
70
|
const deadline = Date.now() + SHUTDOWN_WAIT_MS;
|
|
43
71
|
while (isShuttingDown(outcome) && Date.now() < deadline) {
|
|
44
72
|
await delay(SHUTDOWN_POLL_MS);
|
|
45
|
-
outcome = await
|
|
73
|
+
outcome = await attempt();
|
|
74
|
+
}
|
|
75
|
+
return afterSpawnedDaemonExit(outcome, spawned, deps.exitWaitMs);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Let the daemons a failed start spawned finish exiting before the error is
|
|
79
|
+
* reported: a daemon removes its session files on the way out, and a command
|
|
80
|
+
* run right after (`bdg sessions`, another start) would otherwise still see
|
|
81
|
+
* the session as starting. The wait is bounded; a daemon still running after
|
|
82
|
+
* it is reported (`details.daemonStillRunning`, `daemonPid`, a suggestion).
|
|
83
|
+
*
|
|
84
|
+
* @param outcome - Start outcome
|
|
85
|
+
* @param spawned - Exiting daemons the attempts spawned
|
|
86
|
+
* @param waitMs - Milliseconds to wait at most for all of them
|
|
87
|
+
* @returns The failure (without internal fields), with a hint when a daemon did not exit in time
|
|
88
|
+
*/
|
|
89
|
+
export async function afterSpawnedDaemonExit(outcome, spawned, waitMs) {
|
|
90
|
+
if (outcome.ok)
|
|
91
|
+
return outcome;
|
|
92
|
+
const { spawned: _spawned, daemonExiting: _exiting, ...failure } = outcome;
|
|
93
|
+
if (spawned.length === 0)
|
|
94
|
+
return failure;
|
|
95
|
+
if (await waitUntil(() => spawned.every((daemon) => daemon.hasExited()), waitMs)) {
|
|
96
|
+
return failure;
|
|
46
97
|
}
|
|
47
|
-
|
|
98
|
+
const running = spawned.filter((daemon) => !daemon.hasExited()).pop();
|
|
99
|
+
const suggestion = daemonStillExitingSuggestion();
|
|
100
|
+
const previous = failure.details?.['suggestion'];
|
|
101
|
+
return {
|
|
102
|
+
...failure,
|
|
103
|
+
human: joinLines(failure.human, `${daemonStillExitingHint(running?.pid, waitMs)}; ${suggestion}`),
|
|
104
|
+
details: {
|
|
105
|
+
...failure.details,
|
|
106
|
+
daemonStillRunning: true,
|
|
107
|
+
...(running?.pid !== undefined && { daemonPid: running.pid }),
|
|
108
|
+
suggestion: typeof previous === 'string' && previous ? `${previous}; ${suggestion}` : suggestion,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
48
111
|
}
|
|
49
112
|
/**
|
|
50
113
|
* The outcome of a start interrupted with Ctrl-C (the daemon notices the
|
|
@@ -83,9 +146,10 @@ function isShuttingDown(outcome) {
|
|
|
83
146
|
* @param telemetry - Telemetry types
|
|
84
147
|
* @returns Start outcome
|
|
85
148
|
*/
|
|
86
|
-
async function requestSession(url, options, telemetry) {
|
|
149
|
+
async function requestSession(url, options, telemetry, deps) {
|
|
150
|
+
let spawned;
|
|
87
151
|
try {
|
|
88
|
-
await
|
|
152
|
+
spawned = await deps.launch();
|
|
89
153
|
}
|
|
90
154
|
catch (error) {
|
|
91
155
|
if (error instanceof SessionDirError) {
|
|
@@ -101,9 +165,24 @@ async function requestSession(url, options, telemetry) {
|
|
|
101
165
|
const exitCode = error instanceof DaemonError ? error.exitCode : EXIT_CODES.SOFTWARE_ERROR;
|
|
102
166
|
return { ok: false, error: message, human: genericError(message), exitCode };
|
|
103
167
|
}
|
|
168
|
+
const outcome = await sendStart(url, options, telemetry, deps.send);
|
|
169
|
+
return !outcome.ok && outcome.daemonExiting && spawned ? { ...outcome, spawned } : outcome;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Ask the running daemon to start a session.
|
|
173
|
+
*
|
|
174
|
+
* @param url - Target URL
|
|
175
|
+
* @param options - Session options
|
|
176
|
+
* @param telemetry - Telemetry types
|
|
177
|
+
* @param send - Sends the request
|
|
178
|
+
* @returns Start outcome; `daemonExiting` for a failure the daemon reported
|
|
179
|
+
* or a dropped connection, not for a timeout or an unexpected error (the
|
|
180
|
+
* daemon may still be starting the session then)
|
|
181
|
+
*/
|
|
182
|
+
async function sendStart(url, options, telemetry, send) {
|
|
104
183
|
try {
|
|
105
184
|
log.debug('Connecting to daemon...');
|
|
106
|
-
const response = await
|
|
185
|
+
const response = await send(url, filterDefined({
|
|
107
186
|
port: options.port,
|
|
108
187
|
timeout: options.timeout,
|
|
109
188
|
telemetry: telemetry.length > 0 ? telemetry : undefined,
|
|
@@ -116,8 +195,9 @@ async function requestSession(url, options, telemetry) {
|
|
|
116
195
|
viewport: options.viewport,
|
|
117
196
|
colorScheme: options.colorScheme,
|
|
118
197
|
}));
|
|
119
|
-
if (response.status === 'error')
|
|
120
|
-
return describeStartFailure(response, options);
|
|
198
|
+
if (response.status === 'error') {
|
|
199
|
+
return { ...describeStartFailure(response, options), daemonExiting: true };
|
|
200
|
+
}
|
|
121
201
|
if (!response.data) {
|
|
122
202
|
const message = 'Invalid response from daemon: missing data';
|
|
123
203
|
return {
|
|
@@ -147,6 +227,7 @@ async function requestSession(url, options, telemetry) {
|
|
|
147
227
|
human: daemonNotRunningError({ suggestStatus: true, suggestRetry: true }),
|
|
148
228
|
exitCode: EXIT_CODES.RESOURCE_NOT_FOUND,
|
|
149
229
|
retryable: true,
|
|
230
|
+
daemonExiting: true,
|
|
150
231
|
};
|
|
151
232
|
}
|
|
152
233
|
const message = getErrorMessage(error);
|
|
@@ -57,5 +57,16 @@ export declare function consoleLevelOption(value: string): ConsoleLevel;
|
|
|
57
57
|
* @throws InvalidArgumentError (exit 81) when no type is given
|
|
58
58
|
*/
|
|
59
59
|
export declare function eventTypesOption(value: string, previous?: string[]): string[];
|
|
60
|
+
/**
|
|
61
|
+
* Commander parser for `dom inspect --props`: comma-separated CSS property
|
|
62
|
+
* names, lowercased except custom properties (`--brand`), which are
|
|
63
|
+
* case-sensitive; repeated options add up.
|
|
64
|
+
*
|
|
65
|
+
* @param value - Raw option value, e.g. "padding-top,color"
|
|
66
|
+
* @param previous - Names from earlier `--props` options
|
|
67
|
+
* @returns Property names
|
|
68
|
+
* @throws InvalidArgumentError (exit 81) when no name is given
|
|
69
|
+
*/
|
|
70
|
+
export declare function cssPropertiesOption(value: string, previous?: string[]): string[];
|
|
60
71
|
export declare function resourceTypeRule(): ValidationRule<Protocol.Network.ResourceType[]>;
|
|
61
72
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -158,6 +158,22 @@ export function eventTypesOption(value, previous = []) {
|
|
|
158
158
|
return [...new Set([...previous, ...types])];
|
|
159
159
|
throw new InvalidArgumentError('Give at least one event type, e.g. click or click,keydown.');
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Commander parser for `dom inspect --props`: comma-separated CSS property
|
|
163
|
+
* names, lowercased except custom properties (`--brand`), which are
|
|
164
|
+
* case-sensitive; repeated options add up.
|
|
165
|
+
*
|
|
166
|
+
* @param value - Raw option value, e.g. "padding-top,color"
|
|
167
|
+
* @param previous - Names from earlier `--props` options
|
|
168
|
+
* @returns Property names
|
|
169
|
+
* @throws InvalidArgumentError (exit 81) when no name is given
|
|
170
|
+
*/
|
|
171
|
+
export function cssPropertiesOption(value, previous = []) {
|
|
172
|
+
const names = parseCommaSeparated(value).map((name) => name.startsWith('--') ? name : name.toLowerCase());
|
|
173
|
+
if (names.length > 0)
|
|
174
|
+
return [...new Set([...previous, ...names])];
|
|
175
|
+
throw new InvalidArgumentError('Give at least one CSS property, e.g. color or padding-top,color.');
|
|
176
|
+
}
|
|
161
177
|
function parseCommaSeparated(value) {
|
|
162
178
|
return value
|
|
163
179
|
.split(',')
|
package/dist/commands.js
CHANGED
|
@@ -4,6 +4,7 @@ import { registerConsoleCommand } from './commands/console.js';
|
|
|
4
4
|
import { registerDetailsCommand } from './commands/details.js';
|
|
5
5
|
import { registerFormInteractionCommands } from './commands/dom/formInteraction.js';
|
|
6
6
|
import { registerDomCommands } from './commands/dom/index.js';
|
|
7
|
+
import { registerInstallSkillCommand } from './commands/installSkill.js';
|
|
7
8
|
import { registerNetworkCommands } from './commands/network/index.js';
|
|
8
9
|
import { registerPageCommands } from './commands/page.js';
|
|
9
10
|
import { registerPeekCommand } from './commands/peek.js';
|
|
@@ -44,5 +45,7 @@ export const commandRegistry = [
|
|
|
44
45
|
registerNetworkCommands,
|
|
45
46
|
addCommandGroup('Console Commands:'),
|
|
46
47
|
registerConsoleCommand,
|
|
48
|
+
addCommandGroup('Agent Setup:'),
|
|
49
|
+
registerInstallSkillCommand,
|
|
47
50
|
];
|
|
48
51
|
//# sourceMappingURL=commands.js.map
|
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
* SocketServer), so concurrent launches are safe: a losing daemon exits and
|
|
7
7
|
* the CLI talks to the winner.
|
|
8
8
|
*/
|
|
9
|
+
/** A daemon this process spawned */
|
|
10
|
+
export interface SpawnedDaemon {
|
|
11
|
+
pid: number | undefined;
|
|
12
|
+
/** Whether it has exited (from the child process's exit event) */
|
|
13
|
+
hasExited: () => boolean;
|
|
14
|
+
}
|
|
9
15
|
/**
|
|
10
16
|
* Ensure a daemon is running, spawning one if needed.
|
|
11
17
|
*
|
|
18
|
+
* @returns The daemon it spawned, or undefined when one was already running
|
|
12
19
|
* @throws DaemonStartupError if the daemon script is missing or the daemon
|
|
13
20
|
* does not accept connections in time
|
|
14
21
|
*/
|
|
15
|
-
export declare function launchDaemon(): Promise<
|
|
22
|
+
export declare function launchDaemon(): Promise<SpawnedDaemon | undefined>;
|
|
16
23
|
/**
|
|
17
24
|
* Check that the session directory can hold the daemon's files before
|
|
18
25
|
* spawning it (otherwise the daemon dies and only its log says why).
|
package/dist/daemon/launcher.js
CHANGED
|
@@ -24,13 +24,14 @@ const DAEMON_READY_POLL_MS = 20;
|
|
|
24
24
|
/**
|
|
25
25
|
* Ensure a daemon is running, spawning one if needed.
|
|
26
26
|
*
|
|
27
|
+
* @returns The daemon it spawned, or undefined when one was already running
|
|
27
28
|
* @throws DaemonStartupError if the daemon script is missing or the daemon
|
|
28
29
|
* does not accept connections in time
|
|
29
30
|
*/
|
|
30
31
|
export async function launchDaemon() {
|
|
31
32
|
if (await isDaemonAlive()) {
|
|
32
33
|
log.debug('Daemon already running');
|
|
33
|
-
return;
|
|
34
|
+
return undefined;
|
|
34
35
|
}
|
|
35
36
|
if (!fs.existsSync(DAEMON_SCRIPT_PATH)) {
|
|
36
37
|
throw new DaemonStartupError(`Daemon script not found at ${DAEMON_SCRIPT_PATH}. Did you run 'npm run build'?`, 'DAEMON_SCRIPT_NOT_FOUND');
|
|
@@ -51,6 +52,7 @@ export async function launchDaemon() {
|
|
|
51
52
|
});
|
|
52
53
|
daemon.unref();
|
|
53
54
|
await waitForDaemonReady(() => exited);
|
|
55
|
+
return { pid: daemon.pid, hasExited: () => exited };
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Check that the session directory can hold the daemon's files before
|
|
@@ -99,6 +99,13 @@ export declare class Session {
|
|
|
99
99
|
* @returns Session metadata
|
|
100
100
|
*/
|
|
101
101
|
metadata(): SessionStatusMetadata;
|
|
102
|
+
/**
|
|
103
|
+
* Record a page emulation changed mid-session, so screenshots restore it
|
|
104
|
+
* and `bdg status` reports it.
|
|
105
|
+
*
|
|
106
|
+
* @param emulation - Viewport and color scheme now emulated
|
|
107
|
+
*/
|
|
108
|
+
private setEmulation;
|
|
102
109
|
/**
|
|
103
110
|
* Whether stop() has been called.
|
|
104
111
|
*
|
|
@@ -21,6 +21,7 @@ import { getSessionPort } from '../../session/port.js';
|
|
|
21
21
|
import { createLogger } from '../../ui/logging/index.js';
|
|
22
22
|
import { formatChromeNotice } from '../../ui/messages/chrome.js';
|
|
23
23
|
import { delay } from '../../utils/async.js';
|
|
24
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
24
25
|
import { EXIT_CODES } from '../../utils/exitCodes.js';
|
|
25
26
|
import { filterDefined } from '../../utils/objects.js';
|
|
26
27
|
import { isProcessAlive } from '../../utils/process.js';
|
|
@@ -55,7 +56,10 @@ export class Session {
|
|
|
55
56
|
config;
|
|
56
57
|
onEnded;
|
|
57
58
|
store = new TelemetryStore();
|
|
58
|
-
registry = createCommandRegistry(this.store
|
|
59
|
+
registry = createCommandRegistry(this.store, {
|
|
60
|
+
get: () => filterDefined({ viewport: this.config.viewport, colorScheme: this.config.colorScheme }),
|
|
61
|
+
set: (emulation) => this.setEmulation(emulation),
|
|
62
|
+
});
|
|
59
63
|
notify = (notice) => log.info(formatChromeNotice(notice));
|
|
60
64
|
chrome = null;
|
|
61
65
|
cdp = null;
|
|
@@ -174,6 +178,24 @@ export class Session {
|
|
|
174
178
|
}),
|
|
175
179
|
};
|
|
176
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Record a page emulation changed mid-session, so screenshots restore it
|
|
183
|
+
* and `bdg status` reports it.
|
|
184
|
+
*
|
|
185
|
+
* @param emulation - Viewport and color scheme now emulated
|
|
186
|
+
*/
|
|
187
|
+
setEmulation(emulation) {
|
|
188
|
+
const { viewport: _viewport, colorScheme: _colorScheme, ...rest } = this.config;
|
|
189
|
+
this.config = { ...rest, ...emulation };
|
|
190
|
+
if (!this.started)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
writeSessionMetadata(this.metadata());
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
log.debug(`Session metadata not updated: ${getErrorMessage(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
177
199
|
/**
|
|
178
200
|
* Whether stop() has been called.
|
|
179
201
|
*
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import type { TelemetryStore } from './TelemetryStore.js';
|
|
2
2
|
import type { CDPConnection } from '../../connection/cdp.js';
|
|
3
3
|
import type { CommandName, CommandSchemas } from '../../ipc/index.js';
|
|
4
|
+
import { type SessionEmulation } from '../../runtime/page/emulation.js';
|
|
4
5
|
type Handler<K extends CommandName> = (cdp: CDPConnection, params: CommandSchemas[K]['requestSchema']) => Promise<CommandSchemas[K]['responseSchema']>;
|
|
5
6
|
export type CommandRegistry = {
|
|
6
7
|
[K in CommandName]: Handler<K>;
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
/** The session's page emulation, which `page emulate` reads and changes */
|
|
10
|
+
export interface EmulationState {
|
|
11
|
+
get: () => SessionEmulation;
|
|
12
|
+
set: (emulation: SessionEmulation) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The handlers of the session commands.
|
|
16
|
+
*
|
|
17
|
+
* @param store - Telemetry of the session
|
|
18
|
+
* @param emulation - The session's page emulation
|
|
19
|
+
* @returns Command registry
|
|
20
|
+
*/
|
|
21
|
+
export declare function createCommandRegistry(store: TelemetryStore, emulation: EmulationState): CommandRegistry;
|
|
9
22
|
export {};
|
|
10
23
|
//# sourceMappingURL=commandRegistry.d.ts.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { CDPConnectionError } from '../../connection/errors.js';
|
|
1
|
+
import { CDPConnectionError, CDPProtocolError } from '../../connection/errors.js';
|
|
2
2
|
import { PatternDetector } from '../patternDetector.js';
|
|
3
3
|
import { createInteractionRunner } from './interactions.js';
|
|
4
4
|
import { withTriggeredRequestCount } from './triggeredRequests.js';
|
|
5
5
|
import { CommandError } from '../../errors/index.js';
|
|
6
|
-
import { formDiscoveryFailedError } from '../../errors/messages.js';
|
|
6
|
+
import { cdpCallError, formDiscoveryFailedError } from '../../errors/messages.js';
|
|
7
7
|
import { evaluateScript, withBusyPageRecovery } from '../../runtime/dom/evalHelpers.js';
|
|
8
8
|
import { inspectEventListeners } from '../../runtime/dom/eventListeners.js';
|
|
9
9
|
import { FORM_DISCOVERY_SCRIPT, isRawFormData } from '../../runtime/dom/formDiscovery.js';
|
|
@@ -11,10 +11,11 @@ import { fillElement, clickElement, pressKeyElement, scrollPage, withActionStabi
|
|
|
11
11
|
import { exceptionSummary } from '../../runtime/dom/formFillHelpers/shared.js';
|
|
12
12
|
import { submitForm } from '../../runtime/dom/formSubmitHelpers.js';
|
|
13
13
|
import { evaluateInFrame, listFrames } from '../../runtime/dom/frames.js';
|
|
14
|
+
import { inspectElement } from '../../runtime/dom/inspect.js';
|
|
14
15
|
import { inspectLayout } from '../../runtime/dom/layout.js';
|
|
15
16
|
import { onScriptTarget } from '../../runtime/dom/targetNode.js';
|
|
16
17
|
import { waitForCondition } from '../../runtime/dom/wait.js';
|
|
17
|
-
import { pageAppearance } from '../../runtime/page/emulation.js';
|
|
18
|
+
import { emulatePage, pageAppearance } from '../../runtime/page/emulation.js';
|
|
18
19
|
import { readDocumentReadyState } from '../../runtime/page/loadingState.js';
|
|
19
20
|
import { navigatePage } from '../../runtime/page/navigation.js';
|
|
20
21
|
import { skippedBodyReason } from '../../telemetry/network.js';
|
|
@@ -62,6 +63,8 @@ function mapNetworkRequestToPreview(req, withHeaders = false) {
|
|
|
62
63
|
return filterDefined({
|
|
63
64
|
requestId: req.requestId,
|
|
64
65
|
timestamp: req.timestamp,
|
|
66
|
+
sentTime: req.sentTime,
|
|
67
|
+
navigationId: req.navigationId,
|
|
65
68
|
method: req.method,
|
|
66
69
|
url: req.url,
|
|
67
70
|
status: req.status,
|
|
@@ -260,9 +263,45 @@ function pageWebSocketUrl(store) {
|
|
|
260
263
|
throw new CDPConnectionError('No page target');
|
|
261
264
|
return url;
|
|
262
265
|
}
|
|
263
|
-
|
|
266
|
+
/** Chrome's "server error" code, used for failures on the page's state (a missing node, a bad id…) */
|
|
267
|
+
const CDP_SERVER_ERROR = -32000;
|
|
268
|
+
/**
|
|
269
|
+
* A `bdg cdp` failure that is the caller's: wrong parameters (81), or an
|
|
270
|
+
* id of a node, target or frame that does not exist (83). Other failures
|
|
271
|
+
* (internal errors, a detached page) stay software errors.
|
|
272
|
+
*
|
|
273
|
+
* @param method - CDP method
|
|
274
|
+
* @param error - What the call threw
|
|
275
|
+
* @returns The error to report, or undefined to keep the original
|
|
276
|
+
*/
|
|
277
|
+
function callerError(method, error) {
|
|
278
|
+
if (!(error instanceof CDPProtocolError))
|
|
279
|
+
return undefined;
|
|
280
|
+
const err = cdpCallError(method, error.message);
|
|
281
|
+
if (error.isRequestError()) {
|
|
282
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
283
|
+
}
|
|
284
|
+
if (error.code === CDP_SERVER_ERROR && err.notFound) {
|
|
285
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
|
|
286
|
+
}
|
|
287
|
+
if (error.code === CDP_SERVER_ERROR &&
|
|
288
|
+
/must be specified|invalid|expected|missing/i.test(error.message)) {
|
|
289
|
+
return new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The handlers of the session commands.
|
|
295
|
+
*
|
|
296
|
+
* @param store - Telemetry of the session
|
|
297
|
+
* @param emulation - The session's page emulation
|
|
298
|
+
* @returns Command registry
|
|
299
|
+
*/
|
|
300
|
+
export function createCommandRegistry(store, emulation) {
|
|
264
301
|
const patternDetector = new PatternDetector();
|
|
265
302
|
const interact = createInteractionRunner(store);
|
|
303
|
+
/** Frame id behind each index of the last `dom frames` listing */
|
|
304
|
+
let listedFrameIds;
|
|
266
305
|
return {
|
|
267
306
|
session_peek: async (_cdp, params) => {
|
|
268
307
|
const lastN = calculateLastN(params.lastN);
|
|
@@ -360,7 +399,9 @@ export function createCommandRegistry(store) {
|
|
|
360
399
|
});
|
|
361
400
|
},
|
|
362
401
|
cdp_call: async (cdp, params) => {
|
|
363
|
-
const result = await withBusyPageRecovery(cdp, cdp.send(params.method, params.params ?? {}))
|
|
402
|
+
const result = await withBusyPageRecovery(cdp, cdp.send(params.method, params.params ?? {}).catch((error) => {
|
|
403
|
+
throw callerError(params.method, error) ?? error;
|
|
404
|
+
}));
|
|
364
405
|
const detectionResult = patternDetector.trackCommand(params.method);
|
|
365
406
|
let hint;
|
|
366
407
|
if (detectionResult.shouldShow && detectionResult.pattern) {
|
|
@@ -376,11 +417,21 @@ export function createCommandRegistry(store) {
|
|
|
376
417
|
},
|
|
377
418
|
dom_eval: async (cdp, params) => params.frame === undefined
|
|
378
419
|
? evaluateScript(cdp, params.script)
|
|
379
|
-
: evaluateInFrame(cdp, pageWebSocketUrl(store), params.script, params.frame),
|
|
380
|
-
dom_frames: async (cdp) =>
|
|
420
|
+
: evaluateInFrame(cdp, pageWebSocketUrl(store), params.script, params.frame, listedFrameIds),
|
|
421
|
+
dom_frames: async (cdp) => {
|
|
422
|
+
const { frames, frameIds } = await listFrames(cdp, pageWebSocketUrl(store));
|
|
423
|
+
listedFrameIds = frameIds;
|
|
424
|
+
return { frames };
|
|
425
|
+
},
|
|
381
426
|
dom_fill: async (cdp, params) => interact(cdp, async () => onScriptTarget(cdp, params, (target) => withActionStability(cdp, () => fillElement(target.cdp, target.selector, params.value, filterDefined({ index: target.index, blur: params.blur, cwd: params.cwd })), params.wait !== false))),
|
|
382
|
-
dom_click: async (cdp, params) => interact(cdp, async () => onScriptTarget(cdp, params, (target) => withActionStability(cdp, () => clickElement(target.cdp, target.selector, filterDefined({
|
|
427
|
+
dom_click: async (cdp, params) => interact(cdp, async () => onScriptTarget(cdp, params, (target) => withActionStability(cdp, () => clickElement(target.cdp, target.selector, filterDefined({
|
|
428
|
+
index: target.index,
|
|
429
|
+
action: params.action,
|
|
430
|
+
strict: params.strict,
|
|
431
|
+
})), params.wait !== false)), {
|
|
383
432
|
detectNoEffect: params.wait !== false && params.action !== 'hover' && params.action !== 'right',
|
|
433
|
+
reportShown: params.action === 'hover',
|
|
434
|
+
detectUnsettled: params.wait !== false && params.action !== 'hover',
|
|
384
435
|
}),
|
|
385
436
|
dom_submit: async (cdp, params) => withTriggeredRequestCount(await interact(cdp, async () => onScriptTarget(cdp, params, (target) => submitForm(target.cdp, target.selector, {
|
|
386
437
|
...filterDefined({
|
|
@@ -395,7 +446,7 @@ export function createCommandRegistry(store) {
|
|
|
395
446
|
index: target.index,
|
|
396
447
|
times: params.times,
|
|
397
448
|
modifiers: params.modifiers,
|
|
398
|
-
})), params.wait !== false))),
|
|
449
|
+
})), params.wait !== false)), { reportShown: true, detectUnsettled: params.wait !== false }),
|
|
399
450
|
dom_scroll: async (cdp, params) => interact(cdp, async () => onScriptTarget(cdp, params, (target) => withActionStability(cdp, () => scrollPage(target.cdp, target.selector || undefined, filterDefined({
|
|
400
451
|
index: target.index,
|
|
401
452
|
down: params.down,
|
|
@@ -407,11 +458,16 @@ export function createCommandRegistry(store) {
|
|
|
407
458
|
})), params.wait !== false))),
|
|
408
459
|
dom_listeners: async (cdp, params) => withBusyPageRecovery(cdp, inspectEventListeners(cdp, params)),
|
|
409
460
|
dom_layout: async (cdp, params) => withBusyPageRecovery(cdp, inspectLayout(cdp, params)),
|
|
461
|
+
dom_inspect: async (cdp, params) => withBusyPageRecovery(cdp, inspectElement(cdp, params)),
|
|
410
462
|
dom_wait: async (cdp, params) => waitForCondition(cdp, params),
|
|
411
463
|
page_navigate: async (cdp, params) => interact(cdp, () => navigatePage(cdp, params.action, {
|
|
412
464
|
...filterDefined({ url: params.url, wait: params.wait }),
|
|
413
465
|
pendingRequests: () => store.pendingNetworkRequests.values(),
|
|
414
466
|
}), { reportRequests: false, reportEffects: false }),
|
|
467
|
+
page_emulate: async (cdp, params) => {
|
|
468
|
+
const emulated = await emulatePage(cdp, emulation.get(), params, emulation.set);
|
|
469
|
+
return { emulated, ...(await pageAppearance(cdp)) };
|
|
470
|
+
},
|
|
415
471
|
dom_form_discover: async (cdp) => {
|
|
416
472
|
const response = await withBusyPageRecovery(cdp, cdp.send('Runtime.evaluate', {
|
|
417
473
|
expression: FORM_DISCOVERY_SCRIPT,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Running page interactions (fill, click, submit, pressKey, scroll) one at a
|
|
3
|
-
* time, reporting the dialogs, network requests and page changes they caused
|
|
3
|
+
* time, reporting the dialogs, network requests and page changes they caused,
|
|
4
|
+
* and whether the page was still changing when they returned.
|
|
4
5
|
*/
|
|
5
6
|
import type { TelemetryStore } from './TelemetryStore.js';
|
|
6
7
|
import type { CDPConnection } from '../../connection/cdp.js';
|
|
@@ -28,6 +29,17 @@ export interface InteractionOptions {
|
|
|
28
29
|
* whose effect shows in the DOM (click, submit) and that waited for it
|
|
29
30
|
*/
|
|
30
31
|
detectNoEffect?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* List the elements it showed (`shown`): for hover and key presses, whose
|
|
34
|
+
* effect is often a menu, tooltip or new item rather than a message
|
|
35
|
+
*/
|
|
36
|
+
reportShown?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Say when the page was still changing as it returned (`settled: false`
|
|
39
|
+
* with `pending`): for actions that start app transitions (click, key
|
|
40
|
+
* press) and waited for the network
|
|
41
|
+
*/
|
|
42
|
+
detectUnsettled?: boolean;
|
|
31
43
|
}
|
|
32
44
|
/** Runs one interaction after the previous one finished */
|
|
33
45
|
export type InteractionRunner = <T extends object>(cdp: CDPConnection, action: () => Promise<T>, options?: InteractionOptions) => Promise<T & InteractionReport>;
|
|
@@ -40,10 +52,11 @@ export type InteractionRunner = <T extends object>(cdp: CDPConnection, action: (
|
|
|
40
52
|
* from the page (without waiting: during a pending navigation that takes until
|
|
41
53
|
* the new page commits), and the dialogs it opened, what it changed on the
|
|
42
54
|
* page (see {@link watchActionEffects}) and the network requests it
|
|
43
|
-
* triggered (see {@link watchTriggeredRequests}) are added to its result
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
55
|
+
* triggered (see {@link watchTriggeredRequests}) are added to its result,
|
|
56
|
+
* and, when asked, what the page was still working on (see
|
|
57
|
+
* {@link pendingChanges}). They are attributed by time: a dialog or request
|
|
58
|
+
* started by a page timer or a navigation started earlier is reported by
|
|
59
|
+
* whichever interaction is running then.
|
|
47
60
|
*
|
|
48
61
|
* @param store - Session store recording accepted dialogs and network requests
|
|
49
62
|
* @returns Interaction runner
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Running page interactions (fill, click, submit, pressKey, scroll) one at a
|
|
3
|
-
* time, reporting the dialogs, network requests and page changes they caused
|
|
3
|
+
* time, reporting the dialogs, network requests and page changes they caused,
|
|
4
|
+
* and whether the page was still changing when they returned.
|
|
4
5
|
*/
|
|
5
|
-
import { watchActionEffects } from '../../runtime/dom/actionEffects.js';
|
|
6
|
+
import { pendingChanges, watchActionEffects } from '../../runtime/dom/actionEffects.js';
|
|
6
7
|
import { UNBIND_TARGET_SCRIPT } from '../../runtime/dom/targetNode.js';
|
|
7
8
|
import { createLogger } from '../../ui/logging/index.js';
|
|
8
9
|
import { getErrorMessage } from '../../utils/errors.js';
|
|
@@ -26,10 +27,11 @@ function succeeded(result) {
|
|
|
26
27
|
* from the page (without waiting: during a pending navigation that takes until
|
|
27
28
|
* the new page commits), and the dialogs it opened, what it changed on the
|
|
28
29
|
* page (see {@link watchActionEffects}) and the network requests it
|
|
29
|
-
* triggered (see {@link watchTriggeredRequests}) are added to its result
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* triggered (see {@link watchTriggeredRequests}) are added to its result,
|
|
31
|
+
* and, when asked, what the page was still working on (see
|
|
32
|
+
* {@link pendingChanges}). They are attributed by time: a dialog or request
|
|
33
|
+
* started by a page timer or a navigation started earlier is reported by
|
|
34
|
+
* whichever interaction is running then.
|
|
33
35
|
*
|
|
34
36
|
* @param store - Session store recording accepted dialogs and network requests
|
|
35
37
|
* @returns Interaction runner
|
|
@@ -44,17 +46,25 @@ export function createInteractionRunner(store) {
|
|
|
44
46
|
try {
|
|
45
47
|
const result = await action();
|
|
46
48
|
const dialogs = store.dialogs.slice(firstDialog);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (!succeeded(result))
|
|
50
|
+
return { ...result, ...(dialogs.length > 0 && { dialogs }) };
|
|
51
|
+
const collected = await effects?.collect({
|
|
52
|
+
dialogs: dialogs.length,
|
|
53
|
+
detectNoEffect: options.detectNoEffect === true,
|
|
54
|
+
reportShown: options.reportShown === true,
|
|
55
|
+
detectUnsettled: options.detectUnsettled === true,
|
|
56
|
+
});
|
|
57
|
+
const { work, ...changes } = collected ?? {};
|
|
58
|
+
const requests = collectRequests?.();
|
|
59
|
+
const pending = options.detectUnsettled && work
|
|
60
|
+
? pendingChanges(work, requests?.triggeredRequests)
|
|
52
61
|
: undefined;
|
|
53
62
|
return {
|
|
54
63
|
...result,
|
|
55
64
|
...(dialogs.length > 0 && { dialogs }),
|
|
56
65
|
...changes,
|
|
57
|
-
...(
|
|
66
|
+
...(pending && { settled: false, pending }),
|
|
67
|
+
...requests,
|
|
58
68
|
};
|
|
59
69
|
}
|
|
60
70
|
finally {
|