Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b944e8cbaf | ||
|
|
f8f6e2fcc3 |
@@ -494,15 +494,6 @@ http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
<!-- NOTE: This has been generated via update-readme.js -->
|
||||||
|
|
||||||
- **browser_fill_form**
|
|
||||||
- Title: Fill form
|
|
||||||
- Description: Fill multiple form fields
|
|
||||||
- Parameters:
|
|
||||||
- `fields` (array): Fields to fill in
|
|
||||||
- Read-only: **false**
|
|
||||||
|
|
||||||
<!-- NOTE: This has been generated via update-readme.js -->
|
|
||||||
|
|
||||||
- **browser_handle_dialog**
|
- **browser_handle_dialog**
|
||||||
- Title: Handle a dialog
|
- Title: Handle a dialog
|
||||||
- Description: Handle a dialog
|
- Description: Handle a dialog
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Playwright MCP Bridge",
|
"name": "Playwright MCP Bridge",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"description": "Share browser tabs with Playwright MCP server",
|
"description": "Share browser tabs with Playwright MCP server",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9nMS2b0WCohjVHPGb8D9qAdkbIngDqoAjTeSccHJijgcONejge+OJxOQOMLu7b0ovt1c9BiEJa5JcpM+EHFVGL1vluBxK71zmBy1m2f9vZF3HG0LSCp7YRkum9rAIEthDwbkxx6XTvpmAY5rjFa/NON6b9Hlbo+8peUSkoOK7HTwYnnI36asZ9eUTiveIf+DMPLojW2UX33vDWG2UKvMVDewzclb4+uLxAYshY7Mx8we/b44xu+Anb/EBLKjOPk9Yh541xJ5Ozc8EiP/5yxOp9c/lRiYUHaRW+4r0HKZyFt0eZ52ti2iM4Nfk7jRXR7an3JPsUIf5deC/1cVM/+1ZQIDAQAB",
|
||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"debugger",
|
"debugger",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"tabs",
|
"tabs",
|
||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
|
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "lib/background.js",
|
"service_worker": "lib/background.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
|
|
||||||
"action": {
|
"action": {
|
||||||
"default_title": "Playwright MCP Bridge",
|
"default_title": "Playwright MCP Bridge",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
"128": "icons/icon-128.png"
|
"128": "icons/icon-128.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon-16.png",
|
"16": "icons/icon-16.png",
|
||||||
"32": "icons/icon-32.png",
|
"32": "icons/icon-32.png",
|
||||||
|
|||||||
4
extension/package-lock.json
generated
4
extension/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp-extension",
|
"name": "@playwright/mcp-extension",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@playwright/mcp-extension",
|
"name": "@playwright/mcp-extension",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.0.315",
|
"@types/chrome": "^0.0.315",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp-extension",
|
"name": "@playwright/mcp-extension",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"description": "Playwright MCP Browser Extension",
|
"description": "Playwright MCP Browser Extension",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import type { TabInfo } from './tabItem.js';
|
|||||||
type Status =
|
type Status =
|
||||||
| { type: 'connecting'; message: string }
|
| { type: 'connecting'; message: string }
|
||||||
| { type: 'connected'; message: string }
|
| { type: 'connected'; message: string }
|
||||||
| { type: 'error'; message: string };
|
| { type: 'error'; message: string }
|
||||||
|
| { type: 'error'; versionMismatch: { pwMcpVersion: string; extensionVersion: string; downloadUrl: string } };
|
||||||
|
|
||||||
const ConnectApp: React.FC = () => {
|
const ConnectApp: React.FC = () => {
|
||||||
const [tabs, setTabs] = useState<TabInfo[]>([]);
|
const [tabs, setTabs] = useState<TabInfo[]>([]);
|
||||||
@@ -58,6 +59,23 @@ const ConnectApp: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pwMcpVersion = params.get('pwMcpVersion');
|
||||||
|
const extensionVersion = chrome.runtime.getManifest().version;
|
||||||
|
if (pwMcpVersion !== extensionVersion) {
|
||||||
|
const downloadUrl = params.get('downloadUrl') || `https://github.com/microsoft/playwright-mcp/releases/download/v${extensionVersion}/playwright-mcp-extension-v${extensionVersion}.zip`;
|
||||||
|
setShowButtons(false);
|
||||||
|
setShowTabList(false);
|
||||||
|
setStatus({
|
||||||
|
type: 'error',
|
||||||
|
versionMismatch: {
|
||||||
|
pwMcpVersion: pwMcpVersion || 'unknown',
|
||||||
|
extensionVersion,
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
void connectToMCPRelay(relayUrl);
|
void connectToMCPRelay(relayUrl);
|
||||||
|
|
||||||
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
|
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
|
||||||
@@ -181,11 +199,50 @@ const ConnectApp: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const VersionMismatchError: React.FC<{ pwMcpVersion: string; extensionVersion: string; downloadUrl: string }> = ({ pwMcpVersion, extensionVersion, downloadUrl }) => {
|
||||||
|
const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';
|
||||||
|
|
||||||
|
const handleDownloadAndOpenExtensions = () => {
|
||||||
|
// Start download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.download = `playwright-mcp-extension-v${extensionVersion}.zip`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||||
|
if (tabs[0]?.id)
|
||||||
|
chrome.tabs.update(tabs[0].id, { url: 'chrome://extensions/' });
|
||||||
|
});
|
||||||
|
}, 1000); // Wait 1 second for download to initiate
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Incompatible Playwright MCP version: {pwMcpVersion} (extension version: {extensionVersion}).{' '}
|
||||||
|
<button
|
||||||
|
onClick={handleDownloadAndOpenExtensions}
|
||||||
|
className='link-button'
|
||||||
|
>Click here</button> to download the matching extension, then drag and drop it into the Chrome Extensions page.{' '}
|
||||||
|
See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
|
const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`status-banner ${status.type}`}>
|
<div className={`status-banner ${status.type}`}>
|
||||||
{status.message}
|
{'versionMismatch' in status ? (
|
||||||
|
<VersionMismatchError
|
||||||
|
pwMcpVersion={status.versionMismatch.pwMcpVersion}
|
||||||
|
extensionVersion={status.versionMismatch.extensionVersion}
|
||||||
|
downloadUrl={status.versionMismatch.downloadUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
status.message
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
|
import packageJSON from '../../package.json' assert { type: 'json' };
|
||||||
import { test as base, expect } from '../../tests/fixtures.js';
|
import { test as base, expect } from '../../tests/fixtures.js';
|
||||||
|
|
||||||
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
@@ -116,7 +117,7 @@ async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testWithOldExtensionVersion = test.extend({
|
const testWithOldVersion = test.extend({
|
||||||
pathToExtension: async ({}, use, testInfo) => {
|
pathToExtension: async ({}, use, testInfo) => {
|
||||||
const extensionDir = testInfo.outputPath('extension');
|
const extensionDir = testInfo.outputPath('extension');
|
||||||
const oldPath = fileURLToPath(new URL('../dist', import.meta.url));
|
const oldPath = fileURLToPath(new URL('../dist', import.meta.url));
|
||||||
@@ -215,7 +216,7 @@ for (const [mode, startClientMethod] of [
|
|||||||
await confirmationPagePromise;
|
await confirmationPagePromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithOldExtensionVersion(`works with old extension version (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
|
testWithOldVersion(`extension version mismatch (${mode})`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {
|
||||||
useShortConnectionTimeout(500);
|
useShortConnectionTimeout(500);
|
||||||
|
|
||||||
// Prelaunch the browser, so that it is properly closed after the test.
|
// Prelaunch the browser, so that it is properly closed after the test.
|
||||||
@@ -232,13 +233,19 @@ for (const [mode, startClientMethod] of [
|
|||||||
arguments: { url: server.HELLO_WORLD },
|
arguments: { url: server.HELLO_WORLD },
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectorPage = await confirmationPagePromise;
|
const confirmationPage = await confirmationPagePromise;
|
||||||
// For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector
|
await expect(confirmationPage.locator('.status-banner')).toHaveText(`Incompatible Playwright MCP version: ${packageJSON.version} (extension version: 0.0.1). Click here to download the matching extension, then drag and drop it into the Chrome Extensions page. See installation instructions for more details.`);
|
||||||
await selectorPage.getByRole('button', { name: 'Allow' }).click();
|
|
||||||
|
|
||||||
expect(await navigateResponse).toHaveResponse({
|
expect(await navigateResponse).toHaveResponse({
|
||||||
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
|
result: expect.stringContaining('Extension connection timeout.'),
|
||||||
|
isError: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const downloadPromise = confirmationPage.waitForEvent('download');
|
||||||
|
await confirmationPage.locator('.status-banner').getByRole('button', { name: 'Click here' }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.url()).toBe(`https://github.com/microsoft/playwright-mcp/releases/download/v0.0.1/playwright-mcp-extension-v0.0.1.zip`);
|
||||||
|
await download.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.16.0",
|
"@modelcontextprotocol/sdk": "^1.16.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@playwright/mcp",
|
"name": "@playwright/mcp",
|
||||||
"version": "0.0.35",
|
"version": "0.0.34",
|
||||||
"description": "Playwright Tools for MCP",
|
"description": "Playwright Tools for MCP",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { WebSocket, WebSocketServer } from 'ws';
|
|||||||
import { httpAddressToString } from '../mcp/http.js';
|
import { httpAddressToString } from '../mcp/http.js';
|
||||||
import { logUnhandledError } from '../utils/log.js';
|
import { logUnhandledError } from '../utils/log.js';
|
||||||
import { ManualPromise } from '../mcp/manualPromise.js';
|
import { ManualPromise } from '../mcp/manualPromise.js';
|
||||||
|
import { packageJSON } from '../utils/package.js';
|
||||||
|
|
||||||
import type websocket from 'ws';
|
import type websocket from 'ws';
|
||||||
import type { ClientInfo } from '../browserContextFactory.js';
|
import type { ClientInfo } from '../browserContextFactory.js';
|
||||||
@@ -119,6 +120,7 @@ export class CDPRelayServer {
|
|||||||
version: clientInfo.version,
|
version: clientInfo.version,
|
||||||
};
|
};
|
||||||
url.searchParams.set('client', JSON.stringify(client));
|
url.searchParams.set('client', JSON.stringify(client));
|
||||||
|
url.searchParams.set('pwMcpVersion', packageJSON.version);
|
||||||
if (toolName)
|
if (toolName)
|
||||||
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
|
||||||
const href = url.toString();
|
const href = url.toString();
|
||||||
|
|||||||
114
src/test.ts
Normal file
114
src/test.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
|
import { ListRootsRequestSchema, PingRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
|
export async function connectMCP() {
|
||||||
|
// const transport = new StreamableHTTPClientTransport(new URL('http://localhost:4242/mcp'));
|
||||||
|
|
||||||
|
const transport = new StdioClientTransport({
|
||||||
|
command: 'node',
|
||||||
|
env: process.env as any,
|
||||||
|
args: [
|
||||||
|
'/Users/yurys/playwright-mcp/cli.js',
|
||||||
|
// '--browser=chrome-canary',
|
||||||
|
// '--extension'
|
||||||
|
// '--browser=chromium',
|
||||||
|
// '--no-sandbox',
|
||||||
|
// '--isolated',
|
||||||
|
],
|
||||||
|
stderr: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.error('will create client');
|
||||||
|
const client = new Client({ name: 'Visual Studio Code', version: '1.0.0' });
|
||||||
|
client.setRequestHandler(PingRequestSchema, async () => ({}));
|
||||||
|
|
||||||
|
console.error('Will connect');
|
||||||
|
try {
|
||||||
|
await client.connect(transport);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
}
|
||||||
|
console.error('Connected');
|
||||||
|
|
||||||
|
// const tools = await client.listTools();
|
||||||
|
// console.log('Available tools:', tools.tools.length);
|
||||||
|
|
||||||
|
// await client.ping();
|
||||||
|
// console.error('Pinged');
|
||||||
|
|
||||||
|
{
|
||||||
|
const response = await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'https://amazon.com/'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Navigated to Amazon', response.isError ? 'error' : '', response.error ? response.error : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// const r = await client.callTool({
|
||||||
|
// name: 'browser_connect',
|
||||||
|
// arguments: {
|
||||||
|
// name: 'extension'
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// console.log('Connected to extension', r.isError ? 'error' : '', r.content);
|
||||||
|
|
||||||
|
const response = await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'https://google.com/'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Navigated to Google', response.isError ? 'error' : '', response.isError ? response : '');
|
||||||
|
|
||||||
|
if (response.isError)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const response2 = await client.callTool({
|
||||||
|
name: 'browser_type',
|
||||||
|
arguments: {
|
||||||
|
text: 'Browser MCP',
|
||||||
|
submit: true,
|
||||||
|
element: 'combobox "Search" [active] [ref=e44]',
|
||||||
|
ref: 'e44',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log('Typed text', response2.isError ? response2.content : '');
|
||||||
|
|
||||||
|
// console.log('Closing browser...');
|
||||||
|
// const response3 = await client.callTool({
|
||||||
|
// name: 'browser_close',
|
||||||
|
// arguments: {}
|
||||||
|
// });
|
||||||
|
// console.log('Closed browser');
|
||||||
|
// console.log(response3.isError ? 'error' : '', response3.error ? response3.error : '');
|
||||||
|
|
||||||
|
|
||||||
|
// await new Promise(resolve => setTimeout(resolve, 5_000));
|
||||||
|
|
||||||
|
// await transport.terminateSession();
|
||||||
|
await client.close();
|
||||||
|
console.log('Closed MCP client');
|
||||||
|
}
|
||||||
|
|
||||||
|
void connectMCP();
|
||||||
@@ -19,7 +19,6 @@ import console from './tools/console.js';
|
|||||||
import dialogs from './tools/dialogs.js';
|
import dialogs from './tools/dialogs.js';
|
||||||
import evaluate from './tools/evaluate.js';
|
import evaluate from './tools/evaluate.js';
|
||||||
import files from './tools/files.js';
|
import files from './tools/files.js';
|
||||||
import form from './tools/form.js';
|
|
||||||
import install from './tools/install.js';
|
import install from './tools/install.js';
|
||||||
import keyboard from './tools/keyboard.js';
|
import keyboard from './tools/keyboard.js';
|
||||||
import navigate from './tools/navigate.js';
|
import navigate from './tools/navigate.js';
|
||||||
@@ -40,7 +39,6 @@ export const allTools: Tool<any>[] = [
|
|||||||
...dialogs,
|
...dialogs,
|
||||||
...evaluate,
|
...evaluate,
|
||||||
...files,
|
...files,
|
||||||
...form,
|
|
||||||
...install,
|
...install,
|
||||||
...keyboard,
|
...keyboard,
|
||||||
...navigate,
|
...navigate,
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,
|
|
||||||
];
|
|
||||||
@@ -24,7 +24,6 @@ test('test snapshot tool list', async ({ client }) => {
|
|||||||
'browser_drag',
|
'browser_drag',
|
||||||
'browser_evaluate',
|
'browser_evaluate',
|
||||||
'browser_file_upload',
|
'browser_file_upload',
|
||||||
'browser_fill_form',
|
|
||||||
'browser_handle_dialog',
|
'browser_handle_dialog',
|
||||||
'browser_hover',
|
'browser_hover',
|
||||||
'browser_select_option',
|
'browser_select_option',
|
||||||
@@ -55,7 +54,6 @@ test('test tool list proxy mode', async ({ startClient }) => {
|
|||||||
'browser_drag',
|
'browser_drag',
|
||||||
'browser_evaluate',
|
'browser_evaluate',
|
||||||
'browser_file_upload',
|
'browser_file_upload',
|
||||||
'browser_fill_form',
|
|
||||||
'browser_handle_dialog',
|
'browser_handle_dialog',
|
||||||
'browser_hover',
|
'browser_hover',
|
||||||
'browser_select_option',
|
'browser_select_option',
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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('/', `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<form>
|
|
||||||
<label>
|
|
||||||
<input type="text" id="name" name="name" />
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="email" id="email" name="email" />
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="range" id="age" name="age" min="18" max="100" />
|
|
||||||
Age
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<select id="country" name="country">
|
|
||||||
<option value="">Choose a country</option>
|
|
||||||
<option value="us">United States</option>
|
|
||||||
<option value="uk">United Kingdom</option>
|
|
||||||
</select>
|
|
||||||
Country
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="subscribe" value="newsletter" />
|
|
||||||
Subscribe to newsletter
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`, '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]'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user