@@ -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
4859export const emulateNetwork = defineTool ( {
4960 name : 'emulate_network' ,
@@ -107,16 +118,17 @@ export const emulateCpu = defineTool({
107118
108119export 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