diff --git a/src/tools/script.ts b/src/tools/script.ts index beaef94b..128363bd 100644 --- a/src/tools/script.ts +++ b/src/tools/script.ts @@ -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'; @@ -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> = [fn]; + const args: Array> = []; try { + const frames = new Set(); 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)); @@ -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())); } }, }); diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index bad9a902..a31cff1c 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -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) => { @@ -152,5 +155,36 @@ describe('script', () => { assert.strictEqual(JSON.parse(lineEvaluation), true); }); }); + + it('work for elements inside iframes', async () => { + server.addHtmlRoute( + '/iframe', + html`
`, + ); + server.addRoute('/main', async (_req, res) => { + res.write(html``); + 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'); + }); + }); }); });