Skip to content

Commit c663f4d

Browse files
authored
feat: support script evaluate arguments (#40)
This change allows clients to pass snapshot elements as arguments.
1 parent 36115d7 commit c663f4d

File tree

4 files changed

+112
-21
lines changed

4 files changed

+112
-21
lines changed

.github/workflows/presubmit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
run: npm ci
5050

5151
- name: Generate documents
52-
run: npm run generate-docs
52+
run: npm run generate-docs && npm run format
5353

5454
- name: Check if autogenerated docs differ
5555
run: |

docs/tool-reference.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,11 +259,21 @@
259259

260260
### `evaluate_script`
261261

262-
**Description:** Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON.
262+
**Description:** Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON
263+
so returned values have to JSON-serializable.
263264

264265
**Parameters:**
265266

266-
- **function** (string) **(required)**: A JavaScript function to run in the currently selected page. Example: `() => {return document.title}` or `async () => {return await fetch("example.com")}`
267+
- **args** (array) _(optional)_: An optional list of arguments to pass to the function.
268+
- **function** (string) **(required)**: A JavaScript function to run in the currently selected page.
269+
Example without arguments: `() => {
270+
return document.title
271+
}` or `async () => {
272+
return await fetch("example.com")
273+
}`.
274+
Example with arguments: `(el) => {
275+
return el.innerText;
276+
}`
267277

268278
---
269279

src/tools/script.ts

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,65 @@ import z from 'zod';
77
import {defineTool} from './ToolDefinition.js';
88
import {ToolCategories} from './categories.js';
99
import {waitForEventsAfterAction} from '../waitForHelpers.js';
10+
import {JSHandle} from 'puppeteer-core';
1011

1112
export const evaluateScript = defineTool({
1213
name: 'evaluate_script',
13-
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON.`,
14+
description: `Evaluate a JavaScript function inside the currently selected page. Returns the response as JSON
15+
so returned values have to JSON-serializable.`,
1416
annotations: {
1517
category: ToolCategories.DEBUGGING,
1618
readOnlyHint: false,
1719
},
1820
schema: {
19-
function: z
20-
.string()
21-
.describe(
22-
'A JavaScript function to run in the currently selected page. Example: `() => {return document.title}` or `async () => {return await fetch("example.com")}`',
23-
),
21+
function: z.string().describe(
22+
`A JavaScript function to run in the currently selected page.
23+
Example without arguments: \`() => {
24+
return document.title
25+
}\` or \`async () => {
26+
return await fetch("example.com")
27+
}\`.
28+
Example with arguments: \`(el) => {
29+
return el.innerText;
30+
}\`
31+
`,
32+
),
33+
args: z
34+
.array(
35+
z.object({
36+
uid: z
37+
.string()
38+
.describe(
39+
'The uid of an element on the page from the page content snapshot',
40+
),
41+
}),
42+
)
43+
.optional()
44+
.describe(`An optional list of arguments to pass to the function.`),
2445
},
2546
handler: async (request, response, context) => {
2647
const page = context.getSelectedPage();
27-
28-
const script = `(async () => {
29-
return JSON.stringify(await (${request.params.function})());
30-
})()`;
31-
32-
await waitForEventsAfterAction(page, async () => {
33-
const result = await page.evaluate(script);
34-
response.appendResponseLine('Script ran on page and returned:');
35-
response.appendResponseLine('```json');
36-
response.appendResponseLine(`${result}`);
37-
response.appendResponseLine('```');
38-
});
48+
const fn = await page.evaluateHandle(`(${request.params.function})`);
49+
const args: JSHandle<unknown>[] = [fn];
50+
try {
51+
for (const el of request.params.args ?? []) {
52+
args.push(await context.getElementByUid(el.uid));
53+
}
54+
await waitForEventsAfterAction(page, async () => {
55+
const result = await page.evaluate(
56+
async (fn, ...args) => {
57+
// @ts-expect-error no types.
58+
return JSON.stringify(await fn(...args));
59+
},
60+
...args,
61+
);
62+
response.appendResponseLine('Script ran on page and returned:');
63+
response.appendResponseLine('```json');
64+
response.appendResponseLine(`${result}`);
65+
response.appendResponseLine('```');
66+
});
67+
} finally {
68+
Promise.allSettled(args.map(arg => arg.dispose())).catch(() => {});
69+
}
3970
},
4071
});

tests/tools/script.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,55 @@ describe('script', () => {
103103
assert.strictEqual(JSON.parse(lineEvaluation), 'Works');
104104
});
105105
});
106+
107+
it('work with one argument', async () => {
108+
await withBrowser(async (response, context) => {
109+
const page = context.getSelectedPage();
110+
111+
await page.setContent(html`<button id="test">test</button>`);
112+
113+
await context.createTextSnapshot();
114+
115+
await evaluateScript.handler(
116+
{
117+
params: {
118+
function: String(async (el: Element) => {
119+
return el.id;
120+
}),
121+
args: [{uid: '1_1'}],
122+
},
123+
},
124+
response,
125+
context,
126+
);
127+
const lineEvaluation = response.responseLines.at(2)!;
128+
assert.strictEqual(JSON.parse(lineEvaluation), 'test');
129+
});
130+
});
131+
132+
it('work with multiple args', async () => {
133+
await withBrowser(async (response, context) => {
134+
const page = context.getSelectedPage();
135+
136+
await page.setContent(html`<button id="test">test</button>`);
137+
138+
await context.createTextSnapshot();
139+
140+
await evaluateScript.handler(
141+
{
142+
params: {
143+
function: String((container: Element, child: Element) => {
144+
return container.contains(child);
145+
}),
146+
args: [{uid: '1_0'}, {uid: '1_1'}],
147+
},
148+
},
149+
response,
150+
context,
151+
);
152+
const lineEvaluation = response.responseLines.at(2)!;
153+
assert.strictEqual(JSON.parse(lineEvaluation), true);
154+
});
155+
});
106156
});
107157
});

0 commit comments

Comments
 (0)