diff --git a/packages/playwright-core/src/tools/backend/files.ts b/packages/playwright-core/src/tools/backend/files.ts index 44e1f5ee6badd..86160f2e3cbc1 100644 --- a/packages/playwright-core/src/tools/backend/files.ts +++ b/packages/playwright-core/src/tools/backend/files.ts @@ -44,10 +44,16 @@ export const uploadFile = defineTabTool({ response.addCode(`await fileChooser.setFiles(${JSON.stringify(paths)})`); tab.clearModalState(modalState); - await tab.waitForCompletion(async () => { - if (paths) - await modalState.fileChooser.setFiles(paths); - }); + try { + await tab.waitForCompletion(async () => { + if (paths) + await modalState.fileChooser.setFiles(paths); + }); + } catch (e) { + tab.setModalState(modalState); + response.addError(e instanceof Error ? e.message : String(e)); + return; + } }, clearsModalState: 'fileChooser', diff --git a/packages/playwright-core/src/tools/backend/route.ts b/packages/playwright-core/src/tools/backend/route.ts index 71a3ae014f93d..9c2e293c0d13c 100644 --- a/packages/playwright-core/src/tools/backend/route.ts +++ b/packages/playwright-core/src/tools/backend/route.ts @@ -40,10 +40,18 @@ const route = defineTool({ }, handle: async (context, params, response) => { - const addHeaders = params.headers ? Object.fromEntries(params.headers.map(h => { - const colonIndex = h.indexOf(':'); - return [h.substring(0, colonIndex).trim(), h.substring(colonIndex + 1).trim()]; - })) : undefined; + let addHeaders: Record | undefined; + if (params.headers) { + addHeaders = {}; + for (const h of params.headers) { + const colonIndex = h.indexOf(':'); + if (colonIndex <= 0) { + response.addError(`Invalid header "${h}": expected "Name: Value"`); + return; + } + addHeaders[h.substring(0, colonIndex).trim()] = h.substring(colonIndex + 1).trim(); + } + } const removeHeaders = params.removeHeaders ? params.removeHeaders.split(',').map(h => h.trim()) : undefined; const handler = async (route: playwright.Route) => { diff --git a/tests/mcp/files.spec.ts b/tests/mcp/files.spec.ts index 583923a56a04f..8cc7ddaf9028e 100644 --- a/tests/mcp/files.spec.ts +++ b/tests/mcp/files.spec.ts @@ -103,6 +103,43 @@ test('browser_file_upload', async ({ client, server }, testInfo) => { } }); +test('browser_file_upload keeps chooser when setFiles fails', async ({ client, server }, testInfo) => { + server.setContent('/', ``, 'text/html'); + + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + }); + + await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Textbox', + target: 'e2', + }, + }); + + const missing = testInfo.outputPath('missing.txt'); + const failed = await client.callTool({ + name: 'browser_file_upload', + arguments: { paths: [missing] }, + }); + expect(failed).toHaveResponse({ + isError: true, + modalState: expect.stringContaining(`[File chooser]`), + }); + + const filePath = testInfo.outputPath('retry.txt'); + await fs.writeFile(filePath, 'retry'); + const retried = await client.callTool({ + name: 'browser_file_upload', + arguments: { paths: [filePath] }, + }); + expect(retried).toHaveResponse({ + modalState: undefined, + }); +}); + test('clicking on download link emits download', async ({ startClient, server }, testInfo) => { const { client } = await startClient({ config: { outputDir: testInfo.outputPath('output') }, diff --git a/tests/mcp/route.spec.ts b/tests/mcp/route.spec.ts index 6c1b5267a38f4..0f627d30868b7 100644 --- a/tests/mcp/route.spec.ts +++ b/tests/mcp/route.spec.ts @@ -136,6 +136,24 @@ test('browser_route modifies request headers', async ({ client, server }) => { expect(receivedHeaders['x-custom-header']).toBe('test-value'); }); +test('browser_route errors on header lines without a colon', async ({ client, server }) => { + await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.EMPTY_PAGE }, + }); + + expect(await client.callTool({ + name: 'browser_route', + arguments: { + pattern: '**/api/check', + headers: ['NotAHeader', 'X-Custom-Header: test-value'], + }, + })).toHaveResponse({ + isError: true, + error: expect.stringContaining('Invalid header "NotAHeader"'), + }); +}); + test('browser_route_list shows active routes', async ({ client, server }) => { await client.callTool({ name: 'browser_navigate',