Skip to content

feat: added tool to upload files to input[type='file'] element #144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/docs/playwright-web/Supported-Tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,17 @@ Select an element with the `SELECT` tag.

---

### playwright_upload_file
Upload a file to an input[type='file'] element on the page.

- **Inputs:**
- **`selector`** *(string, required)*:
CSS selector for the file input element.
- **`filePath`** *(string, required)*:
Absolute path to the file to upload.

---

### Playwright_evaluate
Execute JavaScript in the browser console.

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@
"jest-playwright-preset": "4.0.0",
"shx": "^0.3.4",
"ts-jest": "^29.2.6",
"typescript": "^5.8.2"
"typescript": "^5.8.3"
}
}
6 changes: 5 additions & 1 deletion src/__tests__/toolHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jest.mock('playwright', () => {
const mockFill = jest.fn().mockImplementation(() => Promise.resolve());
const mockSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
const mockHover = jest.fn().mockImplementation(() => Promise.resolve());
const mockUploadFile = jest.fn().mockImplementation(() => Promise.resolve());
const mockEvaluate = jest.fn().mockImplementation(() => Promise.resolve());
const mockOn = jest.fn();
const mockIsClosed = jest.fn().mockReturnValue(false);
Expand All @@ -28,12 +29,14 @@ jest.mock('playwright', () => {
const mockLocatorFill = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorHover = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorUploadFile = jest.fn().mockImplementation(() => Promise.resolve());

const mockLocator = jest.fn().mockReturnValue({
click: mockLocatorClick,
fill: mockLocatorFill,
selectOption: mockLocatorSelectOption,
hover: mockLocatorHover
hover: mockLocatorHover,
uploadFile: mockLocatorUploadFile
});

const mockFrames = jest.fn().mockReturnValue([{
Expand All @@ -47,6 +50,7 @@ jest.mock('playwright', () => {
fill: mockFill,
selectOption: mockSelectOption,
hover: mockHover,
uploadFile: mockUploadFile,
evaluate: mockEvaluate,
on: mockOn,
frames: mockFrames,
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,12 @@ describe('Tool Definitions', () => {
expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('margin');
expect(saveAsPdfTool!.inputSchema.required).toEqual(['outputPath']);
});

test('should validate upload_file tool schema', () => {
const uploadFileTool = toolDefinitions.find(tool => tool.name === 'playwright_upload_file');
expect(uploadFileTool).toBeDefined();
expect(uploadFileTool!.inputSchema.properties).toHaveProperty('selector');
expect(uploadFileTool!.inputSchema.properties).toHaveProperty('filePath');
expect(uploadFileTool!.inputSchema.required).toEqual(['selector', 'filePath']);
});
});
26 changes: 24 additions & 2 deletions src/__tests__/tools/browser/interaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ClickTool,ClickAndSwitchTabTool, FillTool, SelectTool, HoverTool, EvaluateTool, IframeClickTool } from '../../../tools/browser/interaction.js';
import { ClickTool,ClickAndSwitchTabTool, FillTool, SelectTool, HoverTool, EvaluateTool, IframeClickTool, UploadFileTool } from '../../../tools/browser/interaction.js';
import { NavigationTool } from '../../../tools/browser/navigation.js';
import { ToolContext } from '../../../tools/common/types.js';
import { Page, Browser } from 'playwright';
Expand All @@ -9,6 +9,7 @@ const mockPageClick = jest.fn().mockImplementation(() => Promise.resolve());
const mockPageFill = jest.fn().mockImplementation(() => Promise.resolve());
const mockPageSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
const mockPageHover = jest.fn().mockImplementation(() => Promise.resolve());
const mockPageSetInputFiles = jest.fn().mockImplementation(() => Promise.resolve());
const mockPageWaitForSelector = jest.fn().mockImplementation(() => Promise.resolve());
const mockWaitForEvent = jest.fn().mockImplementation(() => Promise.resolve(mockNewPage));
const mockWaitForLoadState = jest.fn().mockImplementation(() => Promise.resolve());
Expand All @@ -29,13 +30,15 @@ const mockLocatorClick = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorFill = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorSelectOption = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorHover = jest.fn().mockImplementation(() => Promise.resolve());
const mockLocatorUploadFile = jest.fn().mockImplementation(() => Promise.resolve());

// Mock locator
const mockLocator = jest.fn().mockReturnValue({
click: mockLocatorClick,
fill: mockLocatorFill,
selectOption: mockLocatorSelectOption,
hover: mockLocatorHover
hover: mockLocatorHover,
uploadFile: mockLocatorUploadFile
});

// Mock iframe locator
Expand All @@ -60,6 +63,7 @@ const mockPage = {
fill: mockPageFill,
selectOption: mockPageSelectOption,
hover: mockPageHover,
setInputFiles: mockPageSetInputFiles,
waitForSelector: mockPageWaitForSelector,
locator: mockLocator,
frameLocator: mockFrameLocator,
Expand Down Expand Up @@ -97,6 +101,7 @@ describe('Browser Interaction Tools', () => {
let evaluateTool: EvaluateTool;
let iframeClickTool: IframeClickTool;
let clickAndSwitchTabTool: ClickAndSwitchTabTool;
let uploadFileTool: UploadFileTool;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -107,6 +112,7 @@ describe('Browser Interaction Tools', () => {
evaluateTool = new EvaluateTool(mockServer);
iframeClickTool = new IframeClickTool(mockServer);
clickAndSwitchTabTool = new ClickAndSwitchTabTool(mockServer);
uploadFileTool = new UploadFileTool(mockServer);
});

describe('ClickTool', () => {
Expand Down Expand Up @@ -276,6 +282,22 @@ describe('Browser Interaction Tools', () => {
});
});

describe('UploadFileTool', () => {
test('should upload a file to an input element', async () => {
const args = {
selector: '#file-input',
filePath: '/tmp/testfile.txt'
};

const result = await uploadFileTool.execute(args, mockContext);

expect(mockPageWaitForSelector).toHaveBeenCalledWith('#file-input');
expect(mockPageSetInputFiles).toHaveBeenCalledWith('#file-input', '/tmp/testfile.txt');
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain("Uploaded file '/tmp/testfile.txt' to '#file-input'");
});
});

describe('EvaluateTool', () => {
test('should evaluate JavaScript', async () => {
const args = {
Expand Down
8 changes: 7 additions & 1 deletion src/toolHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
SelectTool,
HoverTool,
EvaluateTool,
IframeFillTool
IframeFillTool,
UploadFileTool
} from './tools/browser/interaction.js';
import {
VisibleTextTool,
Expand Down Expand Up @@ -78,6 +79,7 @@ let iframeFillTool: IframeFillTool;
let fillTool: FillTool;
let selectTool: SelectTool;
let hoverTool: HoverTool;
let uploadFileTool: UploadFileTool;
let evaluateTool: EvaluateTool;
let expectResponseTool: ExpectResponseTool;
let assertResponseTool: AssertResponseTool;
Expand Down Expand Up @@ -316,6 +318,7 @@ function initializeTools(server: any) {
if (!fillTool) fillTool = new FillTool(server);
if (!selectTool) selectTool = new SelectTool(server);
if (!hoverTool) hoverTool = new HoverTool(server);
if (!uploadFileTool) uploadFileTool = new UploadFileTool(server);
if (!evaluateTool) evaluateTool = new EvaluateTool(server);
if (!expectResponseTool) expectResponseTool = new ExpectResponseTool(server);
if (!assertResponseTool) assertResponseTool = new AssertResponseTool(server);
Expand Down Expand Up @@ -489,6 +492,9 @@ export async function handleToolCall(

case "playwright_hover":
return await hoverTool.execute(args, context);

case "playwright_upload_file":
return await uploadFileTool.execute(args, context);

case "playwright_evaluate":
return await evaluateTool.execute(args, context);
Expand Down
13 changes: 13 additions & 0 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ export function createToolDefinitions() {
required: ["selector"],
},
},
{
name: "playwright_upload_file",
description: "Upload a file to an input[type='file'] element on the page",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for the file input element" },
filePath: { type: "string", description: "Absolute path to the file to upload" }
},
required: ["selector", "filePath"],
},
},
{
name: "playwright_evaluate",
description: "Execute JavaScript in the browser console",
Expand Down Expand Up @@ -435,6 +447,7 @@ export const BROWSER_TOOLS = [
"playwright_fill",
"playwright_select",
"playwright_hover",
"playwright_upload_file",
"playwright_evaluate",
"playwright_close",
"playwright_expect_response",
Expand Down
16 changes: 16 additions & 0 deletions src/tools/browser/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,22 @@ export class HoverTool extends BrowserToolBase {
}
}

/**
* Tool for uploading files
*/
export class UploadFileTool extends BrowserToolBase {
/**
* Execute the upload file tool
*/
async execute(args: any, context: ToolContext): Promise<ToolResponse> {
return this.safeExecute(context, async (page) => {
await page.waitForSelector(args.selector);
await page.setInputFiles(args.selector, args.filePath);
return createSuccessResponse(`Uploaded file '${args.filePath}' to '${args.selector}'`);
});
}
}

/**
* Tool for executing JavaScript in the browser
*/
Expand Down
Loading