forked from ChromeDevTools/chrome-devtools-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathemulation.ts
More file actions
272 lines (241 loc) · 9.06 KB
/
emulation.ts
File metadata and controls
272 lines (241 loc) · 9.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {zod, PredefinedNetworkConditions} from '../third_party/index.js';
import { KnownDevices } from 'puppeteer-core';
import { ToolCategories } from './categories.js';
import { defineTool } from './ToolDefinition.js';
const throttlingOptions: [string, ...string[]] = [
'No emulation',
'Offline',
...Object.keys(PredefinedNetworkConditions),
];
/**
* Get all mobile device list (dynamically from KnownDevices)
* Filter out landscape devices and uncommon devices, keep only common portrait mobile devices
*/
function getMobileDeviceList(): string[] {
const allDevices = Object.keys(KnownDevices);
// Filter out landscape devices (containing 'landscape') and some uncommon devices
const mobileDevices = allDevices.filter(device => {
const lowerDevice = device.toLowerCase();
// Exclude landscape devices
if (lowerDevice.includes('landscape')) return false;
// Exclude tablets (optional, but keep iPad as common device)
// if (lowerDevice.includes('ipad') || lowerDevice.includes('tab')) return false;
// Exclude some old or uncommon devices
if (lowerDevice.includes('blackberry')) return false;
if (lowerDevice.includes('lumia')) return false;
if (lowerDevice.includes('nokia')) return false;
if (lowerDevice.includes('kindle')) return false;
if (lowerDevice.includes('jio')) return false;
if (lowerDevice.includes('optimus')) return false;
return true;
});
return mobileDevices;
}
/**
* Get default mobile device
*/
function getDefaultMobileDevice(): string {
return 'iPhone 8';
}
/**
* Validate if device exists in KnownDevices
*/
function validateDeviceExists(device: string): boolean {
return device in KnownDevices;
}
export const emulateNetwork = defineTool({
name: 'emulate_network',
description: `Emulates network conditions such as throttling or offline mode on the selected page.`,
annotations: {
category: ToolCategories.EMULATION,
readOnlyHint: false,
},
schema: {
throttlingOption: zod
.enum(throttlingOptions)
.describe(
`The network throttling option to emulate. Available throttling options are: ${throttlingOptions.join(', ')}. Set to "No emulation" to disable. Set to "Offline" to simulate offline network conditions.`,
),
},
handler: async (request, _response, context) => {
const page = context.getSelectedPage();
const conditions = request.params.throttlingOption;
if (conditions === 'No emulation') {
await page.emulateNetworkConditions(null);
context.setNetworkConditions(null);
return;
}
if (conditions === 'Offline') {
await page.emulateNetworkConditions({
offline: true,
download: 0,
upload: 0,
latency: 0,
});
context.setNetworkConditions('Offline');
return;
}
if (conditions in PredefinedNetworkConditions) {
const networkCondition =
PredefinedNetworkConditions[
conditions as keyof typeof PredefinedNetworkConditions
];
await page.emulateNetworkConditions(networkCondition);
context.setNetworkConditions(conditions);
}
},
});
export const emulateCpu = defineTool({
name: 'emulate_cpu',
description: `Emulates CPU throttling by slowing down the selected page's execution.`,
annotations: {
category: ToolCategories.EMULATION,
readOnlyHint: false,
},
schema: {
throttlingRate: zod
.number()
.min(1)
.max(20)
.describe(
'The CPU throttling rate representing the slowdown factor 1-20x. Set the rate to 1 to disable throttling',
),
},
handler: async (request, _response, context) => {
const page = context.getSelectedPage();
const { throttlingRate } = request.params;
await page.emulateCPUThrottling(throttlingRate);
context.setCpuThrottlingRate(throttlingRate);
},
});
export const emulateDevice = defineTool({
name: 'emulate_device',
description: `IMPORTANT: Emulates a mobile device including viewport, user-agent, touch support, and device scale factor. This tool MUST be called BEFORE navigating to any website to ensure the correct mobile user-agent is used. Essential for testing mobile website performance and user experience. If no device is specified, defaults to iPhone 8.`,
annotations: {
category: ToolCategories.EMULATION,
readOnlyHint: false,
},
schema: {
device: zod
.string()
.optional()
.describe(
`The mobile device to emulate. If not specified, defaults to "${getDefaultMobileDevice()}". Available devices include all mobile devices from Puppeteer's KnownDevices (e.g., iPhone 8, iPhone 13, iPhone 14, iPhone 15, Galaxy S8, Galaxy S9+, Pixel 2-5, iPad, iPad Pro, etc.). Use the exact device name as defined in Puppeteer.`,
),
customUserAgent: zod
.string()
.optional()
.describe(
'Optional custom user agent string. If provided, it will override the device\'s default user agent.',
),
},
handler: async (request, response, context) => {
let { device, customUserAgent } = request.params;
// ========== Phase 0: Handle default device ==========
// If user didn't specify device, use default mobile device
if (!device) {
device = getDefaultMobileDevice();
}
// ========== Phase 1: Device validation ==========
// Validate if device exists in KnownDevices
if (!validateDeviceExists(device)) {
const availableDevices = getMobileDeviceList();
device = availableDevices[0];
}
// ========== Phase 2: Page collection and state check ==========
await context.createPagesSnapshot();
const allPages = context.getPages();
const currentPage = context.getSelectedPage();
// Filter out closed pages
const activePages = allPages.filter(page => !page.isClosed());
if (activePages.length === 0) {
response.appendResponseLine('❌ Error: No active pages available for device emulation.');
return;
}
// ========== Phase 3: Determine pages to emulate ==========
let pagesToEmulate = [currentPage];
if (activePages.length > 1) {
// Check if other pages have navigated content
const navigatedPages = [];
for (const page of activePages) {
if (page.isClosed()) continue; // Double check
try {
const url = page.url();
if (url !== 'about:blank' && url !== currentPage.url()) {
navigatedPages.push({ page, url });
}
} catch (error) {
// Page may have been closed during check
continue;
}
}
// Set emulation for all pages
if (navigatedPages.length > 0) {
pagesToEmulate = [currentPage, ...navigatedPages.map(p => p.page)];
}
}
// Filter again to ensure all pages to emulate are active
pagesToEmulate = pagesToEmulate.filter(page => !page.isClosed());
if (pagesToEmulate.length === 0) {
response.appendResponseLine('❌ Error: All target pages have been closed.');
return;
}
// ========== Phase 4: Mobile device emulation ==========
const deviceConfig = KnownDevices[device as keyof typeof KnownDevices];
let successCount = 0;
const failedPages: Array<{ url: string; reason: string }> = [];
for (const pageToEmulate of pagesToEmulate) {
if (pageToEmulate.isClosed()) {
failedPages.push({
url: 'unknown',
reason: 'Page closed'
});
continue;
}
const pageUrl = pageToEmulate.url();
try {
// Directly apply device emulation
await pageToEmulate.emulate({
userAgent: customUserAgent || deviceConfig.userAgent,
viewport: deviceConfig.viewport,
});
successCount++;
} catch (error) {
failedPages.push({
url: pageUrl,
reason: (error as Error).message
});
}
}
// ========== Phase 5: Save state and report results ==========
if (successCount > 0) {
context.setDeviceEmulation(device);
}
// Build detailed report
if (successCount > 0) {
response.appendResponseLine(
`✅ Successfully emulated device: ${device}, applied to ${successCount} page(s).\n` +
`Viewport: ${deviceConfig.viewport.width}x${deviceConfig.viewport.height}, ` +
`Scale: ${deviceConfig.viewport.deviceScaleFactor}x, ` +
`Mobile: ${deviceConfig.viewport.isMobile ? 'Yes' : 'No'}, ` +
`Touch: ${deviceConfig.viewport.hasTouch ? 'Yes' : 'No'}${customUserAgent ? ', Custom UA applied' : ''}.`
);
} else {
// Complete failure
response.appendResponseLine(
`❌ Error: Unable to apply device emulation to any page.\n\n` +
`Failure details:\n${failedPages.map(p => ` - ${p.url}: ${p.reason}`).join('\n')}\n\n` +
`Diagnostic suggestions:\n` +
` 1. Confirm all target pages are in active state\n` +
` 2. Check if pages allow device emulation (some internal pages may restrict it)\n` +
` 3. Try closing other pages and keep only one page\n` +
` 4. Restart browser and retry`
);
}
},
});