Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@

**Parameters:**

- **clearGeolocation** (boolean) _(optional)_: Set to true to clear the geolocation override.
- **cpuThrottlingRate** (number) _(optional)_: Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.
- **latitude** (number) _(optional)_: Latitude between -90 and 90 for geolocation emulation. Must be provided together with longitude.
- **longitude** (number) _(optional)_: Longitude between -180 and 180 for geolocation emulation. Must be provided together with latitude.
- **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.

---
Expand Down
20 changes: 20 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TextSnapshotNode>;
Expand Down Expand Up @@ -104,6 +109,7 @@ export class McpContext implements Context {
#isRunningTrace = false;
#networkConditionsMap = new WeakMap<Page, string>();
#cpuThrottlingRateMap = new WeakMap<Page, number>();
#geolocationMap = new WeakMap<Page, GeolocationOptions>();
#dialog?: Dialog;

#nextSnapshotId = 1;
Expand Down Expand Up @@ -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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ArrayBufferLike>,
mimeType: 'image/png' | 'image/jpeg' | 'image/webp',
Expand Down
53 changes: 43 additions & 10 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,50 @@ export const emulate = defineTool({
.describe(
'Represents the CPU slowdown factor. Set the rate to 1 to disable throttling. If omitted, throttling remains unchanged.',
),
latitude: zod
.number()
.min(-90)
.max(90)
.optional()
.describe(
'Latitude between -90 and 90 for geolocation emulation. Must be provided together with longitude.',
),
longitude: zod
.number()
.min(-180)
.max(180)
.optional()
.describe(
'Longitude between -180 and 180 for geolocation emulation. Must be provided together with latitude.',
),
clearGeolocation: zod
.boolean()
.optional()
.describe('Set to true 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,
latitude,
longitude,
clearGeolocation,
} = 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,
upload: 0,
latency: 0,
});
context.setNetworkConditions('Offline');
return;
}

if (networkConditions in PredefinedNetworkConditions) {
} else if (networkConditions in PredefinedNetworkConditions) {
const networkCondition =
PredefinedNetworkConditions[
networkConditions as keyof typeof PredefinedNetworkConditions
Expand All @@ -75,5 +94,19 @@ export const emulate = defineTool({
await page.emulateCPUThrottling(cpuThrottlingRate);
context.setCpuThrottlingRate(cpuThrottlingRate);
}

if (clearGeolocation) {
await page.setGeolocation({latitude: 0, longitude: 0});
context.setGeolocation(null);
} else if (latitude !== undefined || longitude !== undefined) {
if (latitude !== undefined && longitude !== undefined) {
await page.setGeolocation({latitude, longitude});
context.setGeolocation({latitude, longitude});
} else {
throw new Error(
'Both latitude and longitude must be provided together for geolocation emulation.',
);
}
}
},
});
98 changes: 98 additions & 0 deletions tests/tools/emulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,102 @@ describe('emulation', () => {
});
});
});

describe('geolocation', () => {
it('emulates geolocation with latitude and longitude', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
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 clearGeolocation is true', async () => {
await withMcpContext(async (response, context) => {
// First set a geolocation
await emulate.handler(
{
params: {
latitude: 48.137154,
longitude: 11.576124,
},
},
response,
context,
);

assert.notStrictEqual(context.getGeolocation(), null);

// Then clear it using clearGeolocation
await emulate.handler(
{
params: {
clearGeolocation: true,
},
},
response,
context,
);

assert.strictEqual(context.getGeolocation(), null);
});
});

it('throws error when only latitude is provided', async () => {
await withMcpContext(async (response, context) => {
await assert.rejects(
async () => {
await emulate.handler(
{
params: {
latitude: 48.137154,
},
},
response,
context,
);
},
{
message:
'Both latitude and longitude must be provided together for geolocation emulation.',
},
);
});
});

it('reports correctly for the currently selected page', async () => {
await withMcpContext(async (response, context) => {
await emulate.handler(
{
params: {
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);
});
});
});
});