diff --git a/README.md b/README.md index fa62a49c..dbae14d1 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,23 @@ Your MCP client should open the browser and record a performance trace. > [!NOTE] > The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser. +### Mobile emulation with Copilot prompts + +When you are working inside VS Code Copilot (or any MCP-aware client), you can chain multiple tool invocations in a single prompt and let the agent run them sequentially. The example below opens a local site, switches to the built-in iPhone 12 Pro profile, applies Slow 4G throttling, records a 10-second performance trace, and finally surfaces LCP/CLS insights: + +``` +Please use mcp chrome-devtools: +1. navigate_page http://localhost:5173 +2. emulate_device_profile profile=iPhone-12-Pro +3. emulate_network throttlingOption="Slow 4G" +4. performance_start_trace duration=10000 +5. performance_stop_trace +6. performance_analyze_insight focus="lcp,cls" +``` + +> [!TIP] +> The `profile` parameter is case-sensitive. Use `iPhone-12-Pro` exactly as written (other presets are listed in the [tool reference](./docs/tool-reference.md)). + ## Tools If you run into any issues, checkout our [troubleshooting guide](./docs/troubleshooting.md). @@ -227,8 +244,9 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles - [`new_page`](docs/tool-reference.md#new_page) - [`select_page`](docs/tool-reference.md#select_page) - [`wait_for`](docs/tool-reference.md#wait_for) -- **Emulation** (3 tools) +- **Emulation** (4 tools) - [`emulate_cpu`](docs/tool-reference.md#emulate_cpu) + - [`emulate_device_profile`](docs/tool-reference.md#emulate_device_profile) - [`emulate_network`](docs/tool-reference.md#emulate_network) - [`resize_page`](docs/tool-reference.md#resize_page) - **Performance** (3 tools) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index ad9410a2..b013dc30 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -18,8 +18,9 @@ - [`new_page`](#new_page) - [`select_page`](#select_page) - [`wait_for`](#wait_for) -- **[Emulation](#emulation)** (3 tools) +- **[Emulation](#emulation)** (4 tools) - [`emulate_cpu`](#emulate_cpu) + - [`emulate_device_profile`](#emulate_device_profile) - [`emulate_network`](#emulate_network) - [`resize_page`](#resize_page) - **[Performance](#performance)** (3 tools) @@ -198,6 +199,16 @@ --- +### `emulate_device_profile` + +**Description:** Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings. + +**Parameters:** + +- **profile** (enum: "iPhone-12-Pro", "Pixel-7") **(required)**: The device profile preset to apply. Supported profiles: iPhone-12-Pro, Pixel-7. + +--- + ### `emulate_network` **Description:** Emulates network conditions such as throttling on the selected page. diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 9228c59b..b92ce90e 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -5,6 +5,7 @@ */ import {PredefinedNetworkConditions} from 'puppeteer-core'; +import type {CDPSession, Protocol, Viewport} from 'puppeteer-core'; import z from 'zod'; import {ToolCategories} from './categories.js'; @@ -15,6 +16,100 @@ const throttlingOptions: [string, ...string[]] = [ ...Object.keys(PredefinedNetworkConditions), ]; +const deviceProfileOptions = ['iPhone-12-Pro', 'Pixel-7'] as const; + +type DeviceProfileName = (typeof deviceProfileOptions)[number]; + +interface DeviceProfileDefinition { + metrics: Protocol.Emulation.SetDeviceMetricsOverrideRequest; + touch: Protocol.Emulation.SetTouchEmulationEnabledRequest; + userAgent: Protocol.Network.SetUserAgentOverrideRequest; + viewport: Viewport; + locale?: string; + timezoneId?: string; +} + +const DEVICE_PROFILES: Record = { + 'iPhone-12-Pro': { + metrics: { + width: 390, + height: 844, + deviceScaleFactor: 3, + mobile: true, + screenWidth: 390, + screenHeight: 844, + screenOrientation: { + type: 'portraitPrimary', + angle: 0, + }, + positionX: 0, + positionY: 0, + scale: 1, + }, + touch: { + enabled: true, + maxTouchPoints: 5, + }, + viewport: { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + userAgent: { + userAgent: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', + platform: 'iPhone', + acceptLanguage: 'en-US,en', + }, + locale: 'en-US', + timezoneId: 'America/Los_Angeles', + }, + 'Pixel-7': { + metrics: { + width: 412, + height: 915, + deviceScaleFactor: 2.625, + mobile: true, + screenWidth: 412, + screenHeight: 915, + screenOrientation: { + type: 'portraitPrimary', + angle: 0, + }, + positionX: 0, + positionY: 0, + scale: 1, + }, + touch: { + enabled: true, + maxTouchPoints: 5, + }, + viewport: { + width: 412, + height: 915, + deviceScaleFactor: 2.625, + isMobile: true, + hasTouch: true, + isLandscape: false, + }, + userAgent: { + userAgent: + 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TD1A.221105.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36', + platform: 'Android', + acceptLanguage: 'en-US,en', + }, + locale: 'en-US', + timezoneId: 'America/Los_Angeles', + }, +}; + +function getClient(page: unknown): CDPSession { + return (page as { _client(): CDPSession })._client(); +} + export const emulateNetwork = defineTool({ name: 'emulate_network', description: `Emulates network conditions such as throttling on the selected page.`, @@ -74,3 +169,54 @@ export const emulateCpu = defineTool({ context.setCpuThrottlingRate(throttlingRate); }, }); + +export const emulateDeviceProfile = defineTool({ + name: 'emulate_device_profile', + description: + 'Emulates a device profile by applying predefined viewport metrics, touch, user agent, locale, and timezone settings.', + annotations: { + category: ToolCategories.EMULATION, + readOnlyHint: false, + }, + schema: { + profile: z + .enum(deviceProfileOptions) + .describe( + `The device profile preset to apply. Supported profiles: ${deviceProfileOptions.join(', ')}.`, + ), + }, + handler: async (request, response, context) => { + const page = context.getSelectedPage(); + const profileName = request.params.profile; + const profile = DEVICE_PROFILES[profileName]; + + if (!profile) { + throw new Error(`Unknown device profile: ${profileName}`); + } + + const client = getClient(page); + + await client.send('Emulation.setDeviceMetricsOverride', profile.metrics); + await client.send('Network.setUserAgentOverride', profile.userAgent); + + if (profile.locale) { + await client.send('Emulation.setLocaleOverride', { + locale: profile.locale, + }); + } + + if (profile.timezoneId) { + await client.send('Emulation.setTimezoneOverride', { + timezoneId: profile.timezoneId, + }); + } + + await page.setViewport(profile.viewport); + + await client.send('Emulation.setTouchEmulationEnabled', profile.touch); + + response.appendResponseLine( + `Applied device profile "${profileName}" to the selected page.`, + ); + }, +}); diff --git a/tests/tools/emulation.test.ts b/tests/tools/emulation.test.ts index 151a621b..2ac32651 100644 --- a/tests/tools/emulation.test.ts +++ b/tests/tools/emulation.test.ts @@ -6,7 +6,11 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import {emulateCpu, emulateNetwork} from '../../src/tools/emulation.js'; +import { + emulateCpu, + emulateDeviceProfile, + emulateNetwork, +} from '../../src/tools/emulation.js'; import {withBrowser} from '../utils.js'; describe('emulation', () => { @@ -136,4 +140,88 @@ describe('emulation', () => { }); }); }); + + describe('device profile', () => { + it('applies iPhone 12 Pro preset', async () => { + await withBrowser(async (response, context) => { + await emulateDeviceProfile.handler( + { + params: { + profile: 'iPhone-12-Pro', + }, + }, + response, + context, + ); + + const page = context.getSelectedPage(); + const result = await page.evaluate(() => { + return { + screenWidth: window.screen.width, + screenHeight: window.screen.height, + devicePixelRatio: window.devicePixelRatio, + userAgent: navigator.userAgent, + language: navigator.language, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + maxTouchPoints: navigator.maxTouchPoints, + }; + }); + + const viewport = page.viewport(); + + assert.strictEqual(result.screenWidth, 390); + assert.strictEqual(result.screenHeight, 844); + assert.strictEqual(result.devicePixelRatio, 3); + assert.ok(result.userAgent.includes('iPhone')); + assert.strictEqual(result.language, 'en-US'); + assert.strictEqual(result.timeZone, 'America/Los_Angeles'); + assert.strictEqual(result.maxTouchPoints, 5); + assert.deepStrictEqual(viewport, { + width: 390, + height: 844, + deviceScaleFactor: 3, + isMobile: true, + hasTouch: true, + isLandscape: false, + }); + assert.deepStrictEqual(response.responseLines, [ + 'Applied device profile "iPhone-12-Pro" to the selected page.', + ]); + }); + }); + + it('applies Pixel 7 preset', async () => { + await withBrowser(async (response, context) => { + await emulateDeviceProfile.handler( + { + params: { + profile: 'Pixel-7', + }, + }, + response, + context, + ); + + const page = context.getSelectedPage(); + const result = await page.evaluate(() => { + return { + screenWidth: window.screen.width, + screenHeight: window.screen.height, + devicePixelRatio: window.devicePixelRatio, + userAgent: navigator.userAgent, + maxTouchPoints: navigator.maxTouchPoints, + }; + }); + + assert.strictEqual(result.screenWidth, 412); + assert.strictEqual(result.screenHeight, 915); + assert.strictEqual(result.devicePixelRatio, 2.625); + assert.ok(result.userAgent.includes('Android')); + assert.strictEqual(result.maxTouchPoints, 5); + assert.deepStrictEqual(response.responseLines, [ + 'Applied device profile "Pixel-7" to the selected page.', + ]); + }); + }); + }); });