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();