diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts index 22a22144e4b7f..fd51d5ca05be3 100644 --- a/packages/injected/src/recorder/recorder.ts +++ b/packages/injected/src/recorder/recorder.ts @@ -412,10 +412,15 @@ class RecordActionTool implements RecorderTool { const target = this._recorder.deepEventTarget(event); if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') { + // When the file input is hidden and triggered by another element (e.g. a button with + // onclick="input.click()"), the hover model points to the trigger, not the input. + // Derive the selector from the actual target element in that case. + const selector = target === this._hoveredElement + ? this._hoveredModel!.selector + : this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName }).selector; this._recordAction({ name: 'setInputFiles', - // webkit doesn't focus file inputs on click, so activeModel is unreliable here. - selector: this._hoveredModel!.selector, + selector, signals: [], files: [...((target as HTMLInputElement).files || [])].map(file => file.name), }); diff --git a/packages/playwright-core/src/tools/mcp/program.ts b/packages/playwright-core/src/tools/mcp/program.ts index 06de1755de454..b697f8500a36d 100644 --- a/packages/playwright-core/src/tools/mcp/program.ts +++ b/packages/playwright-core/src/tools/mcp/program.ts @@ -138,7 +138,7 @@ export function decorateMCPCommand(command: Command) { sharedBrowserPromise = undefined; const browserContext = (backend as BrowserBackend).browserContext; await browserContext.close().catch(() => { }); - await browserContext.browser()!.close().catch(() => { }); + await browserContext.browser()?.close().catch(() => { }); } }; await mcpServer.start(factory, config.server); diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index 1bf8bf09b21dd..67ce3951537ae 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -199,6 +199,28 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFi await page.GetByRole(AriaRole.Button, new() { Name = "Choose File" }).SetInputFilesAsync(new[] { });`); }); + test('should upload a file via hidden input triggered by button', async ({ openRecorder, browserName, asset, isLinux }) => { + const { page, recorder } = await openRecorder(); + await recorder.setContentAndWait(` + + + `); + + await recorder.hoverOverElement('button'); + const [chooser] = await Promise.all([ + page.waitForEvent('filechooser'), + recorder.trustedClick(), + ]); + await new Promise(f => setTimeout(f, 1000)); + await chooser.setFiles(asset('file-to-upload.txt')); + + const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles'); + + // The setInputFiles call must target the actual file input, not the trigger button. + expect(sources.get('JavaScript')!.text).toContain(`setInputFiles('file-to-upload.txt')`); + expect(sources.get('JavaScript')!.text).not.toContain(`getByRole('button', { name: 'select file' }).setInputFiles`); + }); + test('should download files', async ({ openRecorder, server }) => { const { page, recorder } = await openRecorder();