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.
- package/CONTRACT.md +1 -1
- package/README.md +34 -0
- package/dist/cli.js +3156 -1290
- package/dist/index.js +1116 -927
- package/dist/mcp.js +4472 -2626
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +823 -0
- package/src/bidi/launch.ts +192 -0
- package/src/bidi/protocol.ts +782 -0
- package/src/bidi/snapshot.ts +419 -0
- package/src/bidi-tools.ts +193 -0
- package/src/capabilities.ts +43 -0
- package/src/cdp/driver.ts +660 -0
- package/src/cli.ts +72 -14
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +14 -0
- package/src/index.ts +49 -41
- package/src/mcp.ts +59 -9
- package/src/shared-tools.ts +420 -0
- package/src/tools/dialogs.ts +0 -236
- package/src/tools/emulation.ts +0 -212
- package/src/tools/input.ts +0 -399
- package/src/tools/pages.ts +0 -159
- package/src/tools/screenshot.ts +0 -180
package/src/tools/dialogs.ts
DELETED
|
@@ -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
|
-
* -------------------------------------------------------------------------- */
|
package/src/tools/emulation.ts
DELETED
|
@@ -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
|
-
*/
|