Skip to content
Merged
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
30 changes: 21 additions & 9 deletions src/tools/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {JSHandle} from 'puppeteer-core';
import type {Frame, JSHandle, Page} from 'puppeteer-core';

import {zod} from '../third_party/modelcontextprotocol-sdk/index.js';

Expand Down Expand Up @@ -45,15 +45,29 @@ Example with arguments: \`(el) => {
.describe(`An optional list of arguments to pass to the function.`),
},
handler: async (request, response, context) => {
const page = context.getSelectedPage();
const fn = await page.evaluateHandle(`(${request.params.function})`);
const args: Array<JSHandle<unknown>> = [fn];
const args: Array<JSHandle<unknown>> = [];
try {
const frames = new Set<Frame>();
for (const el of request.params.args ?? []) {
args.push(await context.getElementByUid(el.uid));
const handle = await context.getElementByUid(el.uid);
frames.add(handle.frame);
args.push(handle);
}
let pageOrFrame: Page | Frame;
// We can't evaluate the element handle across frames
if (frames.size > 1) {
throw new Error(
"Elements from different frames can't be evaluated together.",
);
} else {
pageOrFrame = [...frames.values()][0] ?? context.getSelectedPage();
}
const fn = await pageOrFrame.evaluateHandle(
`(${request.params.function})`,
);
args.unshift(fn);
await context.waitForEventsAfterAction(async () => {
const result = await page.evaluate(
const result = await pageOrFrame.evaluate(
async (fn, ...args) => {
// @ts-expect-error no types.
return JSON.stringify(await fn(...args));
Expand All @@ -66,9 +80,7 @@ Example with arguments: \`(el) => {
response.appendResponseLine('```');
});
} finally {
Promise.allSettled(args.map(arg => arg.dispose())).catch(() => {
// Ignore errors
});
void Promise.allSettled(args.map(arg => arg.dispose()));
}
},
});
34 changes: 34 additions & 0 deletions tests/tools/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import assert from 'node:assert';
import {describe, it} from 'node:test';

import {evaluateScript} from '../../src/tools/script.js';
import {serverHooks} from '../server.js';
import {html, withBrowser} from '../utils.js';

describe('script', () => {
const server = serverHooks();

describe('browser_evaluate_script', () => {
it('evaluates', async () => {
await withBrowser(async (response, context) => {
Expand Down Expand Up @@ -152,5 +155,36 @@ describe('script', () => {
assert.strictEqual(JSON.parse(lineEvaluation), true);
});
});

it('work for elements inside iframes', async () => {
server.addHtmlRoute(
'/iframe',
html`<main><button>I am iframe button</button></main>`,
);
server.addRoute('/main', async (_req, res) => {
res.write(html`<iframe src="/iframe"></iframe>`);
res.end();
});

await withBrowser(async (response, context) => {
const page = context.getSelectedPage();
await page.goto(server.getRoute('/main'));
await context.createTextSnapshot();
await evaluateScript.handler(
{
params: {
function: String((element: Element) => {
return element.textContent;
}),
args: [{uid: '1_3'}],
},
},
response,
context,
);
const lineEvaluation = response.responseLines.at(2)!;
assert.strictEqual(JSON.parse(lineEvaluation), 'I am iframe button');
});
});
});
});