Skip to content

Commit 5a3832c

Browse files
feat: report new URL after actions that trigger navigation
Click, fill, press_key, drag, hover, type_text, and evaluate_script already wait for cross-document navigations triggered by the action, but the resulting URL was never surfaced to the agent. Callers had to follow up with list_pages to figure out where they landed. Propagate a {navigated: boolean} result out of WaitForHelper up through McpPage so each handler can append a "Page navigated to <url>." line when the action actually caused a navigation. Same-document (history API) navigations are still filtered out by waitForNavigationStarted, matching existing behavior. Fixes #243 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 728d902 commit 5a3832c

File tree

6 files changed

+116
-18
lines changed

6 files changed

+116
-18
lines changed

src/McpPage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
import {
2323
getNetworkMultiplierFromString,
2424
WaitForHelper,
25+
type WaitForEventsResult,
2526
} from './WaitForHelper.js';
2627

2728
/**
@@ -113,7 +114,7 @@ export class McpPage implements ContextPage {
113114
waitForEventsAfterAction(
114115
action: () => Promise<unknown>,
115116
options?: {timeout?: number},
116-
): Promise<void> {
117+
): Promise<WaitForEventsResult> {
117118
const helper = this.createWaitForHelper(
118119
this.cpuThrottlingRate,
119120
getNetworkMultiplierFromString(this.networkConditions),

src/WaitForHelper.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,12 @@ export class WaitForHelper {
127127
async waitForEventsAfterAction(
128128
action: () => Promise<unknown>,
129129
options?: {timeout?: number},
130-
): Promise<void> {
130+
): Promise<WaitForEventsResult> {
131+
let navigated = false;
131132
const navigationFinished = this.waitForNavigationStarted()
132133
.then(navigationStated => {
133134
if (navigationStated) {
135+
navigated = true;
134136
return this.#page.waitForNavigation({
135137
timeout: options?.timeout ?? this.#navigationTimeout,
136138
signal: this.#abortController.signal,
@@ -159,9 +161,19 @@ export class WaitForHelper {
159161
} finally {
160162
this.#abortController.abort();
161163
}
164+
165+
return {navigated};
162166
}
163167
}
164168

169+
export interface WaitForEventsResult {
170+
/**
171+
* Whether a cross-document navigation started and finished during the
172+
* action. Same-document (history API) navigations are not reported.
173+
*/
174+
navigated: boolean;
175+
}
176+
165177
export function getNetworkMultiplierFromString(
166178
condition: string | null,
167179
): number {

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
} from '../types.js';
2323
import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
2424
import type {PaginationOptions} from '../utils/types.js';
25+
import type {WaitForEventsResult} from '../WaitForHelper.js';
2526

2627
import type {ToolCategory} from './categories.js';
2728
import type {
@@ -211,7 +212,7 @@ export type ContextPage = Readonly<{
211212
waitForEventsAfterAction(
212213
action: () => Promise<unknown>,
213214
options?: {timeout?: number},
214-
): Promise<void>;
215+
): Promise<WaitForEventsResult>;
215216
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
216217
}>;
217218

src/tools/input.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ 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 type {WaitForEventsResult} from '../WaitForHelper.js';
1314

1415
import {ToolCategory} from './categories.js';
15-
import type {ContextPage} from './ToolDefinition.js';
16+
import type {ContextPage, Response} from './ToolDefinition.js';
1617
import {definePageTool} from './ToolDefinition.js';
1718

1819
const dblClickSchema = zod
@@ -42,6 +43,16 @@ function handleActionError(error: unknown, uid: string) {
4243
);
4344
}
4445

