Skip to content

Commit e395098

Browse files
committed
feat: increase mobile devices emulate
1 parent 4202513 commit e395098

File tree

2 files changed

+158
-10
lines changed

2 files changed

+158
-10
lines changed

src/tools/ToolDefinition.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {Dialog, ElementHandle, Page} from 'puppeteer-core';
7+
import type { Dialog, ElementHandle, Page } from 'puppeteer-core';
88
import type z from 'zod';
99

10-
import type {TraceResult} from '../trace-processing/parse.js';
10+
import type { TraceResult } from '../trace-processing/parse.js';
1111

12-
import type {ToolCategories} from './categories.js';
12+
import type { ToolCategories } from './categories.js';
1313

1414
export interface ToolDefinition<Schema extends z.ZodRawShape = z.ZodRawShape> {
1515
name: string;
@@ -44,7 +44,7 @@ export interface Response {
4444
setIncludePages(value: boolean): void;
4545
setIncludeNetworkRequests(
4646
value: boolean,
47-
options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]},
47+
options?: { pageSize?: number; pageIdx?: number; resourceTypes?: string[] },
4848
): void;
4949
setIncludeConsoleData(value: boolean): void;
5050
setIncludeSnapshot(value: boolean): void;
@@ -73,8 +73,11 @@ export type Context = Readonly<{
7373
saveTemporaryFile(
7474
data: Uint8Array<ArrayBufferLike>,
7575
mimeType: 'image/png' | 'image/jpeg',
76-
): Promise<{filename: string}>;
76+
): Promise<{ filename: string }>;
7777
waitForEventsAfterAction(action: () => Promise<unknown>): Promise<void>;
78+
// Added for multi-page device emulation support
79+
createPagesSnapshot(): Promise<Page[]>;
80+
getPages(): Page[];
7881
}>;
7982

8083
export function defineTool<Schema extends z.ZodRawShape>(

src/tools/emulation.ts

Lines changed: 150 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,47 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import {PredefinedNetworkConditions} from 'puppeteer-core';
7+
import { PredefinedNetworkConditions } from 'puppeteer-core';
8+
import { KnownDevices } from 'puppeteer-core';
89
import z from 'zod';
910

10-
import {ToolCategories} from './categories.js';
11-
import {defineTool} from './ToolDefinition.js';
11+
import { ToolCategories } from './categories.js';
12+
import { defineTool } from './ToolDefinition.js';
1213

1314
const throttlingOptions: [string, ...string[]] = [
1415
'No emulation',
1516
...Object.keys(PredefinedNetworkConditions),
1617
];
1718

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+
];
47+
1848
export const emulateNetwork = defineTool({
1949
name: 'emulate_network',
2050
description: `Emulates network conditions such as throttling on the selected page.`,
@@ -42,7 +72,7 @@ export const emulateNetwork = defineTool({
4272
if (conditions in PredefinedNetworkConditions) {
4373
const networkCondition =
4474
PredefinedNetworkConditions[
45-
conditions as keyof typeof PredefinedNetworkConditions
75+
conditions as keyof typeof PredefinedNetworkConditions
4676
];
4777
await page.emulateNetworkConditions(networkCondition);
4878
context.setNetworkConditions(conditions);
@@ -68,9 +98,124 @@ export const emulateCpu = defineTool({
6898
},
6999
handler: async (request, _response, context) => {
70100
const page = context.getSelectedPage();
71-
const {throttlingRate} = request.params;
101+
const { throttlingRate } = request.params;
72102

73103
await page.emulateCPUThrottling(throttlingRate);
74104
context.setCpuThrottlingRate(throttlingRate);
75105
},
76106
});
107+
108+
export const emulateDevice = defineTool({
109+
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.`,
111+
annotations: {
112+
category: ToolCategories.EMULATION,
113+
readOnlyHint: false,
114+
},
115+
schema: {
116+
device: z
117+
.enum(deviceOptions)
118+
.describe(
119+
`The device to emulate. Available devices are: ${deviceOptions.join(', ')}. Set to "No emulation" to disable device emulation and use desktop mode.`,
120+
),
121+
customUserAgent: z
122+
.string()
123+
.optional()
124+
.describe(
125+
'Optional custom user agent string. If provided, it will override the device\'s default user agent.',
126+
),
127+
},
128+
handler: async (request, response, context) => {
129+
const { device, customUserAgent } = request.params;
130+
131+
// get all pages to support multi-page scene
132+
await context.createPagesSnapshot();
133+
const allPages = context.getPages();
134+
const currentPage = context.getSelectedPage();
135+
136+
// check if multi pages and apply to all pages
137+
let pagesToEmulate = [currentPage];
138+
let multiPageMessage = '';
139+
140+
if (allPages.length > 1) {
141+
// check if other pages have navigated content (maybe new tab page)
142+
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 });
147+
}
148+
}
149+
150+
if (navigatedPages.length > 0) {
151+
// found other pages have navigated, apply device emulation to all pages
152+
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(', ')}. `;
156+
}
157+
}
158+
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+
);
166+
}
167+
168+
if (multiPageMessage) {
169+
response.appendResponseLine(multiPageMessage);
170+
}
171+
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+
});
183+
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+
}
189+
190+
response.appendResponseLine(
191+
`Device emulation disabled. Desktop mode applied to ${pagesToEmulate.length} page(s).`
192+
);
193+
return;
194+
}
195+
196+
// check if current device is in KnownDevices
197+
if (device in KnownDevices) {
198+
const deviceConfig = KnownDevices[device as keyof typeof KnownDevices];
199+
200+
// apply device config to all page
201+
for (const pageToEmulate of pagesToEmulate) {
202+
await pageToEmulate.emulate({
203+
userAgent: customUserAgent || deviceConfig.userAgent,
204+
viewport: deviceConfig.viewport,
205+
});
206+
}
207+
208+
response.appendResponseLine(
209+
`Successfully emulated device: ${device} on ${pagesToEmulate.length} page(s). ` +
210+
`Viewport: ${deviceConfig.viewport.width}x${deviceConfig.viewport.height}, ` +
211+
`Scale: ${deviceConfig.viewport.deviceScaleFactor}x, ` +
212+
`Mobile: ${deviceConfig.viewport.isMobile ? 'Yes' : 'No'}, ` +
213+
`Touch: ${deviceConfig.viewport.hasTouch ? 'Yes' : 'No'}${customUserAgent ? ', Custom UA applied' : ''}.`
214+
);
215+
} else {
216+
response.appendResponseLine(
217+
`Device "${device}" not found in known devices. Available devices: ${deviceOptions.filter(d => d !== 'No emulation').join(', ')}`
218+
);
219+
}
220+
},
221+
});

0 commit comments

Comments
 (0)