diff --git a/README.md b/README.md index a44276b..5f97f26 100644 --- a/README.md +++ b/README.md @@ -494,6 +494,15 @@ http.createServer(async (req, res) => { +- **browser_fill_form** + - Title: Fill form + - Description: Fill multiple form fields + - Parameters: + - `fields` (array): Fields to fill in + - Read-only: **false** + + + - **browser_handle_dialog** - Title: Handle a dialog - Description: Handle a dialog diff --git a/src/tools.ts b/src/tools.ts index a1b1531..4f5d348 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -19,6 +19,7 @@ import console from './tools/console.js'; import dialogs from './tools/dialogs.js'; import evaluate from './tools/evaluate.js'; import files from './tools/files.js'; +import form from './tools/form.js'; import install from './tools/install.js'; import keyboard from './tools/keyboard.js'; import navigate from './tools/navigate.js'; @@ -39,6 +40,7 @@ export const allTools: Tool[] = [ ...dialogs, ...evaluate, ...files, + ...form, ...install, ...keyboard, ...navigate, diff --git a/src/tools/form.ts b/src/tools/form.ts new file mode 100644 index 0000000..28ffc0b --- /dev/null +++ b/src/tools/form.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; + +import { defineTabTool } from './tool.js'; +import { generateLocator } from './utils.js'; +import * as javascript from '../utils/codegen.js'; + +const fillForm = defineTabTool({ + capability: 'core', + + schema: { + name: 'browser_fill_form', + title: 'Fill form', + description: 'Fill multiple form fields', + inputSchema: z.object({ + fields: z.array(z.object({ + name: z.string().describe('Human-readable field name'), + type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'), + ref: z.string().describe('Exact target field reference from the page snapshot'), + value: z.string().describe('Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.'), + })).describe('Fields to fill in'), + }), + type: 'destructive', + }, + + handle: async (tab, params, response) => { + for (const field of params.fields) { + const locator = await tab.refLocator({ element: field.name, ref: field.ref }); + const locatorSource = `await page.${await generateLocator(locator)}`; + if (field.type === 'textbox' || field.type === 'slider') { + await locator.fill(field.value); + response.addCode(`${locatorSource}.fill(${javascript.quote(field.value)});`); + } else if (field.type === 'checkbox' || field.type === 'radio') { + await locator.setChecked(field.value === 'true'); + response.addCode(`${locatorSource}.setChecked(${javascript.quote(field.value)});`); + } else if (field.type === 'combobox') { + await locator.selectOption({ label: field.value }); + response.addCode(`${locatorSource}.selectOption(${javascript.quote(field.value)});`); + } + } + }, +}); + +export default [ + fillForm, +]; diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts index 3b15a6e..fe928de 100644 --- a/tests/capabilities.spec.ts +++ b/tests/capabilities.spec.ts @@ -24,6 +24,7 @@ test('test snapshot tool list', async ({ client }) => { 'browser_drag', 'browser_evaluate', 'browser_file_upload', + 'browser_fill_form', 'browser_handle_dialog', 'browser_hover', 'browser_select_option', @@ -54,6 +55,7 @@ test('test tool list proxy mode', async ({ startClient }) => { 'browser_drag', 'browser_evaluate', 'browser_file_upload', + 'browser_fill_form', 'browser_handle_dialog', 'browser_hover', 'browser_select_option', diff --git a/tests/form.spec.ts b/tests/form.spec.ts new file mode 100644 index 0000000..458cc4c --- /dev/null +++ b/tests/form.spec.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures.js'; + +test('browser_fill_form (textbox)', async ({ client, server }) => { + server.setContent('/', ` + + + +
+ + + + + +
+ + + `, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + expect(await client.callTool({ + name: 'browser_fill_form', + arguments: { + fields: [ + { + name: 'Name textbox', + type: 'textbox', + ref: 'e4', + value: 'John Doe' + }, + { + name: 'Email textbox', + type: 'textbox', + ref: 'e6', + value: 'john.doe@example.com' + }, + { + name: 'Age textbox', + type: 'slider', + ref: 'e8', + value: '25' + }, + { + name: 'Country select', + type: 'combobox', + ref: 'e10', + value: 'United States' + }, + { + name: 'Subscribe checkbox', + type: 'checkbox', + ref: 'e12', + value: 'true' + }, + ] + }, + })).toHaveResponse({ + code: `await page.getByRole('textbox', { name: 'Name' }).fill('John Doe'); +await page.getByRole('textbox', { name: 'Email' }).fill('john.doe@example.com'); +await page.getByRole('slider', { name: 'Age' }).fill('25'); +await page.getByLabel('Choose a country United').selectOption('United States'); +await page.getByRole('checkbox', { name: 'Subscribe to newsletter' }).setChecked('true');`, + }); + + const response = await client.callTool({ + name: 'browser_snapshot', + arguments: { + }, + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/textbox "Name".*John Doe/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/textbox "Email".*john.doe@example.com/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringMatching(/slider "Age".*"25"/), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringContaining('option \"United States\" [selected]'), + }); + expect.soft(response).toHaveResponse({ + pageState: expect.stringContaining('checkbox \"Subscribe to newsletter\" [checked]'), + }); +});