Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 9 additions & 3 deletions src/McpPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,21 @@ export class McpPage implements ContextPage {
return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
}

waitForEventsAfterAction(
async waitForEventsAfterAction(
action: () => Promise<unknown>,
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<void> {
): Promise<{navigatedToUrl?: string}> {
const urlBefore = this.pptrPage.url();
const helper = this.createWaitForHelper(
this.cpuThrottlingRate,
getNetworkMultiplierFromString(this.networkConditions),
);
return helper.waitForEventsAfterAction(action, options);
await helper.waitForEventsAfterAction(action, options);
const urlAfter = this.pptrPage.url();
if (urlAfter !== urlBefore) {
return {navigatedToUrl: urlAfter};
}
return {};
}

dispose(): void {
Expand Down
9 changes: 9 additions & 0 deletions src/WaitForHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,12 @@ export function getNetworkMultiplierFromString(
}
return 1;
}

export function appendNavigatedToUrl(
response: {appendResponseLine(value: string): void},
result: {navigatedToUrl?: string},
): void {
if (result.navigatedToUrl) {
response.appendResponseLine(`Navigated to ${result.navigatedToUrl}`);
}
}
2 changes: 1 addition & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ export type ContextPage = Readonly<{
waitForEventsAfterAction(
action: () => Promise<unknown>,
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
): Promise<void>;
): Promise<{navigatedToUrl?: string}>;
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
executeInPageTool(
toolName: string,
Expand Down
29 changes: 21 additions & 8 deletions src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {zod} from '../third_party/index.js';
import type {ElementHandle, KeyInput} from '../third_party/index.js';
import type {TextSnapshotNode} from '../types.js';
import {parseKey} from '../utils/keyboard.js';
import {appendNavigatedToUrl} from '../WaitForHelper.js';

import {ToolCategory} from './categories.js';
import type {ContextPage} from './ToolDefinition.js';
Expand Down Expand Up @@ -62,7 +63,7 @@ export const click = definePageTool({
const uid = request.params.uid;
const handle = await request.page.getElementByUid(uid);
try {
await request.page.waitForEventsAfterAction(async () => {
const result = await request.page.waitForEventsAfterAction(async () => {
await handle.asLocator().click({
count: request.params.dblClick ? 2 : 1,
});
Expand All @@ -72,6 +73,7 @@ export const click = definePageTool({
? `Successfully double clicked on the element`
: `Successfully clicked on the element`,
);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand Down Expand Up @@ -99,7 +101,7 @@ export const clickAt = definePageTool({
},
handler: async (request, response) => {
const page = request.page;
await page.waitForEventsAfterAction(async () => {
const result = await page.waitForEventsAfterAction(async () => {
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
clickCount: request.params.dblClick ? 2 : 1,
});
Expand All @@ -109,6 +111,7 @@ export const clickAt = definePageTool({
? `Successfully double clicked at the coordinates`
: `Successfully clicked at the coordinates`,
);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand All @@ -134,10 +137,11 @@ export const hover = definePageTool({
const uid = request.params.uid;
const handle = await request.page.getElementByUid(uid);
try {
await request.page.waitForEventsAfterAction(async () => {
const result = await request.page.waitForEventsAfterAction(async () => {
await handle.asLocator().hover();
});
response.appendResponseLine(`Successfully hovered over the element`);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand Down Expand Up @@ -235,7 +239,7 @@ export const fill = definePageTool({
},
handler: async (request, response, context) => {
const page = request.page;
await page.waitForEventsAfterAction(async () => {
const result = await page.waitForEventsAfterAction(async () => {
await fillFormElement(
request.params.uid,
request.params.value,
Expand All @@ -244,6 +248,7 @@ export const fill = definePageTool({
);
});
response.appendResponseLine(`Successfully filled out the element`);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand All @@ -263,7 +268,7 @@ export const typeText = definePageTool({
},
handler: async (request, response) => {
const page = request.page;
await page.waitForEventsAfterAction(async () => {
const result = await page.waitForEventsAfterAction(async () => {
await page.pptrPage.keyboard.type(request.params.text);
if (request.params.submitKey) {
await page.pptrPage.keyboard.press(
Expand All @@ -274,6 +279,7 @@ export const typeText = definePageTool({
response.appendResponseLine(
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
);
appendNavigatedToUrl(response, result);
},
});

Expand All @@ -295,12 +301,13 @@ export const drag = definePageTool({
);
const toHandle = await request.page.getElementByUid(request.params.to_uid);
try {
await request.page.waitForEventsAfterAction(async () => {
const result = await request.page.waitForEventsAfterAction(async () => {
await fromHandle.drag(toHandle);
await new Promise(resolve => setTimeout(resolve, 50));
await toHandle.drop(fromHandle);
});
response.appendResponseLine(`Successfully dragged an element`);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand Down Expand Up @@ -332,17 +339,22 @@ export const fillForm = definePageTool({
},
handler: async (request, response, context) => {
const page = request.page;
let lastResult: {navigatedToUrl?: string} = {};
for (const element of request.params.elements) {
await page.waitForEventsAfterAction(async () => {
const result = await page.waitForEventsAfterAction(async () => {
await fillFormElement(
element.uid,
element.value,
context as McpContext,
page,
);
});
if (result.navigatedToUrl) {
lastResult = result;
}
}
response.appendResponseLine(`Successfully filled out the form`);
appendNavigatedToUrl(response, lastResult);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand Down Expand Up @@ -419,7 +431,7 @@ export const pressKey = definePageTool({
const tokens = parseKey(request.params.key);
const [key, ...modifiers] = tokens;

await page.waitForEventsAfterAction(async () => {
const result = await page.waitForEventsAfterAction(async () => {
for (const modifier of modifiers) {
await page.pptrPage.keyboard.down(modifier);
}
Expand All @@ -432,6 +444,7 @@ export const pressKey = definePageTool({
response.appendResponseLine(
`Successfully pressed key: ${request.params.key}`,
);
appendNavigatedToUrl(response, result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
Expand Down
6 changes: 5 additions & 1 deletion src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ export const newPage = defineTool(args => {
request.params.timeout,
);

response.appendResponseLine(
`Successfully navigated to ${page.pptrPage.url()}.`,
);
response.setIncludePages(true);
response.setListInPageTools();
},
Expand Down Expand Up @@ -303,8 +306,9 @@ export const navigatePage = definePageTool(args => {
}
try {
await page.pptrPage.goto(request.params.url, options);
const finalUrl = page.pptrPage.url();
response.appendResponseLine(
`Successfully navigated to ${request.params.url}.`,
`Successfully navigated to ${finalUrl}.`,
);
} catch (error) {
response.appendResponseLine(
Expand Down
19 changes: 12 additions & 7 deletions src/tools/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import {zod} from '../third_party/index.js';
import type {Frame, JSHandle, Page, WebWorker} from '../third_party/index.js';
import type {ExtensionServiceWorker} from '../types.js';
import {appendNavigatedToUrl} from '../WaitForHelper.js';

import {ToolCategory} from './categories.js';
import type {Context, Response} from './ToolDefinition.js';
Expand Down Expand Up @@ -84,12 +85,15 @@ Example with arguments: \`(el) => {
}

const worker = await getWebWorker(context, serviceWorkerId);
await context.getSelectedMcpPage().waitForEventsAfterAction(
async () => {
await performEvaluation(worker, fnString, [], response);
},
{handleDialog: dialogAction ?? 'accept'},
);
const result = await context
.getSelectedMcpPage()
.waitForEventsAfterAction(
async () => {
await performEvaluation(worker, fnString, [], response);
},
{handleDialog: dialogAction ?? 'accept'},
);
appendNavigatedToUrl(response, result);
return;
}

Expand All @@ -109,12 +113,13 @@ Example with arguments: \`(el) => {

const evaluatable = await getPageOrFrame(page, frames);

await mcpPage.waitForEventsAfterAction(
const result = await mcpPage.waitForEventsAfterAction(
async () => {
await performEvaluation(evaluatable, fnString, args, response);
},
{handleDialog: dialogAction ?? 'accept'},
);
appendNavigatedToUrl(response, result);
} finally {
void Promise.allSettled(args.map(arg => arg.dispose()));
}
Expand Down
58 changes: 58 additions & 0 deletions tests/tools/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,64 @@ describe('input', () => {
});
});

it('reports navigated URL after click', async () => {
server.addHtmlRoute('/nav-link', html`<a href="/nav-target">Go</a>`);
server.addHtmlRoute('/nav-target', html`<main>Target</main>`);

await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.goto(server.getRoute('/nav-link'));
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
await click.handler(
{
params: {uid: '1_1'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully clicked on the element',
);
assert.ok(
response.responseLines[1]?.startsWith('Navigated to '),
`Expected "Navigated to" but got: ${response.responseLines[1]}`,
);
assert.ok(
response.responseLines[1]?.includes('/nav-target'),
`Expected URL to contain /nav-target but got: ${response.responseLines[1]}`,
);
});
});

it('does not report navigated URL when no navigation occurs', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(
html`<button onclick="this.innerText = 'clicked';">test</button>`,
);
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
await click.handler(
{
params: {uid: '1_1'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(response.responseLines.length, 1);
assert.strictEqual(
response.responseLines[0],
'Successfully clicked on the element',
);
});
});

it('waits for stable DOM', async () => {
server.addHtmlRoute(
'/unstable',
Expand Down
21 changes: 21 additions & 0 deletions tests/tools/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,27 @@ describe('pages', () => {
});
});

it('reports final URL after navigation', async () => {
await withMcpContext(async (response, context) => {
await navigatePage().handler(
{
params: {url: 'data:text/html,<div>Hello</div>'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.ok(
response.responseLines[0]?.startsWith('Successfully navigated to '),
`Expected "Successfully navigated to" but got: ${response.responseLines[0]}`,
);
assert.ok(
response.responseLines[0]?.includes('data:text/html'),
`Expected URL in response but got: ${response.responseLines[0]}`,
);
});
});

it('throws an error if the page was closed not by the MCP server', async () => {
await withMcpContext(async (response, context) => {
const page = await context.newPage();
Expand Down