feat: browser_choose_file (#52)

Resolves https://github.com/microsoft/playwright-mcp/issues/31.

Prompt used for testing: 
```
upload test.txt to dropbox
```

This won't work for asynchronously opened filechoosers, but let's start
with the synchronous variant. I also tested with including the file
chooser open state in the snapshot, but that didn't give better results.

I also tested with OneDrive and WeTransfer, but somehow our ARIA
snapshots for those pages are missing some elements that are crucial to
locate the upload buttons.
This commit is contained in:
Simon Knott
2025-03-27 20:49:57 +01:00
committed by GitHub
parent f033213618
commit 5e200405e5
6 changed files with 152 additions and 10 deletions

View File

@@ -14,6 +14,7 @@
* limitations under the License.
*/
import fs from 'fs/promises';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { test, expect } from './fixtures';
@@ -38,6 +39,9 @@ test('test tool list', async ({ server, visionServer }) => {
expect.objectContaining({
name: 'browser_go_forward',
}),
expect.objectContaining({
name: 'browser_choose_file',
}),
expect.objectContaining({
name: 'browser_snapshot',
}),
@@ -455,6 +459,83 @@ test('stitched aria frames', async ({ server }) => {
}));
});
test('browser_choose_file', async ({ server }) => {
let response = await server.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<html><title>Title</title><input type="file" /><button>Button</button></html>',
},
},
});
expect(response.result.content[0].text).toContain('- textbox [ref=s1e4]');
response = await server.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'browser_click',
arguments: {
element: 'Textbox',
ref: 's1e4',
},
},
});
expect(response.result.content[0].text).toContain('There is a file chooser visible that requires browser_choose_file to be called');
const filePath = test.info().outputPath('test.txt');
await fs.writeFile(filePath, 'Hello, world!');
response = await server.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'browser_choose_file',
arguments: {
paths: [filePath],
},
},
});
expect(response.result.content[0].text).not.toContain('There is a file chooser visible that requires browser_choose_file to be called');
expect(response.result.content[0].text).toContain('textbox [ref=s3e4]: C:\\fakepath\\test.txt');
response = await server.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'browser_click',
arguments: {
element: 'Textbox',
ref: 's3e4',
},
},
});
expect(response.result.content[0].text).toContain('There is a file chooser visible that requires browser_choose_file to be called');
expect(response.result.content[0].text).toContain('button "Button" [ref=s4e5]');
response = await server.send({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'browser_click',
arguments: {
element: 'Button',
ref: 's4e5',
},
},
});
expect(response.result.content[0].text, 'not submitting browser_choose_file dismisses file chooser').not.toContain('There is a file chooser visible that requires browser_choose_file to be called');
});
test('sse transport', async () => {
const cp = spawn('node', [path.join(__dirname, '../cli.js'), '--port', '0'], { stdio: 'pipe' });
try {