chore: save downloads to outputDir (#310)

This commit is contained in:
Simon Knott 2025-05-02 10:57:31 +02:00 committed by GitHub
parent 23ce973377
commit a15f0f301b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 3 deletions

View File

@ -20,10 +20,9 @@ import os from 'os';
import path from 'path';
import { devices } from 'playwright';
import { sanitizeForFilePath } from './tools/utils.js';
import type { Config, ToolCapability } from '../config.js';
import type { BrowserContextOptions, LaunchOptions } from 'playwright';
import { sanitizeForFilePath } from './tools/utils.js';
export type CLIOptions = {
browser?: string;

View File

@ -23,6 +23,7 @@ import { Tab } from './tab.js';
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
import type { Config } from '../config.js';
import { outputFile } from './config.js';
type PendingAction = {
dialogShown: ManualPromise<void>;
@ -38,6 +39,7 @@ export class Context {
private _currentTab: Tab | undefined;
private _modalStates: (ModalState & { tab: Tab })[] = [];
private _pendingAction: PendingAction | undefined;
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
constructor(tools: Tool[], config: Config) {
this.tools = tools;
@ -164,6 +166,17 @@ ${code.join('\n')}
};
}
if (this._downloads.length) {
result.push('', '### Downloads');
for (const entry of this._downloads) {
if (entry.finished)
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
else
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
}
result.push('');
}
if (this.tabs().length > 1)
result.push(await this.listTabsMarkdown(), '');
@ -228,6 +241,17 @@ ${code.join('\n')}
this._pendingAction?.dialogShown.resolve();
}
async downloadStarted(tab: Tab, download: playwright.Download) {
const entry = {
download,
finished: false,
outputFile: await outputFile(this.config, download.suggestedFilename())
};
this._downloads.push(entry);
await download.saveAs(entry.outputFile);
entry.finished = true;
}
private _onPageCreated(page: playwright.Page) {
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
this._tabs.push(tab);

View File

@ -48,6 +48,9 @@ export class Tab {
}, this);
});
page.on('dialog', dialog => this.context.dialogShown(this, dialog));
page.on('download', download => {
void this.context.downloadStarted(this, download);
});
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(5000);
}

View File

@ -31,7 +31,7 @@ const pdf = defineTool({
handle: async context => {
const tab = context.currentTabOrDie();
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}'.pdf'`);
const fileName = await outputFile(context.config, `page-${new Date().toISOString()}.pdf`);
const code = [
`// Save page as ${fileName}`,

View File

@ -16,6 +16,7 @@
import { test, expect } from './fixtures.js';
import fs from 'fs/promises';
import path from 'path';
test('browser_file_upload', async ({ client }) => {
expect(await client.callTool({
@ -96,3 +97,27 @@ The tool "browser_file_upload" can only be used when there is related modal stat
- [File chooser]: can be handled by the "browser_file_upload" tool`);
}
});
test('clicking on download link emits download', async ({ startClient }, testInfo) => {
const outputDir = testInfo.outputPath('output');
const client = await startClient({
config: { outputDir },
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: {
url: 'data:text/html,<a href="data:text/plain,Hello world!" download="test.txt">Download</a>',
},
})).toContainTextContent('- link "Download" [ref=s1e3]');
await expect.poll(() => client.callTool({
name: 'browser_click',
arguments: {
element: 'Download link',
ref: 's1e3',
},
})).toContainTextContent(`
### Downloads
- Downloaded file test.txt to ${path.join(outputDir, 'test-txt')}`);
});