Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,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** (2 tools)
- **Emulation** (3 tools)
- [`emulate`](docs/tool-reference.md#emulate)
- [`emulate_geolocation`](docs/tool-reference.md#emulate_geolocation)
- [`resize_page`](docs/tool-reference.md#resize_page)
- **Performance** (3 tools)
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
Expand Down
14 changes: 13 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
- [`new_page`](#new_page)
- [`select_page`](#select_page)
- [`wait_for`](#wait_for)
- **[Emulation](#emulation)** (2 tools)
- **[Emulation](#emulation)** (3 tools)
- [`emulate`](#emulate)
- [`emulate_geolocation`](#emulate_geolocation)
- [`resize_page`](#resize_page)
- **[Performance](#performance)** (3 tools)
- [`performance_analyze_insight`](#performance_analyze_insight)
Expand Down Expand Up @@ -200,6 +201,17 @@

---

### `emulate_geolocation`

**Description:** Emulates geolocation on the selected page. Useful for testing location-based features.

**Parameters:**

- **latitude** (number) _(optional)_: Latitude between -90 and 90. Omit latitude and longitude to clear the override.
- **longitude** (number) _(optional)_: Longitude between -180 and 180. Omit latitude and longitude to clear the override.

---

### `resize_page`

**Description:** Resizes the selected page's window so that the page has specified dimension
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
54 changes: 54 additions & 0 deletions src/tools/emulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,57 @@ export const emulate = defineTool({
}
},
});

export const emulateGeolocation = defineTool({
name: 'emulate_geolocation',
description: `Emulates geolocation on the selected page. Useful for testing location-based features.`,
annotations: {
category: ToolCategory.EMULATION,
readOnlyHint: false,
},
schema: {
latitude: zod
.number()
.min(-90)
.max(90)
.optional()
.describe(
'Latitude between -90 and 90. Omit latitude and longitude to clear the override.',
),
longitude: zod
.number()
.min(-180)
.max(180)
.optional()
.describe(
'Longitude between -180 and 180. Omit latitude and longitude to clear the override.',
),
},
handler: async (request, _response, context) => {
const page = context.getSelectedPage();
const {latitude, longitude} = request.params;

if (latitude === undefined && longitude === undefined) {
// Clear geolocation override
await page.setGeolocation({
latitude: 0,
longitude: 0,
});
context.setGeolocation(null);
} else if (latitude !== undefined && longitude !== undefined) {
// Set geolocation override
await page.setGeolocation({
latitude,
longitude,
});
context.setGeolocation({
latitude,
longitude,
});
} else {
throw new Error(
'Both latitude and longitude must be provided, or both must be omitted to clear the override.',
);
}
},
});
98 changes: 97 additions & 1 deletion tests/tools/emulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';

import {emulate} from '../../src/tools/emulation.js';
import {emulate, emulateGeolocation} from '../../src/tools/emulation.js';
import {withMcpContext} from '../utils.js';

describe('emulation', () => {
Expand Down Expand Up @@ -152,4 +152,100 @@ describe('emulation', () => {
});
});
});

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

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

// Then clear it by omitting both params
await emulateGeolocation.handler(
{
params: {},
},
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 emulateGeolocation.handler(
{
params: {
latitude: 48.137154,
},
},
response,
context,
);
},
{
message:
'Both latitude and longitude must be provided, or both must be omitted to clear the override.',
},
);
});
});

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