Skip to content

Commit 407c2bd

Browse files
zyzyzyryxyPiotr Paulski
andauthored
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>
1 parent faac61d commit 407c2bd

4 files changed

Lines changed: 263 additions & 11 deletions

File tree

docs/tool-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,14 @@
9090
**Parameters:**
9191

9292
- **uid** (string) **(required)**: The uid of an element on the page from the page content snapshot
93-
- **value** (string) **(required)**: The value to [`fill`](#fill) in
93+
- **value** (string) **(required)**: The value to [`fill`](#fill) in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.
9494
- **includeSnapshot** (boolean) _(optional)_: Whether to include a snapshot in the response. Default is false.
9595

9696
---
9797

9898
### `fill_form`
9999

100-
**Description:** [`Fill`](#fill) out multiple form elements at once
100+
**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.
101101

102102
**Parameters:**
103103

src/bin/chrome-devtools-cli-options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ export const commands: Commands = {
250250
value: {
251251
name: 'value',
252252
type: 'string',
253-
description: 'The value to fill in',
253+
description:
254+
'The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
254255
required: true,
255256
},
256257
includeSnapshot: {

src/tools/input.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,29 @@ async function fillFormElement(
260260
if (aXNode && aXNode.role === 'combobox' && hasOptionChildren(aXNode)) {
261261
await selectOption(handle, aXNode, value);
262262
} else {
263-
// Increase timeout for longer input values.
264-
const timeoutPerChar = 10; // ms
265-
const fillTimeout =
266-
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
267-
await handle.asLocator().setTimeout(fillTimeout).fill(value);
263+
const isToggle = await handle.evaluate(el => {
264+
if (el instanceof HTMLInputElement) {
265+
return el.type === 'checkbox' || el.type === 'radio';
266+
}
267+
const role = el.getAttribute('role');
268+
return role === 'checkbox' || role === 'radio' || role === 'switch';
269+
});
270+
271+
if (isToggle) {
272+
if (['true', 'false'].includes(value)) {
273+
await handle.asLocator().fill(value === 'true');
274+
} else {
275+
throw new Error(
276+
`Checkboxes, radio boxes and toggles require "true" or "false" value, but ${value} was used`,
277+
);
278+
}
279+
} else {
280+
// Increase timeout for longer input values.
281+
const timeoutPerChar = 10; // ms
282+
const fillTimeout =
283+
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
284+
await handle.asLocator().setTimeout(fillTimeout).fill(value);
285+
}
268286
}
269287
} catch (error) {
270288
handleActionError(error, uid);
@@ -286,7 +304,11 @@ export const fill = definePageTool({
286304
.describe(
287305
'The uid of an element on the page from the page content snapshot',
288306
),
289-
value: zod.string().describe('The value to fill in'),
307+
value: zod
308+
.string()
309+
.describe(
310+
'The value to fill in. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
311+
),
290312
includeSnapshot: includeSnapshotSchema,
291313
},
292314
blockedByDialog: true,
@@ -372,7 +394,7 @@ export const drag = definePageTool({
372394

373395
export const fillForm = definePageTool({
374396
name: 'fill_form',
375-
description: `Fill out multiple form elements at once`,
397+
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.`,
376398
annotations: {
377399
category: ToolCategory.INPUT,
378400
readOnlyHint: false,
@@ -383,7 +405,11 @@ export const fillForm = definePageTool({
383405
// eslint-disable-next-line @local/enforce-zod-schema
384406
zod.object({
385407
uid: zod.string().describe('The uid of the element to fill out'),
386-
value: zod.string().describe('Value for the element'),
408+
value: zod
409+
.string()
410+
.describe(
411+
'Value for the element. "true" or "false" for checkboxes and toggles, "true" for radio buttons.',
412+
),
387413
}),
388414
)
389415
.describe('Elements from snapshot to fill out.'),

tests/tools/input.test.ts

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,180 @@ describe('input', () => {
761761
);
762762
});
763763
});
764+
765+
it('toggles checkboxes', async () => {
766+
await withMcpContext(async (response, context) => {
767+
const page = context.getSelectedPptrPage();
768+
await page.setContent(
769+
html`<input
770+
type="checkbox"
771+
id="cb"
772+
/>`,
773+
);
774+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
775+
context.getSelectedMcpPage(),
776+
);
777+
778+
// Check it
779+
await fill.handler(
780+
{
781+
params: {
782+
uid: '1_1',
783+
value: 'true',
784+
},
785+
page: context.getSelectedMcpPage(),
786+
},
787+
response,
788+
context,
789+
);
790+
791+
assert.strictEqual(
792+
response.responseLines[0],
793+
'Successfully filled out the element',
794+
);
795+
assert.ok(response.includeSnapshot);
796+
let isChecked = await page.$eval(
797+
'#cb',
798+
el => (el as HTMLInputElement).checked,
799+
);
800+
assert.strictEqual(isChecked, true);
801+
802+
// Uncheck it
803+
await fill.handler(
804+
{
805+
params: {
806+
uid: '1_1',
807+
value: 'false',
808+
},
809+
page: context.getSelectedMcpPage(),
810+
},
811+
new McpResponse({} as ParsedArguments),
812+
context,
813+
);
814+
815+
isChecked = await page.$eval(
816+
'#cb',
817+
el => (el as HTMLInputElement).checked,
818+
);
819+
assert.strictEqual(isChecked, false);
820+
});
821+
});
822+
823+
it('toggles switches', async () => {
824+
await withMcpContext(async (response, context) => {
825+
const page = context.getSelectedPptrPage();
826+
await page.setContent(html`
827+
<div
828+
role="switch"
829+
aria-checked="false"
830+
id="sw"
831+
style="width: 20px; height: 20px; background: blue;"
832+
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
833+
>
834+
switch
835+
</div>
836+
`);
837+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
838+
context.getSelectedMcpPage(),
839+
);
840+
841+
// Turn it on
842+
await fill.handler(
843+
{
844+
params: {
845+
uid: '1_1',
846+
value: 'true',
847+
},
848+
page: context.getSelectedMcpPage(),
849+
},
850+
response,
851+
context,
852+
);
853+
854+
let swChecked = await page.$eval(
855+
'#sw',
856+
el => el.getAttribute('aria-checked') === 'true',
857+
);
858+
assert.strictEqual(swChecked, true);
859+
860+
// Turn it off
861+
await fill.handler(
862+
{
863+
params: {
864+
uid: '1_1',
865+
value: 'false',
866+
},
867+
page: context.getSelectedMcpPage(),
868+
},
869+
new McpResponse({} as ParsedArguments),
870+
context,
871+
);
872+
873+
swChecked = await page.$eval(
874+
'#sw',
875+
el => el.getAttribute('aria-checked') === 'true',
876+
);
877+
assert.strictEqual(swChecked, false);
878+
});
879+
});
880+
881+
it('selects radio buttons', async () => {
882+
await withMcpContext(async (response, context) => {
883+
const page = context.getSelectedPptrPage();
884+
await page.setContent(html`
885+
<input
886+
type="radio"
887+
name="group1"
888+
id="r1"
889+
checked
890+
/>
891+
<input
892+
type="radio"
893+
name="group1"
894+
id="r2"
895+
/>
896+
`);
897+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
898+
context.getSelectedMcpPage(),
899+
);
900+
901+
// Initial state
902+
let r1Checked = await page.$eval(
903+
'#r1',
904+
el => (el as HTMLInputElement).checked,
905+
);
906+
let r2Checked = await page.$eval(
907+
'#r2',
908+
el => (el as HTMLInputElement).checked,
909+
);
910+
assert.strictEqual(r1Checked, true);
911+
assert.strictEqual(r2Checked, false);
912+
913+
// Fill second radio with true
914+
await fill.handler(
915+
{
916+
params: {
917+
uid: '1_2',
918+
value: 'true',
919+
},
920+
page: context.getSelectedMcpPage(),
921+
},
922+
response,
923+
context,
924+
);
925+
926+
r1Checked = await page.$eval(
927+
'#r1',
928+
el => (el as HTMLInputElement).checked,
929+
);
930+
r2Checked = await page.$eval(
931+
'#r2',
932+
el => (el as HTMLInputElement).checked,
933+
);
934+
assert.strictEqual(r1Checked, false);
935+
assert.strictEqual(r2Checked, true);
936+
});
937+
});
764938
});
765939

766940
describe('drags', () => {
@@ -882,6 +1056,57 @@ describe('input', () => {
8821056
);
8831057
});
8841058
});
1059+
1060+
it('fill_form handles checkboxes', async () => {
1061+
await withMcpContext(async (response, context) => {
1062+
const page = context.getSelectedPptrPage();
1063+
await page.setContent(
1064+
html`<input
1065+
name="username"
1066+
type="text"
1067+
/><input
1068+
name="cb"
1069+
type="checkbox"
1070+
/>`,
1071+
);
1072+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
1073+
context.getSelectedMcpPage(),
1074+
);
1075+
await fillForm.handler(
1076+
{
1077+
params: {
1078+
elements: [
1079+
{
1080+
uid: '1_1',
1081+
value: 'test',
1082+
},
1083+
{
1084+
uid: '1_2',
1085+
value: 'true',
1086+
},
1087+
],
1088+
},
1089+
page: context.getSelectedMcpPage(),
1090+
},
1091+
response,
1092+
context,
1093+
);
1094+
assert.strictEqual(
1095+
await page.evaluate(() => {
1096+
// @ts-expect-error missing types
1097+
return document.querySelector('input[name=username]').value;
1098+
}),
1099+
'test',
1100+
);
1101+
assert.strictEqual(
1102+
await page.evaluate(() => {
1103+
// @ts-expect-error missing types
1104+
return document.querySelector('input[name=cb]').checked;
1105+
}),
1106+
true,
1107+
);
1108+
});
1109+
});
8851110
});
8861111

8871112
describe('uploadFile', () => {

0 commit comments

Comments
 (0)