Skip to content

Commit 053f1f8

Browse files
fix: allow evaluating in Frames (#443)
Closes #439
1 parent b6b42ec commit 053f1f8

File tree

2 files changed

+55
-9
lines changed

2 files changed

+55
-9
lines changed

src/tools/script.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Copyright 2025 Google LLC
44
* SPDX-License-Identifier: Apache-2.0
55
*/
6-
import type {JSHandle} from 'puppeteer-core';
6+
import type {Frame, JSHandle, Page} from 'puppeteer-core';
77

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

@@ -45,15 +45,29 @@ Example with arguments: \`(el) => {
4545
.describe(`An optional list of arguments to pass to the function.`),
4646
},
4747
handler: async (request, response, context) => {
48-
const page = context.getSelectedPage();
49-
const fn = await page.evaluateHandle(`(${request.params.function})`);
50-
const args: Array<JSHandle<unknown>> = [fn];
48+
const args: Array<JSHandle<unknown>> = [];
5149
try {
50+
const frames = new Set<Frame>();
5251
for (const el of request.params.args ?? []) {
53-
args.push(await context.getElementByUid(el.uid));
52+
const handle = await context.getElementByUid(el.uid);
53+
frames.add(handle.frame);
54+
args.push(handle);
5455
}
56+
let pageOrFrame: Page | Frame;
57+
// We can't evaluate the element handle across frames
58+
if (frames.size > 1) {
59+
throw new Error(
60+
"Elements from different frames can't be evaluated together.",
61+
);
62+
} else {
63+
pageOrFrame = [...frames.values()][0] ?? context.getSelectedPage();
64+
}
65+
const fn = await pageOrFrame.evaluateHandle(
66+
`(${request.params.function})`,
67+
);
68+
args.unshift(fn);
5569
await context.waitForEventsAfterAction(async () => {
56-
const result = await page.evaluate(
70+
const result = await pageOrFrame.evaluate(
5771
async (fn, ...args) => {
5872
// @ts-expect-error no types.
5973
return JSON.stringify(await fn(...args));
@@ -66,9 +80,7 @@ Example with arguments: \`(el) => {
6680
response.appendResponseLine('```');
6781
});
6882
} finally {
69-
Promise.allSettled(args.map(arg => arg.dispose())).catch(() => {
70-
// Ignore errors
71-
});
83+
void Promise.allSettled(args.map(arg => arg.dispose()));
7284
}
7385
},
7486
});

tests/tools/script.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import assert from 'node:assert';
77
import {describe, it} from 'node:test';
88

99
import {evaluateScript} from '../../src/tools/script.js';
10+
import {serverHooks} from '../server.js';
1011
import {html, withBrowser} from '../utils.js';
1112

1213
describe('script', () => {
14+
const server = serverHooks();
15+
1316
describe('browser_evaluate_script', () => {
1417
it('evaluates', async () => {
1518
await withBrowser(async (response, context) => {
@@ -152,5 +155,36 @@ describe('script', () => {
152155
assert.strictEqual(JSON.parse(lineEvaluation), true);
153156
});
154157
});
158+
159+
it('work for elements inside iframes', async () => {
160+
server.addHtmlRoute(
161+
'/iframe',
162+
html`<main><button>I am iframe button</button></main>`,
163+
);
164+
server.addRoute('/main', async (_req, res) => {
165+
res.write(html`<iframe src="/iframe"></iframe>`);
166+
res.end();
167+
});
168+
169+
await withBrowser(async (response, context) => {
170+
const page = context.getSelectedPage();
171+
await page.goto(server.getRoute('/main'));
172+
await context.createTextSnapshot();
173+
await evaluateScript.handler(
174+
{
175+
params: {
176+
function: String((element: Element) => {
177+
return element.textContent;
178+
}),
179+
args: [{uid: '1_3'}],
180+
},
181+
},
182+
response,
183+
context,
184+
);
185+
const lineEvaluation = response.responseLines.at(2)!;
186+
assert.strictEqual(JSON.parse(lineEvaluation), 'I am iframe button');
187+
});
188+
});
155189
});
156190
});

0 commit comments

Comments
 (0)