Skip to content

Commit 8c875af

Browse files
feat: report new URL after actions that trigger navigation
Input tools (click, fill, press_key, hover, drag, type_text, fill_form, click_at) and evaluate_script now append a "Page navigated to <url>." line to the response when the action triggers a cross-document navigation. WaitForHelper.waitForEventsAfterAction returns {navigatedToUrl?: string} instead of void. The URL is detected by comparing page.url() before and after the action, which is robust against timing variations in navigation-start detection. A shared appendWaitForResult helper is exposed from WaitForHelper so input.ts and script.ts can both render the navigation line without duplicating the logic. Fixes #243
1 parent a90378a commit 8c875af

6 files changed

Lines changed: 125 additions & 19 deletions

File tree

src/McpPage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import type {
2828
import {
2929
getNetworkMultiplierFromString,
3030
WaitForHelper,
31+
type WaitForEventsResult,
3132
} from './WaitForHelper.js';
3233

3334
/**
@@ -132,7 +133,7 @@ export class McpPage implements ContextPage {
132133
waitForEventsAfterAction(
133134
action: () => Promise<unknown>,
134135
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
135-
): Promise<void> {
136+
): Promise<WaitForEventsResult> {
136137
const helper = this.createWaitForHelper(
137138
this.cpuThrottlingRate,
138139
getNetworkMultiplierFromString(this.networkConditions),

src/WaitForHelper.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class WaitForHelper {
127127
async waitForEventsAfterAction(
128128
action: () => Promise<unknown>,
129129
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
130-
): Promise<void> {
130+
): Promise<WaitForEventsResult> {
131131
let dialogOpened = false;
132132
if (options?.handleDialog) {
133133
const dialogHandler = (dialog: Pick<Dialog, 'accept' | 'dismiss'>) => {
@@ -146,6 +146,7 @@ export class WaitForHelper {
146146
});
147147
}
148148

149+
const urlBeforeAction = this.#page.url();
149150
const navigationFinished = this.waitForNavigationStarted()
150151
.then(navigationStated => {
151152
if (navigationStated) {
@@ -170,7 +171,7 @@ export class WaitForHelper {
170171
await navigationFinished;
171172

172173
if (dialogOpened) {
173-
return;
174+
return {};
174175
}
175176

176177
// Wait for stable dom after navigation so we execute in
@@ -181,6 +182,30 @@ export class WaitForHelper {
181182
} finally {
182183
this.#abortController.abort();
183184
}
185+
186+
const urlAfterAction = this.#page.url();
187+
return {
188+
...(urlAfterAction !== urlBeforeAction
189+
? {navigatedToUrl: urlAfterAction}
190+
: {}),
191+
};
192+
}
193+
}
194+
195+
export interface WaitForEventsResult {
196+
/**
197+
* The URL the page navigated to during the action, if a navigation
198+
* occurred.
199+
*/
200+
navigatedToUrl?: string;
201+
}
202+
203+
export function appendWaitForResult(
204+
response: {appendResponseLine(value: string): void},
205+
result: WaitForEventsResult,
206+
): void {
207+
if (result.navigatedToUrl) {
208+
response.appendResponseLine(`Page navigated to ${result.navigatedToUrl}.`);
184209
}
185210
}
186211

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
ExtensionServiceWorker,
2525
} from '../types.js';
2626
import type {PaginationOptions} from '../utils/types.js';
27+
import type {WaitForEventsResult} from '../WaitForHelper.js';
2728

