Files
AI/참고/playwright-main/tests/mcp/files.spec.ts

438 lines
12 KiB
TypeScript
Raw Normal View History

2026-05-12 19:40:31 +09:00
/**
* 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 fs from 'fs/promises';
import path from 'path';
import { test, expect, parseResponse } from './fixtures';
test('browser_file_upload', async ({ client, server }, testInfo) => {
server.setContent('/', `
<input type="file" />
<button>Button</button>
`, 'text/html');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
snapshot: expect.stringContaining(`- generic [active] [ref=e1]:
- button "Choose File" [ref=e2]
- button "Button" [ref=e3]`),
});
{
expect(await client.callTool({
name: 'browser_file_upload',
arguments: { paths: [] },
})).toHaveResponse({
isError: true,
error: expect.stringContaining(`The tool "browser_file_upload" can only be used when there is related modal state present.`),
modalState: undefined,
});
}
expect(await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
})).toHaveResponse({
modalState: expect.stringContaining(`- [File chooser]: can be handled by browser_file_upload`),
});
const filePath = testInfo.outputPath('test.txt');
await fs.writeFile(filePath, 'Hello, world!');
{
const response = await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [filePath],
},
});
expect(response).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
modalState: undefined,
});
}
{
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
expect(response).toHaveResponse({
modalState: `- [File chooser]: can be handled by browser_file_upload`,
});
}
{
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Button',
target: 'e3',
},
});
expect(response).toHaveResponse({
isError: true,
error: `Error: Tool "browser_click" does not handle the modal state.`,
modalState: expect.stringContaining(`- [File chooser]: can be handled by browser_file_upload`),
});
}
});
test('clicking on download link emits download', async ({ startClient, server }, testInfo) => {
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
server.setContent('/', `<a href="/download" download="test.txt">Download</a>`, 'text/html');
server.setContent('/download', 'Data', 'text/plain');
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
})).toHaveResponse({
snapshot: expect.stringContaining(`- link "Download" [ref=e2]`),
});
const response = await client.callTool({
name: 'browser_click',
arguments: {
element: 'Download link',
target: 'e2',
},
});
const parsed = parseResponse(response);
let events = parsed.events;
await expect.poll(async () => {
const r = await client.callTool({ name: 'browser_snapshot' });
const p = parseResponse(r);
if (p.events)
events += '\n' + p.events;
return events;
}).toContain(`- Downloading file test.txt ...
- Downloaded file test.txt to "output${path.sep}test.txt"`);
});
test('navigating to download link emits download', async ({ startClient, server, mcpBrowser }, testInfo) => {
test.skip(mcpBrowser !== 'chromium', 'This test is racy');
const { client } = await startClient({
config: { outputDir: testInfo.outputPath('output') },
});
server.setRoute('/download', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename=test.txt',
});
res.end('Hello world!');
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: server.PREFIX + '/download',
},
})).toHaveResponse({
events: expect.stringMatching(`- Downloaded file test\.txt to|- Downloading file test\.txt`),
});
});
test('file upload restricted to roots by default', async ({ startClient, server }, testInfo) => {
const rootDir = testInfo.outputPath('workspace');
await fs.mkdir(rootDir, { recursive: true });
const { client } = await startClient({
roots: [
{
name: 'workspace',
uri: `file://${rootDir}`,
}
],
});
server.setContent('/', `<input type="file" />`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
// Click on file input to trigger file chooser
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
// Create a file inside the root
const fileInsideRoot = testInfo.outputPath('workspace', 'inside.txt');
await fs.writeFile(fileInsideRoot, 'Inside root');
// Should succeed - file is inside root
expect(await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [fileInsideRoot],
},
})).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
});
// Click again to open file chooser
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
// Create a file outside the root
const fileOutsideRoot = testInfo.outputPath('outside.txt');
await fs.writeFile(fileOutsideRoot, 'Outside root');
// Should fail - file is outside root
expect(await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [fileOutsideRoot],
},
})).toHaveResponse({
isError: true,
error: expect.stringMatching('File access denied: .* is outside allowed roots'),
});
});
test('file upload is restricted to cwd if no roots are configured', async ({ startClient, server }, testInfo) => {
const rootDir = testInfo.outputPath('workspace');
await fs.mkdir(rootDir, { recursive: true });
const { client } = await startClient({
cwd: rootDir,
});
server.setContent('/', `<input type="file" />`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
// Click on file input to trigger file chooser
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
// Create a file inside the root
const fileInsideRoot = testInfo.outputPath('workspace', 'inside.txt');
await fs.writeFile(fileInsideRoot, 'Inside root');
// Should succeed - file is inside root
expect(await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [fileInsideRoot],
},
})).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
});
// Click again to open file chooser
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
const fileOutsideRoot = testInfo.outputPath('outside.txt');
await fs.writeFile(fileOutsideRoot, 'Outside root');
expect(await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [fileOutsideRoot],
},
})).toHaveResponse({
isError: true,
error: expect.stringMatching('File access denied: .* is outside allowed roots. Allowed roots: ' + rootDir.replace(/\\/g, '\\\\')),
});
});
test('file upload unrestricted when flag is set', async ({ startClient, server }, testInfo) => {
const rootDir = testInfo.outputPath('workspace');
await fs.mkdir(rootDir, { recursive: true });
const { client } = await startClient({
config: {
allowUnrestrictedFileAccess: true,
},
roots: [
{
name: 'workspace',
uri: `file://${rootDir}`,
}
],
});
server.setContent('/', `<input type="file" />`, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
// Click on file input to trigger file chooser
await client.callTool({
name: 'browser_click',
arguments: {
element: 'Textbox',
target: 'e2',
},
});
// Create a file outside the root
const fileOutsideRoot = testInfo.outputPath('outside.txt');
await fs.writeFile(fileOutsideRoot, 'Outside root');
// Should succeed - unrestricted uploads are allowed
expect(await client.callTool({
name: 'browser_file_upload',
arguments: {
paths: [fileOutsideRoot],
},
})).toHaveResponse({
code: expect.stringContaining(`await fileChooser.setFiles(`),
});
});
const dropzoneHtml = `
<div id="dropzone" aria-label="dropzone" style="width:300px;height:200px;border:2px dashed #888"></div>
<script>
window.__dropInfo = null;
const zone = document.getElementById('dropzone');
zone.addEventListener('dragenter', e => e.preventDefault());
zone.addEventListener('dragover', e => e.preventDefault());
zone.addEventListener('drop', async e => {
e.preventDefault();
const files = [];
for (const f of e.dataTransfer.files)
files.push({ name: f.name, size: f.size, text: await f.text() });
const data = {};
for (const t of e.dataTransfer.types) {
if (t !== 'Files')
data[t] = e.dataTransfer.getData(t);
}
window.__dropInfo = { files, data };
});
</script>
`;
test('browser_drop files', async ({ client, server }, testInfo) => {
server.setContent('/', dropzoneHtml, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
const filePath = testInfo.outputPath('drop-me.txt');
await fs.writeFile(filePath, 'hello');
expect(await client.callTool({
name: 'browser_drop',
arguments: {
element: 'dropzone',
target: 'e2',
paths: [filePath],
},
})).toHaveResponse({
code: expect.stringContaining(`.drop(`),
});
expect(await client.callTool({
name: 'browser_evaluate',
arguments: { function: '() => window.__dropInfo' },
})).toHaveResponse({
result: expect.stringContaining(`"text": "hello"`),
});
});
test('browser_drop data', async ({ client, server }) => {
server.setContent('/', dropzoneHtml, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_drop',
arguments: {
element: 'dropzone',
target: 'e2',
data: { 'text/plain': 'hello world' },
},
})).toHaveResponse({
code: expect.stringContaining(`.drop(`),
});
expect(await client.callTool({
name: 'browser_evaluate',
arguments: { function: '() => window.__dropInfo.data["text/plain"]' },
})).toHaveResponse({
result: `"hello world"`,
});
});
test('browser_drop requires paths or data', async ({ client, server }) => {
server.setContent('/', dropzoneHtml, 'text/html');
await client.callTool({
name: 'browser_navigate',
arguments: { url: server.PREFIX },
});
expect(await client.callTool({
name: 'browser_drop',
arguments: {
element: 'dropzone',
target: 'e2',
},
})).toHaveResponse({
isError: true,
error: expect.stringContaining(`At least one of "paths" or "data" must be provided.`),
});
});