From 2427f43359127279088210627bd5a6e68263c117 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Thu, 11 Jun 2026 10:47:02 -0700 Subject: [PATCH 1/2] fix(mcp): use optional chaining for nullable browser() in disposal (#41237) --- packages/playwright-core/src/tools/mcp/program.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 95460e4363a62dfde2e701b69514c1e3ee5b0c64 Mon Sep 17 00:00:00 2001 From: yen0304 Date: Fri, 12 Jun 2026 01:52:48 +0800 Subject: [PATCH 2/2] fix(codegen): use correct selector for hidden file input triggered by button (#41244) When a page uses a hidden `` triggered by another element: ```html ``` codegen recorded the `setInputFiles` action with the **button's** selector (from `_hoveredModel`, which points to the last visible element the mouse was over) instead of the actual file input's selector. The generated code: ```js // BUG: button is not an HTMLInputElement await page.getByRole('button', { name: 'select file' }).setInputFiles('filename'); ``` fails at runtime with `locator.setInputFiles: Error: Node is not an HTMLInputElement`. The fix: when the `input` event target (the file input element) differs from `_hoveredElement` (the last mouse-hovered element), derive the selector directly from the target via `generateSelector` instead of reusing the hover model. For visible file inputs where the user directly hovers and clicks, `target === _hoveredElement` so existing behaviour is preserved. Fixed output: ```js await page.getByRole('button', { name: 'select file' }).click(); await page.locator('input[type="file"]').setInputFiles('filename'); ``` Fixes https://github.com/microsoft/playwright/issues/41239 --- packages/injected/src/recorder/recorder.ts | 9 ++++++-- tests/library/inspector/cli-codegen-2.spec.ts | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) 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/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();