cdp-toolkit 1.0.0 → 1.1.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.
@@ -1,236 +0,0 @@
1
- /**
2
- * dialogs.ts — JavaScript dialog handling (alert / confirm / prompt / beforeunload).
3
- *
4
- * Replicates chrome-devtools-mcp's `handle_dialog`. A JS dialog opened by the
5
- * page (alert/confirm/prompt/beforeunload) blocks the renderer until the
6
- * embedder responds via Page.handleJavaScriptDialog. Over raw CDP we:
7
- * 1. Page.enable
8
- * 2. subscribe to Page.javascriptDialogOpening
9
- * 3. on fire, respond with Page.handleJavaScriptDialog({ accept, promptText })
10
- *
11
- * Two modes are supported per CONTRACT:
12
- * - "wait for next dialog then handle" (default): arm the listener and resolve
13
- * when the next dialog is handled, or time out if none appears.
14
- * - "auto-handle for N ms" ({ autoMs }): handle every dialog that opens during
15
- * a fixed window, then resolve with the list handled.
16
- *
17
- * Because a dialog blocks the renderer, the code that *triggers* the dialog
18
- * (e.g. a Runtime.evaluate of `confirm("x")`) must NOT be awaited before the
19
- * dialog is handled — otherwise it deadlocks. `handleDialogForExpression` arms
20
- * the listener, fires the expression WITHOUT awaiting, waits for the dialog to
21
- * be handled, then awaits the now-unblocked evaluate result.
22
- */
23
- import type { CdpConnection } from "../client.ts";
24
- import { CdpError, openPage } from "../client.ts";
25
- import type { TargetSelector } from "../types.ts";
26
-
27
- /** Raw shape of the Page.javascriptDialogOpening event params. */
28
- interface JavascriptDialogOpening {
29
- url: string;
30
- message: string;
31
- type: "alert" | "confirm" | "prompt" | "beforeunload" | string;
32
- hasBrowserHandler: boolean;
33
- defaultPrompt?: string;
34
- }
35
-
36
- /** A single handled dialog. */
37
- export interface HandledDialog {
38
- type: string;
39
- message: string;
40
- url: string;
41
- defaultPrompt?: string;
42
- accept: boolean;
43
- promptText?: string;
44
- handled: true;
45
- }
46
-
47
- export interface HandleDialogArgs {
48
- target?: TargetSelector;
49
- /** Whether to accept (OK) or dismiss (Cancel) the dialog. */
50
- accept: boolean;
51
- /** Text to enter for a prompt() dialog when accepting. */
52
- promptText?: string;
53
- /** How long to wait for the next dialog (default 15000ms). */
54
- timeoutMs?: number;
55
- /**
56
- * Auto-handle mode: keep handling every dialog that opens for this many ms,
57
- * then resolve. When set, the return value is a list of handled dialogs and
58
- * the function never throws on "no dialog" (an empty list is a valid result).
59
- */
60
- autoMs?: number;
61
- }
62
-
63
- /**
64
- * Arm a dialog handler on a live page connection. Returns a disarm function and
65
- * a promise that resolves with each dialog as it is handled (via the onHandled
66
- * callback). This is the low-level primitive used by both public modes and by
67
- * the expression helper.
68
- */
69
- function armDialogHandler(
70
- conn: CdpConnection,
71
- accept: boolean,
72
- promptText: string | undefined,
73
- onHandled: (d: HandledDialog) => void,
74
- ): () => void {
75
- return conn.on("Page.javascriptDialogOpening", (params) => {
76
- const ev = params as unknown as JavascriptDialogOpening;
77
- void (async () => {
78
- try {
79
- await conn.send("Page.handleJavaScriptDialog", {
80
- accept,
81
- ...(promptText !== undefined ? { promptText } : {}),
82
- });
83
- onHandled({
84
- type: ev.type,
85
- message: ev.message,
86
- url: ev.url,
87
- ...(ev.defaultPrompt !== undefined ? { defaultPrompt: ev.defaultPrompt } : {}),
88
- accept,
89
- ...(promptText !== undefined ? { promptText } : {}),
90
- handled: true,
91
- });
92
- } catch {
93
- /* connection may have raced closed; the awaiter will time out */
94
- }
95
- })();
96
- });
97
- }
98
-
99
- /**
100
- * handle_dialog — wait for the next JS dialog on a page and respond to it.
101
- *
102
- * Default mode: opens a persistent connection, enables Page, arms the listener,
103
- * and resolves with the first handled dialog (or throws on timeout). Auto mode
104
- * ({ autoMs }): handles every dialog for the window and resolves with the list.
105
- *
106
- * The caller is expected to trigger the dialog out-of-band (e.g. by clicking a
107
- * button on the page). To trigger-and-handle in one call, use
108
- * handleDialogForExpression.
109
- */
110
- export async function handleDialog(
111
- args: HandleDialogArgs,
112
- ): Promise<HandledDialog | { handled: HandledDialog[]; count: number }> {
113
- const { target, accept, promptText, timeoutMs = 15_000, autoMs } = args;
114
- const { conn } = await openPage(target);
115
- try {
116
- await conn.send("Page.enable");
117
-
118
- if (autoMs !== undefined) {
119
- const handled: HandledDialog[] = [];
120
- const disarm = armDialogHandler(conn, accept, promptText, (d) => handled.push(d));
121
- await new Promise<void>((resolve) => setTimeout(resolve, autoMs));
122
- disarm();
123
- return { handled, count: handled.length };
124
- }
125
-
126
- return await new Promise<HandledDialog>((resolve, reject) => {
127
- const timer = setTimeout(() => {
128
- disarm();
129
- reject(new CdpError(`handle_dialog: no dialog opened within ${timeoutMs}ms`));
130
- }, timeoutMs);
131
- const disarm = armDialogHandler(conn, accept, promptText, (d) => {
132
- clearTimeout(timer);
133
- disarm();
134
- resolve(d);
135
- });
136
- });
137
- } finally {
138
- conn.close();
139
- }
140
- }
141
-
142
- export interface HandleDialogForExpressionArgs {
143
- target?: TargetSelector;
144
- /** A JS expression that opens a dialog, e.g. `confirm("ok?")` or `prompt("name?")`. */
145
- expression: string;
146
- accept: boolean;
147
- promptText?: string;
148
- timeoutMs?: number;
149
- }
150
-
151
- /**
152
- * Arm a dialog handler, evaluate an expression that OPENS a dialog (without
153
- * awaiting it — a blocking dialog would otherwise deadlock the evaluate), wait
154
- * for the dialog to be handled, then await the now-unblocked evaluate result.
155
- *
156
- * Returns both the handled-dialog metadata and the value the expression
157
- * resolved to (e.g. `true` for an accepted confirm, the entered text for an
158
- * accepted prompt, `false`/`null` for a dismissed one). This is the
159
- * trigger-and-handle helper used by the live smoke test.
160
- */
161
- export async function handleDialogForExpression(
162
- args: HandleDialogForExpressionArgs,
163
- ): Promise<{ dialog: HandledDialog; value: unknown }> {
164
- const { target, expression, accept, promptText, timeoutMs = 15_000 } = args;
165
- const { conn } = await openPage(target);
166
- try {
167
- await conn.send("Page.enable");
168
- await conn.send("Runtime.enable");
169
-
170
- let resolveDialog!: (d: HandledDialog) => void;
171
- let rejectDialog!: (e: Error) => void;
172
- const dialogPromise = new Promise<HandledDialog>((res, rej) => {
173
- resolveDialog = res;
174
- rejectDialog = rej;
175
- });
176
- const timer = setTimeout(
177
- () => rejectDialog(new CdpError(`handle_dialog: expression opened no dialog within ${timeoutMs}ms`)),
178
- timeoutMs,
179
- );
180
- const disarm = armDialogHandler(conn, accept, promptText, (d) => {
181
- clearTimeout(timer);
182
- resolveDialog(d);
183
- });
184
-
185
- // Fire the dialog-opening expression WITHOUT awaiting: a blocking dialog
186
- // holds the evaluate open until we handle it. We capture the promise so we
187
- // can await its (now-unblocked) value after the dialog is handled.
188
- const evalPromise = conn
189
- .send<{ result: { value?: unknown }; exceptionDetails?: { text?: string } }>("Runtime.evaluate", {
190
- expression,
191
- returnByValue: true,
192
- awaitPromise: true,
193
- // The evaluate itself must not time out before we handle the dialog.
194
- userGesture: true,
195
- })
196
- .catch((e: unknown) => {
197
- // Surface as an exception-shaped result so the await below can decide.
198
- return { result: {}, __error: e } as { result: { value?: unknown }; __error: unknown };
199
- });
200
-
201
- const dialog = await dialogPromise;
202
- disarm();
203
-
204
- const evalResult = (await evalPromise) as {
205
- result: { value?: unknown };
206
- exceptionDetails?: { text?: string };
207
- __error?: unknown;
208
- };
209
- if (evalResult.__error) throw evalResult.__error;
210
- if (evalResult.exceptionDetails) {
211
- throw new CdpError(`expression threw: ${evalResult.exceptionDetails.text ?? "unknown error"}`);
212
- }
213
-
214
- return { dialog, value: evalResult.result?.value };
215
- } finally {
216
- conn.close();
217
- }
218
- }
219
-
220
- /* ----------------------------------------------------------------------------
221
- * CDP methods / domains used:
222
- * - Page.enable
223
- * - Page.javascriptDialogOpening (event, subscribed)
224
- * - Page.handleJavaScriptDialog ({ accept, promptText })
225
- * - Runtime.enable (helper only)
226
- * - Runtime.evaluate (helper only — fires the dialog-opening expr)
227
- *
228
- * Parity gaps vs chrome-devtools-mcp `handle_dialog`:
229
- * - MCP auto-handles the single pending dialog tracked by its Page object;
230
- * here the caller arms first and triggers out-of-band, OR uses the
231
- * handleDialogForExpression helper to trigger-and-handle atomically.
232
- * - MCP returns its standard page/snapshot envelope after handling; this
233
- * returns just the handled-dialog metadata (type/message/url/accept/...).
234
- * - beforeunload dialogs are handled the same way but are only emitted when a
235
- * real navigation/close is attempted; arming alone does not provoke them.
236
- * -------------------------------------------------------------------------- */
@@ -1,212 +0,0 @@
1
- /**
2
- * emulation tools — `emulate` and `resize_page` over raw CDP.
3
- *
4
- * `emulate` is a one-call switchboard over Chrome's emulation overrides:
5
- * device metrics (size / DPR / mobile), user-agent, CPU throttling, emulated
6
- * media (type + features like prefers-color-scheme), and network conditions.
7
- * `resize_page` is the narrow case of setting just the device-metrics size.
8
- * A `clearOverrides` path resets every override back to the browser default.
9
- *
10
- * NOTE on persistence: these overrides live on the DevTools *session*. The
11
- * toolkit's stateless one-shot model (withPage opens then closes the
12
- * connection) means an override applied in one call is gone once that
13
- * connection closes — except device metrics, which Chrome keeps applied to the
14
- * target until explicitly cleared or the renderer navigates/reloads. We
15
- * therefore re-assert metrics in resize_page and document this in the footer.
16
- */
17
- import type { CdpConnection } from "../client.ts";
18
- import { CdpError, withPage } from "../client.ts";
19
- import type { Target, TargetSelector } from "../types.ts";
20
-
21
- export interface NetworkConditions {
22
- /** true = simulate offline. */
23
- offline?: boolean;
24
- /** additional round-trip latency, ms. */
25
- latency?: number;
26
- /** max download throughput, bytes/sec (-1 = no limit). */
27
- downloadThroughput?: number;
28
- /** max upload throughput, bytes/sec (-1 = no limit). */
29
- uploadThroughput?: number;
30
- /** "none" | "cellular2g" | "cellular3g" | "cellular4g" | "bluetooth" | "ethernet" | "wifi" | "wimax" | "other" */
31
- connectionType?: string;
32
- }
33
-
34
- export interface EmulateArgs {
35
- target?: TargetSelector;
36
- width?: number;
37
- height?: number;
38
- deviceScaleFactor?: number;
39
- mobile?: boolean;
40
- userAgent?: string;
41
- /** 1 = no throttle; 2 = 2x slower; etc. */
42
- cpuThrottlingRate?: number;
43
- /** Emulated CSS media type: "screen" | "print" | "" (clear). */
44
- media?: string;
45
- /** Emulated media features, e.g. [{ name:"prefers-color-scheme", value:"dark" }]. */
46
- mediaFeatures?: Array<{ name: string; value: string }>;
47
- networkConditions?: NetworkConditions;
48
- /** Reset every override to the browser default; ignores all other fields. */
49
- clearOverrides?: boolean;
50
- }
51
-
52
- export interface EmulateResult {
53
- target: { id: string; url: string; title: string };
54
- applied: string[];
55
- cleared?: boolean;
56
- }
57
-
58
- export interface ResizeArgs {
59
- target?: TargetSelector;
60
- width: number;
61
- height: number;
62
- deviceScaleFactor?: number;
63
- mobile?: boolean;
64
- }
65
-
66
- export interface ResizeResult {
67
- target: { id: string; url: string; title: string };
68
- width: number;
69
- height: number;
70
- /** window.innerWidth/innerHeight observed after the override is applied. */
71
- innerWidth: number;
72
- innerHeight: number;
73
- }
74
-
75
- async function clearAll(conn: CdpConnection): Promise<void> {
76
- // Order doesn't matter; each is independent. Tolerate per-domain absence.
77
- await conn.send("Emulation.clearDeviceMetricsOverride").catch(() => undefined);
78
- await conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(() => undefined);
79
- await conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(() => undefined);
80
- await conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(() => undefined);
81
- await conn.send("Network.enable").catch(() => undefined);
82
- await conn
83
- .send("Network.emulateNetworkConditions", {
84
- offline: false,
85
- latency: 0,
86
- downloadThroughput: -1,
87
- uploadThroughput: -1,
88
- })
89
- .catch(() => undefined);
90
- }
91
-
92
- /**
93
- * emulate — apply any subset of device-metrics / UA / CPU / media / network
94
- * overrides in a single call, or clear them all with { clearOverrides:true }.
95
- */
96
- export async function emulate(args: EmulateArgs = {}): Promise<EmulateResult> {
97
- const { target } = args;
98
- return withPage(target, async (conn: CdpConnection, t: Target) => {
99
- if (args.clearOverrides) {
100
- await clearAll(conn);
101
- return {
102
- target: { id: t.id, url: t.url, title: t.title },
103
- applied: [],
104
- cleared: true,
105
- };
106
- }
107
-
108
- const applied: string[] = [];
109
-
110
- if (args.width != null || args.height != null) {
111
- if (args.width == null || args.height == null) {
112
- throw new CdpError("emulate device metrics require both width and height");
113
- }
114
- await conn.send("Emulation.setDeviceMetricsOverride", {
115
- width: args.width,
116
- height: args.height,
117
- deviceScaleFactor: args.deviceScaleFactor ?? 0,
118
- mobile: args.mobile ?? false,
119
- });
120
- applied.push("deviceMetrics");
121
- }
122
-
123
- if (args.userAgent != null) {
124
- await conn.send("Emulation.setUserAgentOverride", { userAgent: args.userAgent });
125
- applied.push("userAgent");
126
- }
127
-
128
- if (args.cpuThrottlingRate != null) {
129
- if (args.cpuThrottlingRate < 1) throw new CdpError("cpuThrottlingRate must be >= 1");
130
- await conn.send("Emulation.setCPUThrottlingRate", { rate: args.cpuThrottlingRate });
131
- applied.push("cpuThrottlingRate");
132
- }
133
-
134
- if (args.media != null || args.mediaFeatures != null) {
135
- await conn.send("Emulation.setEmulatedMedia", {
136
- media: args.media ?? "",
137
- features: args.mediaFeatures ?? [],
138
- });
139
- applied.push("emulatedMedia");
140
- }
141
-
142
- if (args.networkConditions != null) {
143
- const n = args.networkConditions;
144
- await conn.send("Network.enable").catch(() => undefined);
145
- await conn.send("Network.emulateNetworkConditions", {
146
- offline: n.offline ?? false,
147
- latency: n.latency ?? 0,
148
- downloadThroughput: n.downloadThroughput ?? -1,
149
- uploadThroughput: n.uploadThroughput ?? -1,
150
- ...(n.connectionType ? { connectionType: n.connectionType } : {}),
151
- });
152
- applied.push("networkConditions");
153
- }
154
-
155
- if (applied.length === 0) {
156
- throw new CdpError("emulate: no overrides specified (pass clearOverrides:true to reset)");
157
- }
158
-
159
- return { target: { id: t.id, url: t.url, title: t.title }, applied };
160
- });
161
- }
162
-
163
- /**
164
- * resize_page — set the page's device-metrics width/height. Verifies by reading
165
- * back window.innerWidth/innerHeight after the override is applied.
166
- */
167
- export async function resizePage(args: ResizeArgs): Promise<ResizeResult> {
168
- const { target, width, height } = args;
169
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
170
- throw new CdpError("resize_page requires positive numeric width and height");
171
- }
172
- return withPage(target, async (conn: CdpConnection, t: Target) => {
173
- await conn.send("Emulation.setDeviceMetricsOverride", {
174
- width,
175
- height,
176
- deviceScaleFactor: args.deviceScaleFactor ?? 0,
177
- mobile: args.mobile ?? false,
178
- });
179
- await conn.send("Runtime.enable").catch(() => undefined);
180
- const { result } = await conn.send<{ result: { value?: { w: number; h: number } } }>("Runtime.evaluate", {
181
- expression: "({ w: window.innerWidth, h: window.innerHeight })",
182
- returnByValue: true,
183
- });
184
- const v = result.value;
185
- return {
186
- target: { id: t.id, url: t.url, title: t.title },
187
- width,
188
- height,
189
- innerWidth: v?.w ?? width,
190
- innerHeight: v?.h ?? height,
191
- };
192
- });
193
- }
194
-
195
- /*
196
- * CDP methods/domains used:
197
- * - Emulation.setDeviceMetricsOverride (resize + emulate metrics)
198
- * - Emulation.clearDeviceMetricsOverride (clearOverrides)
199
- * - Emulation.setUserAgentOverride
200
- * - Emulation.setCPUThrottlingRate
201
- * - Emulation.setEmulatedMedia (media type + features)
202
- * - Network.enable / Network.emulateNetworkConditions
203
- * - Runtime.enable / Runtime.evaluate (resize_page read-back verification)
204
- * Parity gaps vs chrome-devtools-mcp emulate/resize_page:
205
- * - Override lifetime: toolkit is stateless (connection closes after each call). Device-metrics
206
- * overrides persist on the target until cleared/navigated; UA/CPU/media/network overrides are
207
- * session-scoped and reset when the per-call connection closes. The MCP keeps one long-lived
208
- * session so its non-metrics overrides persist across subsequent MCP calls; ours do not.
209
- * - No named device-descriptor presets (e.g. "iPhone 15"); caller passes raw metrics + UA.
210
- * - No touch-emulation / screen-orientation override (Emulation.setTouchEmulationEnabled,
211
- * setDeviceMetricsOverride.screenOrientation) — out of the assigned arg surface.
212
- */