fix: Make fill_form more appealing when filling forms with checkboxes (#1971)

Fixes #1942

Verified using `npm run eval --
scripts/eval_scenarios/fill_select_and_checkboxes_test.ts`

Without this change, I observed 7 runs using fill_form for all controls
at once, 14 runs using click to select checkboxes and 10 runs that did
nothing (total 31 runs)

After this change: 9 fill_form using runs (passes), 1 click based
approach and 10 no-attempt fails (20 runs total)

Depending how we count the no-attempt runs, its either increase from 23%
to 45% or 33% to 90% in eval pass rate.

Co-authored-by: Piotr Paulski <piotrpaulski@chromium.org>
This commit is contained in:
Piotr Paulski
2026-05-07 17:00:52 +02:00
committed by GitHub
parent faac61d2f2
commit 407c2bd03f
4 changed files with 263 additions and 11 deletions
+2 -2
View File
@@ -90,14 +90,14 @@
**Parameters:**
- **uid** (string) **(required)**: The uid of an element on the page from the page content snapshot
- **value** (string) **(required)**: The value to [`fill`](#fill) in
- **value** (string) **(required)**: The value to [`fill`](#fill) in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.
- **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false.
---
### `fill_form`
**Description:** [`Fill`](#fill) out multiple form elements at once
**Description:** [`Fill`](#fill) out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS prefer this tool over multiple individual '[`fill`](#fill)' or '[`click`](#click)' calls when interacting with forms. It is significantly faster, more reliable, and reduces turn count. Example: [`Fill`](#fill) username, password, and check "Remember Me" in one call.
**Parameters:**
+2 -1
View File
@@ -250,7 +250,8 @@ export const commands: Commands = {
value: {
name: 'value',
type: 'string',
description: 'The value to fill in',
description:
'The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
required: true,
},
includeSnapshot: {
+34 -8
View File
@@ -260,11 +260,29 @@ async function fillFormElement(
if (aXNode && aXNode.role === 'combobox' && hasOptionChildren(aXNode)) {
await selectOption(handle, aXNode, value);
} else {
// Increase timeout for longer input values.
const timeoutPerChar = 10; // ms
const fillTimeout =
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
await handle.asLocator().setTimeout(fillTimeout).fill(value);
const isToggle = await handle.evaluate(el => {
if (el instanceof HTMLInputElement) {
return el.type === 'checkbox' || el.type === 'radio';
}
const role = el.getAttribute('role');
return role === 'checkbox' || role === 'radio' || role === 'switch';
});
if (isToggle) {
if (['true', 'false'].includes(value)) {
await handle.asLocator().fill(value === 'true');
} else {
throw new Error(
`Checkboxes, radio boxes and toggles require "true" or "false" value, but ${value} was used`,
);
}
} else {
// Increase timeout for longer input values.
const timeoutPerChar = 10; // ms
const fillTimeout =
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
await handle.asLocator().setTimeout(fillTimeout).fill(value);
}
}
} catch (error) {
handleActionError(error, uid);
@@ -286,7 +304,11 @@ export const fill = definePageTool({
.describe(
'The uid of an element on the page from the page content snapshot',
),
value: zod.string().describe('The value to fill in'),
value: zod
.string()
.describe(
'The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
),
includeSnapshot: includeSnapshotSchema,
},
blockedByDialog: true,
@@ -372,7 +394,7 @@ export const drag = definePageTool({
export const fillForm = definePageTool({
name: 'fill_form',
description: `Fill out multiple form elements at once`,
description: `Fill out multiple form elements (inputs, selects, checkboxes, radios) at once. ALWAYS prefer this tool over multiple individual 'fill' or 'click' calls when interacting with forms. It is significantly faster, more reliable, and reduces turn count. Example: Fill username, password, and check "Remember Me" in one call.`,
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
@@ -383,7 +405,11 @@ export const fillForm = definePageTool({
// eslint-disable-next-line @local/enforce-zod-schema
zod.object({
uid: zod.string().describe('The uid of the element to fill out'),
value: zod.string().describe('Value for the element'),
value: zod
.string()
.describe(
'Value for the element. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
),
}),
)
.describe('Elements from snapshot to fill out.'),
+225
View File
@@ -761,6 +761,180 @@ describe('input', () => {
);
});
});
it('toggles checkboxes', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(
html`<input
type="checkbox"
id="cb"
/>`,
);
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
// Check it
await fill.handler(
{
params: {
uid: '1_1',
value: 'true',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
'Successfully filled out the element',
);
assert.ok(response.includeSnapshot);
let isChecked = await page.$eval(
'#cb',
el => (el as HTMLInputElement).checked,
);
assert.strictEqual(isChecked, true);
// Uncheck it
await fill.handler(
{
params: {
uid: '1_1',
value: 'false',
},
page: context.getSelectedMcpPage(),
},
new McpResponse({} as ParsedArguments),
context,
);
isChecked = await page.$eval(
'#cb',
el => (el as HTMLInputElement).checked,
);
assert.strictEqual(isChecked, false);
});
});
it('toggles switches', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(html`
<div
role="switch"
aria-checked="false"
id="sw"
style="width: 20px; height: 20px; background: blue;"
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
>
switch
</div>
`);
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
// Turn it on
await fill.handler(
{
params: {
uid: '1_1',
value: 'true',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
let swChecked = await page.$eval(
'#sw',
el => el.getAttribute('aria-checked') === 'true',
);
assert.strictEqual(swChecked, true);
// Turn it off
await fill.handler(
{
params: {
uid: '1_1',
value: 'false',
},
page: context.getSelectedMcpPage(),
},
new McpResponse({} as ParsedArguments),
context,
);
swChecked = await page.$eval(
'#sw',
el => el.getAttribute('aria-checked') === 'true',
);
assert.strictEqual(swChecked, false);
});
});
it('selects radio buttons', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(html`
<input
type="radio"
name="group1"
id="r1"
checked
/>
<input
type="radio"
name="group1"
id="r2"
/>
`);
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
// Initial state
let r1Checked = await page.$eval(
'#r1',
el => (el as HTMLInputElement).checked,
);
let r2Checked = await page.$eval(
'#r2',
el => (el as HTMLInputElement).checked,
);
assert.strictEqual(r1Checked, true);
assert.strictEqual(r2Checked, false);
// Fill second radio with true
await fill.handler(
{
params: {
uid: '1_2',
value: 'true',
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
r1Checked = await page.$eval(
'#r1',
el => (el as HTMLInputElement).checked,
);
r2Checked = await page.$eval(
'#r2',
el => (el as HTMLInputElement).checked,
);
assert.strictEqual(r1Checked, false);
assert.strictEqual(r2Checked, true);
});
});
});
describe('drags', () => {
@@ -882,6 +1056,57 @@ describe('input', () => {
);
});
});
it('fill_form handles checkboxes', async () => {
await withMcpContext(async (response, context) => {
const page = context.getSelectedPptrPage();
await page.setContent(
html`<input
name="username"
type="text"
/><input
name="cb"
type="checkbox"
/>`,
);
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
context.getSelectedMcpPage(),
);
await fillForm.handler(
{
params: {
elements: [
{
uid: '1_1',
value: 'test',
},
{
uid: '1_2',
value: 'true',
},
],
},
page: context.getSelectedMcpPage(),
},
response,
context,
);
assert.strictEqual(
await page.evaluate(() => {
// @ts-expect-error missing types
return document.querySelector('input[name=username]').value;
}),
'test',
);
assert.strictEqual(
await page.evaluate(() => {
// @ts-expect-error missing types
return document.querySelector('input[name=cb]').checked;
}),
true,
);
});
});
});
describe('uploadFile', () => {