Skip to content

Commit 3d1a60b

Browse files
authored
chore: introduce form filling tool (#935)
1 parent 86eba22 commit 3d1a60b

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,15 @@ http.createServer(async (req, res) => {
494494

495495
<!-- NOTE: This has been generated via update-readme.js -->
496496

497+
- **browser_fill_form**
498+
- Title: Fill form
499+
- Description: Fill multiple form fields
500+
- Parameters:
501+
- `fields` (array): Fields to fill in
502+
- Read-only: **false**
503+
504+
<!-- NOTE: This has been generated via update-readme.js -->
505+
497506
- **browser_handle_dialog**
498507
- Title: Handle a dialog
499508
- Description: Handle a dialog

src/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import console from './tools/console.js';
1919
import dialogs from './tools/dialogs.js';
2020
import evaluate from './tools/evaluate.js';
2121
import files from './tools/files.js';
22+
import form from './tools/form.js';
2223
import install from './tools/install.js';
2324
import keyboard from './tools/keyboard.js';
2425
import navigate from './tools/navigate.js';
@@ -39,6 +40,7 @@ export const allTools: Tool<any>[] = [
3940
...dialogs,
4041
...evaluate,
4142
...files,
43+
...form,
4244
...install,
4345
...keyboard,
4446
...navigate,

src/tools/form.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { z } from 'zod';
18+
19+
import { defineTabTool } from './tool.js';
20+
import { generateLocator } from './utils.js';
21+
import * as javascript from '../utils/codegen.js';
22+
23+
const fillForm = defineTabTool({
24+
capability: 'core',
25+
26+
schema: {
27+
name: 'browser_fill_form',
28+
title: 'Fill form',
29+
description: 'Fill multiple form fields',
30+
inputSchema: z.object({
31+
fields: z.array(z.object({
32+
name: z.string().describe('Human-readable field name'),
33+
type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),
34+
ref: z.string().describe('Exact target field reference from the page snapshot'),
35+
value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'),
36+
})).describe('Fields to fill in'),
37+
}),
38+
type: 'destructive',
39+
},
40+
41+
handle: async (tab, params, response) => {
42+
for (const field of params.fields) {
43+
const locator = await tab.refLocator({ element: field.name, ref: field.ref });
44+
const locatorSource = `await page.${await generateLocator(locator)}`;
45+
if (field.type === 'textbox' || field.type === 'slider') {
46+
await locator.fill(field.value);
47+
response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`);
48+
} else if (field.type === 'checkbox' || field.type === 'radio') {
49+
await locator.setChecked(field.value === 'true');
50+
response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`);
51+
} else if (field.type === 'combobox') {
52+
await locator.selectOption({ label: field.value });
53+
response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`);
54+
}
55+
}
56+
},
57+
});
58+
59+
export default [
60+
fillForm,
61+
];

tests/capabilities.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ test('test snapshot tool list', async ({ client }) => {
2424
'browser_drag',
2525
'browser_evaluate',
2626
'browser_file_upload',
27+
'browser_fill_form',
2728
'browser_handle_dialog',
2829
'browser_hover',
2930
'browser_select_option',
@@ -54,6 +55,7 @@ test('test tool list proxy mode', async ({ startClient }) => {
5455
'browser_drag',
5556
'browser_evaluate',
5657
'browser_file_upload',
58+
'browser_fill_form',
5759
'browser_handle_dialog',
5860
'browser_hover',
5961
'browser_select_option',

tests/form.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test, expect } from './fixtures.js';
18+
19+
test('browser_fill_form (textbox)', async ({ client, server }) => {
20+
server.setContent('/', `
21+
<!DOCTYPE html>
22+
<html>
23+
<body>
24+
<form>
25+
<label>
26+
<input type="text" id="name" name="name" />
27+
Name
28+
</label>
29+
<label>
30+
<input type="email" id="email" name="email" />
31+
Email
32+
</label>
33+
<label>
34+
<input type="range" id="age" name="age" min="18" max="100" />
35+
Age
36+
</label>
37+
<label>
38+
<select id="country" name="country">
39+
<option value="">Choose a country</option>
40+
<option value="us">United States</option>
41+
<option value="uk">United Kingdom</option>
42+
</select>
43+
Country
44+
</label>
45+
<label>
46+
<input type="checkbox" name="subscribe" value="newsletter" />
47+
Subscribe to newsletter
48+
</label>
49+
</form>
50+
</body>
51+
</html>
52+
`, 'text/html');
53+
54+
await client.callTool({
55+
name: 'browser_navigate',
56+
arguments: { url: server.PREFIX },
57+
});
58+
59+
expect(await client.callTool({
60+
name: 'browser_fill_form',
61+
arguments: {
62+
fields: [
63+
{
64+
name: 'Name textbox',
65+
type: 'textbox',
66+
ref: 'e4',
67+
value: 'John Doe'
68+
},
69+
{
70+
name: 'Email textbox',
71+
type: 'textbox',
72+
ref: 'e6',
73+
74+
},
75+
{
76+
name: 'Age textbox',
77+
type: 'slider',
78+
ref: 'e8',
79+
value: '25'
80+
},
81+
{
82+
name: 'Country select',
83+
type: 'combobox',
84+
ref: 'e10',
85+
value: 'United States'
86+
},
87+
{
88+
name: 'Subscribe checkbox',
89+
type: 'checkbox',
90+
ref: 'e12',
91+
value: 'true'
92+
},
93+
]
94+
},
95+
})).toHaveResponse({
96+
code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe');
97+
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
98+
await page.getByRole('slider', { name: 'Age' }).fill('25');
99+
await page.getByLabel('Choose a country United').selectOption('United States');
100+
await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`,
101+
});
102+
103+
const response = await client.callTool({
104+
name: 'browser_snapshot',
105+
arguments: {
106+
},
107+
});
108+
expect.soft(response).toHaveResponse({
109+
pageState: expect.stringMatching(/textbox "Name".*John Doe/),
110+
});
111+
expect.soft(response).toHaveResponse({
112+
pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/),
113+
});
114+
expect.soft(response).toHaveResponse({
115+
pageState: expect.stringMatching(/slider "Age".*"25"/),
116+
});
117+
expect.soft(response).toHaveResponse({
118+
pageState: expect.stringContaining('option \"United States\" [selected]'),
119+
});
120+
expect.soft(response).toHaveResponse({
121+
pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'),
122+
});
123+
});

0 commit comments

Comments
 (0)