2829
import type {ToolCategory} from './categories.js';
2930
import type {
@@ -260,7 +261,7 @@ export type ContextPage = Readonly<{
260261
waitForEventsAfterAction(
261262
action: () => Promise<unknown>,
262263
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
263-
): Promise<void>;
264+
): Promise<WaitForEventsResult>;
264265
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
265266
executeInPageTool(
266267
toolName: string,

src/tools/input.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +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 {
14+
appendWaitForResult,
15+
type WaitForEventsResult,
16+
} from '../WaitForHelper.js';
1317

1418
import {ToolCategory} from './categories.js';
1519
import type {ContextPage} from './ToolDefinition.js';
@@ -63,7 +67,7 @@ export const click = definePageTool({
6367
const uid = request.params.uid;
6468
const handle = await request.page.getElementByUid(uid);
6569
try {
66-
await request.page.waitForEventsAfterAction(async () => {
70+
const result = await request.page.waitForEventsAfterAction(async () => {
6771
await handle.asLocator().click({
6872
count: request.params.dblClick ? 2 : 1,
6973
});
@@ -73,6 +77,7 @@ export const click = definePageTool({
7377
? `Successfully double clicked on the element`
7478
: `Successfully clicked on the element`,
7579
);
80+
appendWaitForResult(response, result);
7681
if (request.params.includeSnapshot) {
7782
response.includeSnapshot();
7883
}
@@ -101,7 +106,7 @@ export const clickAt = definePageTool({
101106
blockedByDialog: true,
102107
handler: async (request, response) => {
103108
const page = request.page;
104-
await page.waitForEventsAfterAction(async () => {
109+
const result = await page.waitForEventsAfterAction(async () => {
105110
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
106111
clickCount: request.params.dblClick ? 2 : 1,
107112
});
@@ -111,6 +116,7 @@ export const clickAt = definePageTool({
111116
? `Successfully double clicked at the coordinates`
112117
: `Successfully clicked at the coordinates`,
113118
);
119+
appendWaitForResult(response, result);
114120
if (request.params.includeSnapshot) {
115121
response.includeSnapshot();
116122
}
@@ -137,10 +143,11 @@ export const hover = definePageTool({
137143
const uid = request.params.uid;
138144
const handle = await request.page.getElementByUid(uid);
139145
try {
140-
await request.page.waitForEventsAfterAction(async () => {
146+
const result = await request.page.waitForEventsAfterAction(async () => {
141147
await handle.asLocator().hover();
142148
});
143149
response.appendResponseLine(`Successfully hovered over the element`);
150+
appendWaitForResult(response, result);
144151
if (request.params.includeSnapshot) {
145152
response.includeSnapshot();
146153
}
@@ -239,7 +246,7 @@ export const fill = definePageTool({
239246
blockedByDialog: true,
240247
handler: async (request, response, context) => {
241248
const page = request.page;
242-
await page.waitForEventsAfterAction(async () => {
249+
const result = await page.waitForEventsAfterAction(async () => {
243250
await fillFormElement(
244251
request.params.uid,
245252
request.params.value,
@@ -248,6 +255,7 @@ export const fill = definePageTool({
248255
);
249256
});
250257
response.appendResponseLine(`Successfully filled out the element`);
258+
appendWaitForResult(response, result);
251259
if (request.params.includeSnapshot) {
252260
response.includeSnapshot();
253261
}
@@ -268,7 +276,7 @@ export const typeText = definePageTool({
268276
blockedByDialog: true,
269277
handler: async (request, response) => {
270278
const page = request.page;
271-
await page.waitForEventsAfterAction(async () => {
279+
const result = await page.waitForEventsAfterAction(async () => {
272280
await page.pptrPage.keyboard.type(request.params.text);
273281
if (request.params.submitKey) {
274282
await page.pptrPage.keyboard.press(
@@ -279,6 +287,7 @@ export const typeText = definePageTool({
279287
response.appendResponseLine(
280288
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
281289
);
290+
appendWaitForResult(response, result);
282291
},
283292
});
284293

@@ -301,12 +310,13 @@ export const drag = definePageTool({
301310
);
302311
const toHandle = await request.page.getElementByUid(request.params.to_uid);
303312
try {
304-
await request.page.waitForEventsAfterAction(async () => {
313+
const result = await request.page.waitForEventsAfterAction(async () => {
305314
await fromHandle.drag(toHandle);
306315
await new Promise(resolve => setTimeout(resolve, 50));
307316
await toHandle.drop(fromHandle);
308317
});
309318
response.appendResponseLine(`Successfully dragged an element`);
319+
appendWaitForResult(response, result);
310320
if (request.params.includeSnapshot) {
311321
response.includeSnapshot();
312322
}
@@ -339,8 +349,9 @@ export const fillForm = definePageTool({
339349
blockedByDialog: true,
340350
handler: async (request, response, context) => {
341351
const page = request.page;
352+
let lastResult: WaitForEventsResult = {};
342353
for (const element of request.params.elements) {
343-
await page.waitForEventsAfterAction(async () => {
354+
lastResult = await page.waitForEventsAfterAction(async () => {
344355
await fillFormElement(
345356
element.uid,
346357
element.value,
@@ -350,6 +361,7 @@ export const fillForm = definePageTool({
350361
});
351362
}
352363
response.appendResponseLine(`Successfully filled out the form`);
364+
appendWaitForResult(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+
appendWaitForResult(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 {appendWaitForResult} 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+
appendWaitForResult(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+
appendWaitForResult(response, result);
119124
} finally {
120125
void Promise.allSettled(args.map(arg => arg.dispose()));
121126
}

tests/tools/input.test.ts

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

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

0 commit comments

Comments
 (0)