Skip to content

Commit 053365a

Browse files
author
Piotr Paulski
committed
Modify fill and fill_form tools to handle checkboxes and radio buttons.
1 parent a945995 commit 053365a

2 files changed

Lines changed: 201 additions & 5 deletions

File tree

src/tools/input.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,33 @@ async function fillFormElement(
204204
if (aXNode && aXNode.role === 'combobox' && hasOptionChildren(aXNode)) {
205205
await selectOption(handle, aXNode, value);
206206
} else {
207-
// Increase timeout for longer input values.
208-
const timeoutPerChar = 10; // ms
209-
const fillTimeout =
210-
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
211-
await handle.asLocator().setTimeout(fillTimeout).fill(value);
207+
const isToggle = await handle.evaluate(el => {
208+
if (el instanceof HTMLInputElement) {
209+
return el.type === 'checkbox' || el.type === 'radio';
210+
}
211+
const role = el.getAttribute('role');
212+
return role === 'checkbox' || role === 'radio' || role === 'switch';
213+
});
214+
215+
if (isToggle) {
216+
const shouldBeChecked = value.toLowerCase() === 'true';
217+
const isChecked = await handle.evaluate(el => {
218+
if (el instanceof HTMLInputElement) {
219+
return el.checked;
220+
}
221+
return el.getAttribute('aria-checked') === 'true';
222+
});
223+
224+
if (isChecked !== shouldBeChecked) {
225+
await handle.asLocator().click();
226+
}
227+
} else {
228+
// Increase timeout for longer input values.
229+
const timeoutPerChar = 10; // ms
230+
const fillTimeout =
231+
page.pptrPage.getDefaultTimeout() + value.length * timeoutPerChar;
232+
await handle.asLocator().setTimeout(fillTimeout).fill(value);
233+
}
212234
}
213235
} catch (error) {
214236
handleActionError(error, uid);

tests/tools/input.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,180 @@ describe('input', () => {
622622
);
623623
});
624624
});
625+
626+
it('toggles checkboxes', async () => {
627+
await withMcpContext(async (response, context) => {
628+
const page = context.getSelectedPptrPage();
629+
await page.setContent(
630+
html`<input
631+
type="checkbox"
632+
id="cb"
633+
/>`,
634+
);
635+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
636+
context.getSelectedMcpPage(),
637+
);
638+
639+
// Check it
640+
await fill.handler(
641+
{
642+
params: {
643+
uid: '1_1',
644+
value: 'true',
645+
},
646+
page: context.getSelectedMcpPage(),
647+
},
648+
response,
649+
context,
650+
);
651+
652+
assert.strictEqual(
653+
response.responseLines[0],
654+
'Successfully filled out the element',
655+
);
656+
assert.ok(response.includeSnapshot);
657+
let isChecked = await page.$eval(
658+
'#cb',
659+
el => (el as HTMLInputElement).checked,
660+
);
661+
assert.strictEqual(isChecked, true);
662+
663+
// Uncheck it
664+
await fill.handler(
665+
{
666+
params: {
667+
uid: '1_1',
668+
value: 'false',
669+
},
670+
page: context.getSelectedMcpPage(),
671+
},
672+
new McpResponse({} as ParsedArguments),
673+
context,
674+
);
675+
676+
isChecked = await page.$eval(
677+
'#cb',
678+
el => (el as HTMLInputElement).checked,
679+
);
680+
assert.strictEqual(isChecked, false);
681+
});
682+
});
683+
684+
it('toggles switches', async () => {
685+
await withMcpContext(async (response, context) => {
686+
const page = context.getSelectedPptrPage();
687+
await page.setContent(html`
688+
<div
689+
role="switch"
690+
aria-checked="false"
691+
id="sw"
692+
style="width: 20px; height: 20px; background: blue;"
693+
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
694+
>
695+
switch
696+
</div>
697+
`);
698+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
699+
context.getSelectedMcpPage(),
700+
);
701+
702+
// Turn it on
703+
await fill.handler(
704+
{
705+
params: {
706+
uid: '1_1',
707+
value: 'true',
708+
},
709+
page: context.getSelectedMcpPage(),
710+
},
711+
response,
712+
context,
713+
);
714+
715+
let swChecked = await page.$eval(
716+
'#sw',
717+
el => el.getAttribute('aria-checked') === 'true',
718+
);
719+
assert.strictEqual(swChecked, true);
720+
721+
// Turn it off
722+
await fill.handler(
723+
{
724+
params: {
725+
uid: '1_1',
726+
value: 'false',
727+
},
728+
page: context.getSelectedMcpPage(),
729+
},
730+
new McpResponse({} as ParsedArguments),
731+
context,
732+
);
733+
734+
swChecked = await page.$eval(
735+
'#sw',
736+
el => el.getAttribute('aria-checked') === 'true',
737+
);
738+
assert.strictEqual(swChecked, false);
739+
});
740+
});
741+
742+
it('selects radio buttons', async () => {
743+
await withMcpContext(async (response, context) => {
744+
const page = context.getSelectedPptrPage();
745+
await page.setContent(html`
746+
<input
747+
type="radio"
748+
name="group1"
749+
id="r1"
750+
checked
751+
/>
752+
<input
753+
type="radio"
754+
name="group1"
755+
id="r2"
756+
/>
757+
`);
758+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
759+
context.getSelectedMcpPage(),
760+
);
761+
762+
// Initial state
763+
let r1Checked = await page.$eval(
764+
'#r1',
765+
el => (el as HTMLInputElement).checked,
766+
);
767+
let r2Checked = await page.$eval(
768+
'#r2',
769+
el => (el as HTMLInputElement).checked,
770+
);
771+
assert.strictEqual(r1Checked, true);
772+
assert.strictEqual(r2Checked, false);
773+
774+
// Fill second radio with true
775+
await fill.handler(
776+
{
777+
params: {
778+
uid: '1_2',
779+
value: 'true',
780+
},
781+
page: context.getSelectedMcpPage(),
782+
},
783+
response,
784+
context,
785+
);
786+
787+
r1Checked = await page.$eval(
788+
'#r1',
789+
el => (el as HTMLInputElement).checked,
790+
);
791+
r2Checked = await page.$eval(
792+
'#r2',
793+
el => (el as HTMLInputElement).checked,
794+
);
795+
assert.strictEqual(r1Checked, false);
796+
assert.strictEqual(r2Checked, true);
797+
});
798+
});
625799
});
626800

627801
describe('drags', () => {

0 commit comments

Comments
 (0)