Skip to content

Commit 897f3f4

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 895fc65 commit 897f3f4

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';
@@ -109,7 +110,7 @@ export const click = definePageTool({
109110
const shouldSelectNativeOption =
110111
!request.params.dblClick && aXNode?.role === 'option';
111112
try {
112-
await request.page.waitForEventsAfterAction(async () => {
113+
const result = await request.page.waitForEventsAfterAction(async () => {
113114
if (
114115
shouldSelectNativeOption &&
115116
(await selectNativeSelectOption(handle))
@@ -126,6 +127,7 @@ export const click = definePageTool({
126127
? `Successfully double clicked on the element`
127128
: `Successfully clicked on the element`,
128129
);
130+
appendNavigatedToUrl(response, result);
129131
if (request.params.includeSnapshot) {
130132
response.includeSnapshot();
131133
}
@@ -154,7 +156,7 @@ export const clickAt = definePageTool({
154156
blockedByDialog: true,
155157
handler: async (request, response) => {
156158
const page = request.page;
157-
await page.waitForEventsAfterAction(async () => {
159+
const result = await page.waitForEventsAfterAction(async () => {
158160
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
159161
clickCount: request.params.dblClick ? 2 : 1,
160162
});
@@ -164,6 +166,7 @@ export const clickAt = definePageTool({
164166
? `Successfully double clicked at the coordinates`
165167
: `Successfully clicked at the coordinates`,
166168
);
169+
appendNavigatedToUrl(response, result);
167170
if (request.params.includeSnapshot) {
168171
response.includeSnapshot();
169172
}
@@ -190,10 +193,11 @@ export const hover = definePageTool({
190193
const uid = request.params.uid;
191194
const handle = await request.page.getElementByUid(uid);
192195
try {
193-
await request.page.waitForEventsAfterAction(async () => {
196+
const result = await request.page.waitForEventsAfterAction(async () => {
194197
await handle.asLocator().hover();
195198
});
196199
response.appendResponseLine(`Successfully hovered over the element`);
200+
appendNavigatedToUrl(response, result);
197201
if (request.params.includeSnapshot) {
198202
response.includeSnapshot();
199203
}
@@ -292,7 +296,7 @@ export const fill = definePageTool({
292296
blockedByDialog: true,
293297
handler: async (request, response, context) => {
294298
const page = request.page;
295-
await page.waitForEventsAfterAction(async () => {
299+
const result = await page.waitForEventsAfterAction(async () => {
296300
await fillFormElement(
297301
request.params.uid,
298302
request.params.value,
@@ -301,6 +305,7 @@ export const fill = definePageTool({
301305
);
302306
});
303307
response.appendResponseLine(`Successfully filled out the element`);
308+
appendNavigatedToUrl(response, result);
304309
if (request.params.includeSnapshot) {
305310
response.includeSnapshot();
306311
}
@@ -321,7 +326,7 @@ export const typeText = definePageTool({
321326
blockedByDialog: true,
322327
handler: async (request, response) => {
323328
const page = request.page;
324-
await page.waitForEventsAfterAction(async () => {
329+
const result = await page.waitForEventsAfterAction(async () => {
325330
await page.pptrPage.keyboard.type(request.params.text);
326331
if (request.params.submitKey) {
327332
await page.pptrPage.keyboard.press(
@@ -332,6 +337,7 @@ export const typeText = definePageTool({
332337
response.appendResponseLine(
333338
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
334339
);
340+
appendNavigatedToUrl(response, result);
335341
},
336342
});
337343

@@ -354,12 +360,13 @@ export const drag = definePageTool({
354360
);
355361
const toHandle = await request.page.getElementByUid(request.params.to_uid);
356362
try {
357-
await request.page.waitForEventsAfterAction(async () => {
363+
const result = await request.page.waitForEventsAfterAction(async () => {
358364
await fromHandle.drag(toHandle);
359365
await new Promise(resolve => setTimeout(resolve, 50));
360366
await toHandle.drop(fromHandle);
361367
});
362368
response.appendResponseLine(`Successfully dragged an element`);
369+
appendNavigatedToUrl(response, result);
363370
if (request.params.includeSnapshot) {
364371
response.includeSnapshot();
365372
}
@@ -392,17 +399,22 @@ export const fillForm = definePageTool({
392399
blockedByDialog: true,
393400
handler: async (request, response, context) => {
394401
const page = request.page;
402+
let lastResult: {navigatedToUrl?: string} = {};
395403
for (const element of request.params.elements) {
396-
await page.waitForEventsAfterAction(async () => {
404+
const result = await page.waitForEventsAfterAction(async () => {
397405
await fillFormElement(
398406
element.uid,
399407
element.value,
400408
context as McpContext,
401409
page,
402410
);
403411
});
412+
if (result.navigatedToUrl) {
413+
lastResult = result;
414+
}
404415
}
405416
response.appendResponseLine(`Successfully filled out the form`);
417+
appendNavigatedToUrl(response, lastResult);
406418
if (request.params.includeSnapshot) {
407419
response.includeSnapshot();
408420
}
@@ -482,7 +494,7 @@ export const pressKey = definePageTool({
482494
const tokens = parseKey(request.params.key);
483495
const [key, ...modifiers] = tokens;
484496

485-
await page.waitForEventsAfterAction(async () => {
497+
const result = await page.waitForEventsAfterAction(async () => {
486498
for (const modifier of modifiers) {
487499
await page.pptrPage.keyboard.down(modifier);
488500
}
@@ -495,6 +507,7 @@ export const pressKey = definePageTool({
495507
response.appendResponseLine(
496508
`Successfully pressed key: ${request.params.key}`,
497509
);
510+
appendNavigatedToUrl(response, result);
498511
if (request.params.includeSnapshot) {
499512
response.includeSnapshot();
500513
}

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)