46+
function appendNavigationIfAny(
47+
page: ContextPage,
48+
response: Response,
49+
result: WaitForEventsResult,
50+
) {
51+
if (result.navigated) {
52+
response.appendResponseLine(`Page navigated to ${page.pptrPage.url()}.`);
53+
}
54+
}
55+
4556
export const click = definePageTool({
4657
name: 'click',
4758
description: `Clicks on the provided element`,
@@ -62,7 +73,7 @@ export const click = definePageTool({
6273
const uid = request.params.uid;
6374
const handle = await request.page.getElementByUid(uid);
6475
try {
65-
await request.page.waitForEventsAfterAction(async () => {
76+
const result = await request.page.waitForEventsAfterAction(async () => {
6677
await handle.asLocator().click({
6778
count: request.params.dblClick ? 2 : 1,
6879
});
@@ -72,6 +83,7 @@ export const click = definePageTool({
7283
? `Successfully double clicked on the element`
7384
: `Successfully clicked on the element`,
7485
);
86+
appendNavigationIfAny(request.page, response, result);
7587
if (request.params.includeSnapshot) {
7688
response.includeSnapshot();
7789
}
@@ -99,7 +111,7 @@ export const clickAt = definePageTool({
99111
},
100112
handler: async (request, response) => {
101113
const page = request.page;
102-
await page.waitForEventsAfterAction(async () => {
114+
const result = await page.waitForEventsAfterAction(async () => {
103115
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
104116
clickCount: request.params.dblClick ? 2 : 1,
105117
});
@@ -109,6 +121,7 @@ export const clickAt = definePageTool({
109121
? `Successfully double clicked at the coordinates`
110122
: `Successfully clicked at the coordinates`,
111123
);
124+
appendNavigationIfAny(page, response, result);
112125
if (request.params.includeSnapshot) {
113126
response.includeSnapshot();
114127
}
@@ -134,10 +147,11 @@ export const hover = definePageTool({
134147
const uid = request.params.uid;
135148
const handle = await request.page.getElementByUid(uid);
136149
try {
137-
await request.page.waitForEventsAfterAction(async () => {
150+
const result = await request.page.waitForEventsAfterAction(async () => {
138151
await handle.asLocator().hover();
139152
});
140153
response.appendResponseLine(`Successfully hovered over the element`);
154+
appendNavigationIfAny(request.page, response, result);
141155
if (request.params.includeSnapshot) {
142156
response.includeSnapshot();
143157
}
@@ -235,7 +249,7 @@ export const fill = definePageTool({
235249
},
236250
handler: async (request, response, context) => {
237251
const page = request.page;
238-
await page.waitForEventsAfterAction(async () => {
252+
const result = await page.waitForEventsAfterAction(async () => {
239253
await fillFormElement(
240254
request.params.uid,
241255
request.params.value,
@@ -244,6 +258,7 @@ export const fill = definePageTool({
244258
);
245259
});
246260
response.appendResponseLine(`Successfully filled out the element`);
261+
appendNavigationIfAny(page, response, result);
247262
if (request.params.includeSnapshot) {
248263
response.includeSnapshot();
249264
}
@@ -263,7 +278,7 @@ export const typeText = definePageTool({
263278
},
264279
handler: async (request, response) => {
265280
const page = request.page;
266-
await page.waitForEventsAfterAction(async () => {
281+
const result = await page.waitForEventsAfterAction(async () => {
267282
await page.pptrPage.keyboard.type(request.params.text);
268283
if (request.params.submitKey) {
269284
await page.pptrPage.keyboard.press(
@@ -274,6 +289,7 @@ export const typeText = definePageTool({
274289
response.appendResponseLine(
275290
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
276291
);
292+
appendNavigationIfAny(page, response, result);
277293
},
278294
});
279295

@@ -295,12 +311,13 @@ export const drag = definePageTool({
295311
);
296312
const toHandle = await request.page.getElementByUid(request.params.to_uid);
297313
try {
298-
await request.page.waitForEventsAfterAction(async () => {
314+
const result = await request.page.waitForEventsAfterAction(async () => {
299315
await fromHandle.drag(toHandle);
300316
await new Promise(resolve => setTimeout(resolve, 50));
301317
await toHandle.drop(fromHandle);
302318
});
303319
response.appendResponseLine(`Successfully dragged an element`);
320+
appendNavigationIfAny(request.page, response, result);
304321
if (request.params.includeSnapshot) {
305322
response.includeSnapshot();
306323
}
@@ -332,8 +349,9 @@ export const fillForm = definePageTool({
332349
},
333350
handler: async (request, response, context) => {
334351
const page = request.page;
352+
let lastResult: WaitForEventsResult = {navigated: false};
335353
for (const element of request.params.elements) {
336-
await page.waitForEventsAfterAction(async () => {
354+
lastResult = await page.waitForEventsAfterAction(async () => {
337355
await fillFormElement(
338356
element.uid,
339357
element.value,
@@ -343,6 +361,7 @@ export const fillForm = definePageTool({
343361
});
344362
}
345363
response.appendResponseLine(`Successfully filled out the form`);
364+
appendNavigationIfAny(page, response, lastResult);
346365
if (request.params.includeSnapshot) {
347366
response.includeSnapshot();
348367
}
@@ -419,7 +438,7 @@ export const pressKey = definePageTool({
419438
const tokens = parseKey(request.params.key);
420439
const [key, ...modifiers] = tokens;
421440

422-
await page.waitForEventsAfterAction(async () => {
441+
const result = await page.waitForEventsAfterAction(async () => {
423442
for (const modifier of modifiers) {
424443
await page.pptrPage.keyboard.down(modifier);
425444
}
@@ -432,6 +451,7 @@ export const pressKey = definePageTool({
432451
response.appendResponseLine(
433452
`Successfully pressed key: ${request.params.key}`,
434453
);
454+
appendNavigationIfAny(page, response, result);
435455
if (request.params.includeSnapshot) {
436456
response.includeSnapshot();
437457
}

src/tools/script.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,15 @@ Example with arguments: \`(el) => {
7777
}
7878

7979
const worker = await getWebWorker(context, serviceWorkerId);
80-
await context
81-
.getSelectedMcpPage()
82-
.waitForEventsAfterAction(async () => {
83-
await performEvaluation(worker, fnString, [], response);
84-
});
80+
const selectedPage = context.getSelectedMcpPage();
81+
const result = await selectedPage.waitForEventsAfterAction(async () => {
82+
await performEvaluation(worker, fnString, [], response);
83+
});
84+
if (result.navigated) {
85+
response.appendResponseLine(
86+
`Page navigated to ${selectedPage.pptrPage.url()}.`,
87+
);
88+
}
8589
return;
8690
}
8791

@@ -101,9 +105,12 @@ Example with arguments: \`(el) => {
101105

102106
const evaluatable = await getPageOrFrame(page, frames);
103107

104-
await mcpPage.waitForEventsAfterAction(async () => {
108+
const result = await mcpPage.waitForEventsAfterAction(async () => {
105109
await performEvaluation(evaluatable, fnString, args, response);
106110
});
111+
if (result.navigated) {
112+
response.appendResponseLine(`Page navigated to ${page.url()}.`);
113+
}
107114
} finally {
108115
void Promise.allSettled(args.map(arg => arg.dispose()));
109116
}

tests/tools/input.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,63 @@ describe('input', () => {
123123
});
124124
});
125125

126+
it('reports the new URL when click triggers a navigation', async () => {
127+
server.addHtmlRoute(
128+
'/start',
129+
html`<a href="/after-click">Navigate page</a>`,
130+
);
131+
server.addHtmlRoute('/after-click', html`<main>arrived</main>`);
132+
133+
await withMcpContext(async (response, context) => {
134+
const page = context.getSelectedPptrPage();
135+
await page.goto(server.getRoute('/start'));
136+
await context.createTextSnapshot(context.getSelectedMcpPage());
137+
await click.handler(
138+
{
139+
params: {
140+
uid: '1_1',
141+
},
142+
page: context.getSelectedMcpPage(),
143+
},
144+
response,
145+
context,
146+
);
147+
const expectedUrl = server.getRoute('/after-click');
148+
assert.ok(
149+
response.responseLines.some(
150+
line => line === `Page navigated to ${expectedUrl}.`,
151+
),
152+
`Expected response to mention navigation to ${expectedUrl}, got: ${response.responseLines.join(' | ')}`,
153+
);
154+
});
155+
});
156+
157+
it('does not report navigation when click does not navigate', async () => {
158+
await withMcpContext(async (response, context) => {
159+
const page = context.getSelectedPptrPage();
160+
await page.setContent(
161+
html`<button onclick="this.innerText = 'clicked';">test</button>`,
162+
);
163+
await context.createTextSnapshot(context.getSelectedMcpPage());
164+
await click.handler(
165+
{
166+
params: {
167+
uid: '1_1',
168+
},
169+
page: context.getSelectedMcpPage(),
170+
},
171+
response,
172+
context,
173+
);
174+
assert.ok(
175+
!response.responseLines.some(line =>
176+
line.startsWith('Page navigated to '),
177+
),
178+
`Did not expect a navigation line, got: ${response.responseLines.join(' | ')}`,
179+
);
180+
});
181+
});
182+
126183
it('waits for stable DOM', async () => {
127184
server.addHtmlRoute(
128185
'/unstable',

0 commit comments

Comments
 (0)