diff --git a/docs/tool-reference.md b/docs/tool-reference.md index b7f20ab37..2b1e8564a 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -1,6 +1,6 @@ -# Chrome DevTools MCP Tool Reference (~7005 cl100k_base tokens) +# Chrome DevTools MCP Tool Reference (~7014 cl100k_base tokens) - **[Input automation](#input-automation)** (9 tools) - [`click`](#click) @@ -85,7 +85,7 @@ ### `fill_form` -**Description:** [`Fill`](#fill) out multiple form elements at once +**Description:** [`Fill`](#fill) out multiple form elements at once. To set checkboxes [`fill`](#fill) them with 'true' **Parameters:** diff --git a/src/tools/input.ts b/src/tools/input.ts index 588f5308b..ffc1af362 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -204,11 +204,33 @@ 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) { + const shouldBeChecked = value.toLowerCase() === 'true'; + const isChecked = await handle.evaluate(el => { + if (el instanceof HTMLInputElement) { + return el.checked; + } + return el.getAttribute('aria-checked') === 'true'; + }); + + if (isChecked !== shouldBeChecked) { + await handle.asLocator().click(); + } + } 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); @@ -313,7 +335,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 at once. To set checkboxes fill them with 'true'`, annotations: { category: ToolCategory.INPUT, readOnlyHint: false, diff --git a/tests/tools/input.test.ts b/tests/tools/input.test.ts index b0033ec4d..e6bf8dc10 100644 --- a/tests/tools/input.test.ts +++ b/tests/tools/input.test.ts @@ -622,6 +622,180 @@ describe('input', () => { ); }); }); + + it('toggles checkboxes', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedPptrPage(); + await page.setContent( + html``, + ); + 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` +