Skip to content

Commit b78a69b

Browse files
committed
feat: report navigated URL in action responses
Report the new page URL when an action (click, fill, press_key, etc.) triggers a navigation. The URL is detected by comparing the page URL before and after waitForEventsAfterAction. Fixes #243
1 parent b2b3e99 commit b78a69b

6 files changed

Lines changed: 103 additions & 19 deletions

File tree

src/McpPage.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,18 @@ export class McpPage implements ContextPage {
129129
return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
130130
}
131131

132-
waitForEventsAfterAction(
132+
async waitForEventsAfterAction(
133133
action: () => Promise<unknown>,
134134
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
135-
): Promise<void> {
135+
): Promise<{navigatedToUrl?: string}> {
136+
const urlBefore = this.pptrPage.url();
136137
const helper = this.createWaitForHelper(
137138
this.cpuThrottlingRate,
138139
getNetworkMultiplierFromString(this.networkConditions),
139140
);
140-
return helper.waitForEventsAfterAction(action, options);
141+
await helper.waitForEventsAfterAction(action, options);
142+
const urlAfter = this.pptrPage.url();
143+
return urlAfter === urlBefore ? {} : {navigatedToUrl: urlAfter};
141144
}
142145

143146
dispose(): void {

src/WaitForHelper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,12 @@ export function getNetworkMultiplierFromString(
202202
}
203203
return 1;
204204
}
205+
206+
export function appendNavigatedToUrl(
207+
response: {appendResponseLine(value: string): void},
208+
result: {navigatedToUrl?: string},
209+
): void {
210+
if (result.navigatedToUrl) {
211+
response.appendResponseLine(`Navigated to ${result.navigatedToUrl}`);
212+
}
213+
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ export type ContextPage = Readonly<{
260260
waitForEventsAfterAction(
261261
action: () => Promise<unknown>,
262262
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
263-
): Promise<void>;
263+
): Promise<{navigatedToUrl?: string}>;
264264
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
265265
executeInPageTool(
266266
toolName: string,

src/tools/input.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {zod} from '../third_party/index.js';
1010
import type {ElementHandle, KeyInput} from '../third_party/index.js';
1111
import type {TextSnapshotNode} from '../types.js';
1212
import {parseKey} from '../utils/keyboard.js';
13+
import {appendNavigatedToUrl} from '../WaitForHelper.js';
1314

1415
import {ToolCategory} from './categories.js';
1516
import type {ContextPage} from './ToolDefinition.js';
@@ -63,7 +64,7 @@ export const click = definePageTool({
6364
const uid = request.params.uid;
6465
const handle = await request.page.getElementByUid(uid);
6566
try {
66-
await request.page.waitForEventsAfterAction(async () => {
67+
const result = await request.page.waitForEventsAfterAction(async () => {
6768
await handle.asLocator().click({
6869
count: request.params.dblClick ? 2 : 1,
6970
});
@@ -73,6 +74,7 @@ export const click = definePageTool({
7374
? `Successfully double clicked on the element`
7475
: `Successfully clicked on the element`,
7576
);
77+
appendNavigatedToUrl(response, result);
7678
if (request.params.includeSnapshot) {
7779
response.includeSnapshot();
7880
}
@@ -101,7 +103,7 @@ export const clickAt = definePageTool({
101103
blockedByDialog: true,
102104
handler: async (request, response) => {
103105
const page = request.page;
104-
await page.waitForEventsAfterAction(async () => {
106+
const result = await page.waitForEventsAfterAction(async () => {
105107
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
106108
clickCount: request.params.dblClick ? 2 : 1,
107109
});
@@ -111,6 +113,7 @@ export const clickAt = definePageTool({
111113
? `Successfully double clicked at the coordinates`
112114
: `Successfully clicked at the coordinates`,
113115
);
116+
appendNavigatedToUrl(response, result);
114117
if (request.params.includeSnapshot) {
115118
response.includeSnapshot();
116119
}
@@ -137,10 +140,11 @@ export const hover = definePageTool({
137140
const uid = request.params.uid;
138141
const handle = await request.page.getElementByUid(uid);
139142
try {
140-
await request.page.waitForEventsAfterAction(async () => {
143+
const result = await request.page.waitForEventsAfterAction(async () => {
141144
await handle.asLocator().hover();
142145
});
143146
response.appendResponseLine(`Successfully hovered over the element`);
147+
appendNavigatedToUrl(response, result);
144148
if (request.params.includeSnapshot) {
145149
response.includeSnapshot();
146150
}
@@ -239,7 +243,7 @@ export const fill = definePageTool({
239243
blockedByDialog: true,
240244
handler: async (request, response, context) => {
241245
const page = request.page;
242-
await page.waitForEventsAfterAction(async () => {
246+
const result = await page.waitForEventsAfterAction(async () => {
243247
await fillFormElement(
244248
request.params.uid,
245249
request.params.value,
@@ -248,6 +252,7 @@ export const fill = definePageTool({
248252
);
249253
});
250254
response.appendResponseLine(`Successfully filled out the element`);
255+
appendNavigatedToUrl(response, result);
251256
if (request.params.includeSnapshot) {
252257
response.includeSnapshot();
253258
}
@@ -268,7 +273,7 @@ export const typeText = definePageTool({
268273
blockedByDialog: true,
269274
handler: async (request, response) => {
270275
const page = request.page;
271-
await page.waitForEventsAfterAction(async () => {
276+
const result = await page.waitForEventsAfterAction(async () => {
272277
await page.pptrPage.keyboard.type(request.params.text);
273278
if (request.params.submitKey) {
274279
await page.pptrPage.keyboard.press(
@@ -279,6 +284,7 @@ export const typeText = definePageTool({
279284
response.appendResponseLine(
280285
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
281286
);
287+
appendNavigatedToUrl(response, result);
282288
},
283289
});
284290

@@ -301,12 +307,13 @@ export const drag = definePageTool({
301307
);
302308
const toHandle = await request.page.getElementByUid(request.params.to_uid);
303309
try {
304-
await request.page.waitForEventsAfterAction(async () => {
310+
const result = await request.page.waitForEventsAfterAction(async () => {
305311
await fromHandle.drag(toHandle);
306312
await new Promise(resolve => setTimeout(resolve, 50));
307313
await toHandle.drop(fromHandle);
308314
});
309315
response.appendResponseLine(`Successfully dragged an element`);
316+
appendNavigatedToUrl(response, result);
310317
if (request.params.includeSnapshot) {
311318
response.includeSnapshot();
312319
}
@@ -339,17 +346,22 @@ export const fillForm = definePageTool({
339346
blockedByDialog: true,
340347
handler: async (request, response, context) => {
341348
const page = request.page;
349+
let lastResult: {navigatedToUrl?: string} = {};
342350
for (const element of request.params.elements) {
343-
await page.waitForEventsAfterAction(async () => {
351+
const result = await page.waitForEventsAfterAction(async () => {
344352
await fillFormElement(
345353
element.uid,
346354
element.value,
347355
context as McpContext,
348356
page,
349357
);
350358
});
359+
if (result.navigatedToUrl) {
360+
lastResult = result;
361+
}
351362
}
352363
response.appendResponseLine(`Successfully filled out the form`);
364+
appendNavigatedToUrl(response, lastResult);
353365
if (request.params.includeSnapshot) {
354366
response.includeSnapshot();
355367
}
@@ -429,7 +441,7 @@ export const pressKey = definePageTool({
429441
const tokens = parseKey(request.params.key);
430442
const [key, ...modifiers] = tokens;
431443

432-
await page.waitForEventsAfterAction(async () => {
444+
const result = await page.waitForEventsAfterAction(async () => {
433445
for (const modifier of modifiers) {
434446
await page.pptrPage.keyboard.down(modifier);
435447
}
@@ -442,6 +454,7 @@ export const pressKey = definePageTool({
442454
response.appendResponseLine(
443455
`Successfully pressed key: ${request.params.key}`,
444456
);
457+
appendNavigatedToUrl(response, result);
445458
if (request.params.includeSnapshot) {
446459
response.includeSnapshot();
447460
}

src/tools/script.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import {zod} from '../third_party/index.js';
88
import type {Frame, JSHandle, Page, WebWorker} from '../third_party/index.js';
99
import type {ExtensionServiceWorker} from '../types.js';
10+
import {appendNavigatedToUrl} from '../WaitForHelper.js';
1011

1112
import {ToolCategory} from './categories.js';
1213
import type {Context, Response} from './ToolDefinition.js';
@@ -85,12 +86,15 @@ Example with arguments: \`(el) => {
8586
}
8687

8788
const worker = await getWebWorker(context, serviceWorkerId);
88-
await context.getSelectedMcpPage().waitForEventsAfterAction(
89-
async () => {
90-
await performEvaluation(worker, fnString, [], response);
91-
},
92-
{handleDialog: dialogAction ?? 'accept'},
93-
);
89+
const result = await context
90+
.getSelectedMcpPage()
91+
.waitForEventsAfterAction(
92+
async () => {
93+
await performEvaluation(worker, fnString, [], response);
94+
},
95+
{handleDialog: dialogAction ?? 'accept'},
96+
);
97+
appendNavigatedToUrl(response, result);
9498
return;
9599
}
96100

@@ -110,12 +114,13 @@ Example with arguments: \`(el) => {
110114

111115
const evaluatable = await getPageOrFrame(page, frames);
112116

113-
await mcpPage.waitForEventsAfterAction(
117+
const result = await mcpPage.waitForEventsAfterAction(
114118
async () => {
115119
await performEvaluation(evaluatable, fnString, args, response);
116120
},
117121
{handleDialog: dialogAction ?? 'accept'},
118122
);
123+
appendNavigatedToUrl(response, result);
119124
} finally {
120125
void Promise.allSettled(args.map(arg => arg.dispose()));
121126
}

tests/tools/input.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,60 @@ describe('input', () => {
130130
});
131131
});
132132

133+
it('reports navigated URL after click', async () => {
134+
server.addHtmlRoute('/nav-link', html`<a href="/nav-target">Go</a>`);
135+
server.addHtmlRoute('/nav-target', html`<main>Target</main>`);
136+
137+
await withMcpContext(async (response, context) => {
138+
const page = context.getSelectedPptrPage();
139+
await page.goto(server.getRoute('/nav-link'));
140+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
141+
context.getSelectedMcpPage(),
142+
);
143+
await click.handler(
144+
{
145+
params: {uid: '1_1'},
146+
page: context.getSelectedMcpPage(),
147+
},
148+
response,
149+
context,
150+
);
151+
assert.strictEqual(
152+
response.responseLines[0],
153+
'Successfully clicked on the element',
154+
);
155+
assert.strictEqual(
156+
response.responseLines[1],
157+
`Navigated to ${server.getRoute('/nav-target')}`,
158+
);
159+
});
160+
});
161+
162+
it('does not report navigated URL when no navigation occurs', async () => {
163+
await withMcpContext(async (response, context) => {
164+
const page = context.getSelectedPptrPage();
165+
await page.setContent(
166+
html`<button onclick="this.innerText = 'clicked';">test</button>`,
167+
);
168+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
169+
context.getSelectedMcpPage(),
170+
);
171+
await click.handler(
172+
{
173+
params: {uid: '1_1'},
174+
page: context.getSelectedMcpPage(),
175+
},
176+
response,
177+
context,
178+
);
179+
assert.strictEqual(response.responseLines.length, 1);
180+
assert.strictEqual(
181+
response.responseLines[0],
182+
'Successfully clicked on the element',
183+
);
184+
});
185+
});
186+
133187
it('waits for stable DOM', async () => {
134188
server.addHtmlRoute(
135189
'/unstable',

0 commit comments

Comments
 (0)