Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run gen' to update-->

# 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)
Expand Down Expand Up @@ -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:**

Expand Down
34 changes: 28 additions & 6 deletions src/tools/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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'`,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add a test to tests/tools/input.test.ts that tests checkboxes and verify that it actually accepts a string true and false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I misread some eval results thinking it already works this way. Pushed an update to make fill and fill_form handle those, together with tests.

annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
Expand Down
174 changes: 174 additions & 0 deletions tests/tools/input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,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', () => {
Expand Down
Loading