diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 0e126b8c3..f2d37c567 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -196,6 +196,7 @@ **Parameters:** - **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged. +- **geolocation** (unknown) _(optional)_: Geolocation to [`emulate`](#emulate). Set to null to clear the geolocation override. - **networkConditions** (enum: "No emulation", "Offline", "Slow 3G", "Fast 3G", "Slow 4G", "Fast 4G") _(optional)_: Throttle network. Set to "No emulation" to disable. If omitted, conditions remain unchanged. --- diff --git a/src/McpContext.ts b/src/McpContext.ts index 9952a048d..f35bbbc75 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -38,6 +38,11 @@ export interface TextSnapshotNode extends SerializedAXNode { children: TextSnapshotNode[]; } +export interface GeolocationOptions { + latitude: number; + longitude: number; +} + export interface TextSnapshot { root: TextSnapshotNode; idToNode: Map; @@ -104,6 +109,7 @@ export class McpContext implements Context { #isRunningTrace = false; #networkConditionsMap = new WeakMap(); #cpuThrottlingRateMap = new WeakMap(); + #geolocationMap = new WeakMap(); #dialog?: Dialog; #nextSnapshotId = 1; @@ -277,6 +283,20 @@ export class McpContext implements Context { return this.#cpuThrottlingRateMap.get(page) ?? 1; } + setGeolocation(geolocation: GeolocationOptions | null): void { + const page = this.getSelectedPage(); + if (geolocation === null) { + this.#geolocationMap.delete(page); + } else { + this.#geolocationMap.set(page, geolocation); + } + } + + getGeolocation(): GeolocationOptions | null { + const page = this.getSelectedPage(); + return this.#geolocationMap.get(page) ?? null; + } + setIsRunningPerformanceTrace(x: boolean): void { this.#isRunningTrace = x; } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 1586c2d78..d017640cd 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {TextSnapshotNode} from '../McpContext.js'; +import type {TextSnapshotNode, GeolocationOptions} from '../McpContext.js'; import {zod} from '../third_party/index.js'; import type {Dialog, ElementHandle, Page} from '../third_party/index.js'; import type {TraceResult} from '../trace-processing/parse.js'; @@ -98,6 +98,7 @@ export type Context = Readonly<{ getAXNodeByUid(uid: string): TextSnapshotNode | undefined; setNetworkConditions(conditions: string | null): void; setCpuThrottlingRate(rate: number): void; + setGeolocation(geolocation: GeolocationOptions | null): void; saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg' | 'image/webp', diff --git a/src/tools/emulation.ts b/src/tools/emulation.ts index 4b88eead7..13119250d 100644 --- a/src/tools/emulation.ts +++ b/src/tools/emulation.ts @@ -37,20 +37,34 @@ export const emulate = defineTool({ .describe( 'Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.', ), + geolocation: zod + .object({ + latitude: zod + .number() + .min(-90) + .max(90) + .describe('Latitude between -90 and 90.'), + longitude: zod + .number() + .min(-180) + .max(180) + .describe('Longitude between -180 and 180.'), + }) + .nullable() + .optional() + .describe( + 'Geolocation to emulate. Set to null to clear the geolocation override.', + ), }, handler: async (request, _response, context) => { const page = context.getSelectedPage(); - const networkConditions = request.params.networkConditions; - const cpuThrottlingRate = request.params.cpuThrottlingRate; + const {networkConditions, cpuThrottlingRate, geolocation} = request.params; if (networkConditions) { if (networkConditions === 'No emulation') { await page.emulateNetworkConditions(null); context.setNetworkConditions(null); - return; - } - - if (networkConditions === 'Offline') { + } else if (networkConditions === 'Offline') { await page.emulateNetworkConditions({ offline: true, download: 0, @@ -58,10 +72,7 @@ export const emulate = defineTool({ latency: 0, }); context.setNetworkConditions('Offline'); - return; - } - - if (networkConditions in PredefinedNetworkConditions) { + } else if (networkConditions in PredefinedNetworkConditions) { const networkCondition = PredefinedNetworkConditions[ networkConditions as keyof typeof PredefinedNetworkConditions @@ -75,5 +86,15 @@ export const emulate = defineTool({ await page.emulateCPUThrottling(cpuThrottlingRate); context.setCpuThrottlingRate(cpuThrottlingRate); } + + if (geolocation !== undefined) { + if (geolocation === null) { + await page.setGeolocation({latitude: 0, longitude: 0}); + context.setGeolocation(null); + } else { + await page.setGeolocation(geolocation); + context.setGeolocation(geolocation); + } + } }, }); diff --git a/tests/tools/emulation.test.ts b/tests/tools/emulation.test.ts index 1af4a9df3..5275f5640 100644 --- a/tests/tools/emulation.test.ts +++ b/tests/tools/emulation.test.ts @@ -152,4 +152,86 @@ describe('emulation', () => { }); }); }); + + describe('geolocation', () => { + it('emulates geolocation with latitude and longitude', async () => { + await withMcpContext(async (response, context) => { + await emulate.handler( + { + params: { + geolocation: { + latitude: 48.137154, + longitude: 11.576124, + }, + }, + }, + response, + context, + ); + + const geolocation = context.getGeolocation(); + assert.strictEqual(geolocation?.latitude, 48.137154); + assert.strictEqual(geolocation?.longitude, 11.576124); + }); + }); + + it('clears geolocation override when geolocation is set to null', async () => { + await withMcpContext(async (response, context) => { + // First set a geolocation + await emulate.handler( + { + params: { + geolocation: { + latitude: 48.137154, + longitude: 11.576124, + }, + }, + }, + response, + context, + ); + + assert.notStrictEqual(context.getGeolocation(), null); + + // Then clear it by setting geolocation to null + await emulate.handler( + { + params: { + geolocation: null, + }, + }, + response, + context, + ); + + assert.strictEqual(context.getGeolocation(), null); + }); + }); + + it('reports correctly for the currently selected page', async () => { + await withMcpContext(async (response, context) => { + await emulate.handler( + { + params: { + geolocation: { + latitude: 48.137154, + longitude: 11.576124, + }, + }, + }, + response, + context, + ); + + const geolocation = context.getGeolocation(); + assert.strictEqual(geolocation?.latitude, 48.137154); + assert.strictEqual(geolocation?.longitude, 11.576124); + + const page = await context.newPage(); + context.selectPage(page); + + assert.strictEqual(context.getGeolocation(), null); + }); + }); + }); });