Skip to content

Commit b43787a

Browse files
committed
feat: Optimize boundary conditions.Improve the simulation logic of mobile devices
1 parent d7fc20b commit b43787a

File tree

3 files changed

+141
-83
lines changed

3 files changed

+141
-83
lines changed

src/McpContext.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export class McpContext implements Context {
8585
#isRunningTrace = false;
8686
#networkConditionsMap = new WeakMap<Page, string>();
8787
#cpuThrottlingRateMap = new WeakMap<Page, number>();
88+
#deviceEmulationMap = new WeakMap<Page, string>();
8889
#dialog?: Dialog;
8990

9091
#nextSnapshotId = 1;
@@ -197,6 +198,20 @@ export class McpContext implements Context {
197198
return this.#cpuThrottlingRateMap.get(page) ?? 1;
198199
}
199200

201+
setDeviceEmulation(device: string | null): void {
202+
const page = this.getSelectedPage();
203+
if (device === null) {
204+
this.#deviceEmulationMap.delete(page);
205+
} else {
206+
this.#deviceEmulationMap.set(page, device);
207+
}
208+
}
209+
210+
getDeviceEmulation(): string | null {
211+
const page = this.getSelectedPage();
212+
return this.#deviceEmulationMap.get(page) ?? null;
213+
}
214+
200215
setIsRunningPerformanceTrace(x: boolean): void {
201216
this.#isRunningTrace = x;
202217
}

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export type Context = Readonly<{
7070
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
7171
setNetworkConditions(conditions: string | null): void;
7272
setCpuThrottlingRate(rate: number): void;
73+
setDeviceEmulation(device: string | null): void;
74+
getDeviceEmulation(): string | null;
7375
saveTemporaryFile(
7476
data: Uint8Array<ArrayBufferLike>,
7577
mimeType: 'image/png' | 'image/jpeg' | 'image/webp',

src/tools/emulation.ts

Lines changed: 124 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,45 @@ const throttlingOptions: [string, ...string[]] = [
1616
...Object.keys(PredefinedNetworkConditions),
1717
];
1818

19-
// common use device
20-
const deviceOptions: [string, ...string[]] = [
21-
'No emulation',
22-
// iPhone series
23-
'iPhone SE',
24-
'iPhone 12',
25-
'iPhone 12 Pro',
26-
'iPhone 13',
27-
'iPhone 13 Pro',
28-
'iPhone 14',
29-
'iPhone 14 Pro',
30-
'iPhone 15',
31-
'iPhone 15 Pro',
32-
// Android series
33-
'Galaxy S5',
34-
'Galaxy S8',
35-
'Galaxy S9+',
36-
'Pixel 2',
37-
'Pixel 3',
38-
'Pixel 4',
39-
'Pixel 5',
40-
'Nexus 5',
41-
'Nexus 6P',
42-
// ipad
43-
'iPad',
44-
'iPad Pro',
45-
'Galaxy Tab S4',
46-
];
19+
/**
20+
* Get all mobile device list (dynamically from KnownDevices)
21+
* Filter out landscape devices and uncommon devices, keep only common portrait mobile devices
22+
*/
23+
function getMobileDeviceList(): string[] {
24+
const allDevices = Object.keys(KnownDevices);
25+
// Filter out landscape devices (containing 'landscape') and some uncommon devices
26+
const mobileDevices = allDevices.filter(device => {
27+
const lowerDevice = device.toLowerCase();
28+
// Exclude landscape devices
29+
if (lowerDevice.includes('landscape')) return false;
30+
// Exclude tablets (optional, but keep iPad as common device)
31+
// if (lowerDevice.includes('ipad') || lowerDevice.includes('tab')) return false;
32+
// Exclude some old or uncommon devices
33+
if (lowerDevice.includes('blackberry')) return false;
34+
if (lowerDevice.includes('lumia')) return false;
35+
if (lowerDevice.includes('nokia')) return false;
36+
if (lowerDevice.includes('kindle')) return false;
37+
if (lowerDevice.includes('jio')) return false;
38+
if (lowerDevice.includes('optimus')) return false;
39+
return true;
40+
});
41+
42+
return mobileDevices;
43+
}
44+
45+
/**
46+
* Get default mobile device
47+
*/
48+
function getDefaultMobileDevice(): string {
49+
return 'iPhone 8';
50+
}
51+
52+
/**
53+
* Validate if device exists in KnownDevices
54+
*/
55+
function validateDeviceExists(device: string): boolean {
56+
return device in KnownDevices;
57+
}
4758

4859
export const emulateNetwork = defineTool({
4960
name: 'emulate_network',
@@ -107,16 +118,17 @@ export const emulateCpu = defineTool({
107118

108119
export const emulateDevice = defineTool({
109120
name: 'emulate_device',
110-
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.`,
121+
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.`,
111122
annotations: {
112123
category: ToolCategories.EMULATION,
113124
readOnlyHint: false,
114125
},
115126
schema: {
116127
device: z
117-
.enum(deviceOptions)
128+
.string()
129+
.optional()
118130
.describe(
119-
`The device to emulate. Available devices are: ${deviceOptions.join(', ')}. Set to "No emulation" to disable device emulation and use desktop mode.`,
131+
`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.`,
120132
),
121133
customUserAgent: z
122134
.string()
@@ -126,95 +138,124 @@ export const emulateDevice = defineTool({
126138
),
127139
},
128140
handler: async (request, response, context) => {
129-
const { device, customUserAgent } = request.params;
141+
let { device, customUserAgent } = request.params;
142+
143+
// ========== Phase 0: Handle default device ==========
144+
// If user didn't specify device, use default mobile device
145+
if (!device) {
146+
device = getDefaultMobileDevice();
147+
}
130148

131-
// get all pages to support multi-page scene
149+
// ========== Phase 1: Device validation ==========
150+
// Validate if device exists in KnownDevices
151+
if (!validateDeviceExists(device)) {
152+
const availableDevices = getMobileDeviceList();
153+
device = availableDevices[0];
154+
}
155+
156+
// ========== Phase 2: Page collection and state check ==========
132157
await context.createPagesSnapshot();
133158
const allPages = context.getPages();
134159
const currentPage = context.getSelectedPage();
135160

136-
// check if multi pages and apply to all pages
161+
// Filter out closed pages
162+
const activePages = allPages.filter(page => !page.isClosed());
163+
if (activePages.length === 0) {
164+
response.appendResponseLine('❌ Error: No active pages available for device emulation.');
165+
return;
166+
}
167+
168+
// ========== Phase 3: Determine pages to emulate ==========
137169
let pagesToEmulate = [currentPage];
138-
let multiPageMessage = '';
139170

140-
if (allPages.length > 1) {
141-
// check if other pages have navigated content (maybe new tab page)
171+
if (activePages.length > 1) {
172+
// Check if other pages have navigated content
142173
const navigatedPages = [];
143-
for (const page of allPages) {
144-
const url = page.url();
145-
if (url !== 'about:blank' && url !== currentPage.url()) {
146-
navigatedPages.push({ page, url });
174+
for (const page of activePages) {
175+
if (page.isClosed()) continue; // Double check
176+
177+
try {
178+
const url = page.url();
179+
if (url !== 'about:blank' && url !== currentPage.url()) {
180+
navigatedPages.push({ page, url });
181+
}
182+
} catch (error) {
183+
// Page may have been closed during check
184+
continue;
147185
}
148186
}
149187

188+
// Set emulation for all pages
150189
if (navigatedPages.length > 0) {
151-
// found other pages have navigated, apply device emulation to all pages
152190
pagesToEmulate = [currentPage, ...navigatedPages.map(p => p.page)];
153-
multiPageMessage = `🔄 SMART MULTI-PAGE MODE: Detected ${navigatedPages.length} additional page(s) with content. ` +
154-
`Applying device emulation to current page and ${navigatedPages.length} other page(s): ` +
155-
`${navigatedPages.map(p => p.url).join(', ')}. `;
156191
}
157192
}
158193

159-
// check if current page has navigated
160-
const currentUrl = currentPage.url();
161-
if (currentUrl !== 'about:blank') {
162-
response.appendResponseLine(
163-
`⚠️ WARNING: Device emulation is being applied AFTER page navigation (current URL: ${currentUrl}). ` +
164-
`For best results, device emulation should be set BEFORE navigating to the target website.`
165-
);
194+
// Filter again to ensure all pages to emulate are active
195+
pagesToEmulate = pagesToEmulate.filter(page => !page.isClosed());
196+
197+
if (pagesToEmulate.length === 0) {
198+
response.appendResponseLine('❌ Error: All target pages have been closed.');
199+
return;
166200
}
167201

168-
if (multiPageMessage) {
169-
response.appendResponseLine(multiPageMessage);
170-
}
171202

172-
if (device === 'No emulation') {
173-
// apply desktop mode to all pages
174-
for (const pageToEmulate of pagesToEmulate) {
175-
await pageToEmulate.setViewport({
176-
width: 1920,
177-
height: 1080,
178-
deviceScaleFactor: 1,
179-
isMobile: false,
180-
hasTouch: false,
181-
isLandscape: true,
182-
});
203+
// ========== Phase 4: Mobile device emulation ==========
204+
const deviceConfig = KnownDevices[device as keyof typeof KnownDevices];
183205

184-
await pageToEmulate.setUserAgent(
185-
customUserAgent ||
186-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
187-
);
188-
}
206+
let successCount = 0;
207+
const failedPages: Array<{ url: string; reason: string }> = [];
189208

190-
response.appendResponseLine(
191-
`Device emulation disabled. Desktop mode applied to ${pagesToEmulate.length} page(s).`
192-
);
193-
return;
194-
}
209+
for (const pageToEmulate of pagesToEmulate) {
210+
if (pageToEmulate.isClosed()) {
211+
failedPages.push({
212+
url: 'unknown',
213+
reason: 'Page closed'
214+
});
215+
continue;
216+
}
195217

196-
// check if current device is in KnownDevices
197-
if (device in KnownDevices) {
198-
const deviceConfig = KnownDevices[device as keyof typeof KnownDevices];
218+
const pageUrl = pageToEmulate.url();
199219

200-
// apply device config to all page
201-
for (const pageToEmulate of pagesToEmulate) {
220+
try {
221+
// Directly apply device emulation
202222
await pageToEmulate.emulate({
203223
userAgent: customUserAgent || deviceConfig.userAgent,
204224
viewport: deviceConfig.viewport,
205225
});
226+
successCount++;
227+
} catch (error) {
228+
failedPages.push({
229+
url: pageUrl,
230+
reason: (error as Error).message
231+
});
206232
}
233+
}
234+
235+
// ========== Phase 5: Save state and report results ==========
236+
if (successCount > 0) {
237+
context.setDeviceEmulation(device);
238+
}
207239

240+
// Build detailed report
241+
if (successCount > 0) {
208242
response.appendResponseLine(
209-
`Successfully emulated device: ${device} on ${pagesToEmulate.length} page(s). ` +
243+
`Successfully emulated device: ${device}, applied to ${successCount} page(s).\n` +
210244
`Viewport: ${deviceConfig.viewport.width}x${deviceConfig.viewport.height}, ` +
211245
`Scale: ${deviceConfig.viewport.deviceScaleFactor}x, ` +
212246
`Mobile: ${deviceConfig.viewport.isMobile ? 'Yes' : 'No'}, ` +
213247
`Touch: ${deviceConfig.viewport.hasTouch ? 'Yes' : 'No'}${customUserAgent ? ', Custom UA applied' : ''}.`
214248
);
215249
} else {
250+
// Complete failure
216251
response.appendResponseLine(
217-
`Device "${device}" not found in known devices. Available devices: ${deviceOptions.filter(d => d !== 'No emulation').join(', ')}`
252+
`❌ Error: Unable to apply device emulation to any page.\n\n` +
253+
`Failure details:\n${failedPages.map(p => ` - ${p.url}: ${p.reason}`).join('\n')}\n\n` +
254+
`Diagnostic suggestions:\n` +
255+
` 1. Confirm all target pages are in active state\n` +
256+
` 2. Check if pages allow device emulation (some internal pages may restrict it)\n` +
257+
` 3. Try closing other pages and keep only one page\n` +
258+
` 4. Restart browser and retry`
218259
);
219260
}
220261
},

0 commit comments

Comments
 (0)