From 911f8ec68f899bba63b0c062dd77e585b5a413ce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Jul 2026 16:50:36 +0100 Subject: [PATCH] fix(browser): avoid duplicate upload events (#103777) --- .../src/browser/pw-tools-core.downloads.ts | 14 -------------- ...s-core.interactions.set-input-files.test.ts | 18 +++++++++++++----- .../src/browser/pw-tools-core.interactions.ts | 11 ----------- .../browser/pw-tools-core.upload-paths.test.ts | 9 +++++++-- 4 files changed, 20 insertions(+), 32 deletions(-) diff --git a/extensions/browser/src/browser/pw-tools-core.downloads.ts b/extensions/browser/src/browser/pw-tools-core.downloads.ts index 306737ce07d0..f7b2fa127b72 100644 --- a/extensions/browser/src/browser/pw-tools-core.downloads.ts +++ b/extensions/browser/src/browser/pw-tools-core.downloads.ts @@ -93,20 +93,6 @@ export async function armFileUploadViaPlaywright(opts: { return; } await fileChooser.setFiles(uploadPathsResult.paths); - try { - const input = - typeof fileChooser.element === "function" - ? await Promise.resolve(fileChooser.element()) - : null; - if (input) { - await input.evaluate((el) => { - el.dispatchEvent(new Event("input", { bubbles: true })); - el.dispatchEvent(new Event("change", { bubbles: true })); - }); - } - } catch { - // Best-effort for sites that don't react to setFiles alone. - } }) .catch(() => { // Ignore timeouts; the chooser may never appear. diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts b/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts index 742624f378f0..faa04dd49ffb 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts @@ -45,16 +45,22 @@ vi.mock("./paths.js", () => { const { setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js"); -function seedSingleLocatorPage(): { setInputFiles: ReturnType } { +function seedSingleLocatorPage(): { + setInputFiles: ReturnType; + elementHandle: ReturnType; +} { const setInputFiles = vi.fn(async () => {}); + const elementHandle = vi.fn(async () => { + throw new Error("manual upload event dispatch is forbidden"); + }); locator = { setInputFiles, - elementHandle: vi.fn(async () => null), + elementHandle, }; page = { locator: vi.fn(() => ({ first: () => locator })), }; - return { setInputFiles }; + return { setInputFiles, elementHandle }; } describe("setInputFilesViaPlaywright", () => { @@ -68,8 +74,8 @@ describe("setInputFilesViaPlaywright", () => { }); }); - it("revalidates upload paths and uses resolved canonical paths for inputRef", async () => { - const { setInputFiles } = seedSingleLocatorPage(); + it("sets resolved files once and leaves browser events to Playwright", async () => { + const { setInputFiles, elementHandle } = seedSingleLocatorPage(); await setInputFilesViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -83,6 +89,8 @@ describe("setInputFilesViaPlaywright", () => { }); expect(refLocator).toHaveBeenCalledWith(page, "e7"); expect(setInputFiles).toHaveBeenCalledWith(["/private/tmp/openclaw/uploads/ok.txt"]); + expect(setInputFiles).toHaveBeenCalledTimes(1); + expect(elementHandle).not.toHaveBeenCalled(); }); it("throws and skips setInputFiles when use-time validation fails", async () => { diff --git a/extensions/browser/src/browser/pw-tools-core.interactions.ts b/extensions/browser/src/browser/pw-tools-core.interactions.ts index ee41cb781764..8e509b496888 100644 --- a/extensions/browser/src/browser/pw-tools-core.interactions.ts +++ b/extensions/browser/src/browser/pw-tools-core.interactions.ts @@ -1497,17 +1497,6 @@ export async function setInputFilesViaPlaywright(opts: { } catch (err) { throw toFriendlyInteractionError(err, inputRef || element); } - try { - const handle = await locator.elementHandle(); - if (handle) { - await handle.evaluate((el) => { - el.dispatchEvent(new Event("input", { bubbles: true })); - el.dispatchEvent(new Event("change", { bubbles: true })); - }); - } - } catch { - // Best-effort for sites that don't react to setInputFiles alone. - } } async function executeSingleAction( diff --git a/extensions/browser/src/browser/pw-tools-core.upload-paths.test.ts b/extensions/browser/src/browser/pw-tools-core.upload-paths.test.ts index 3e6a58fb03b7..40e50f814479 100644 --- a/extensions/browser/src/browser/pw-tools-core.upload-paths.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.upload-paths.test.ts @@ -26,7 +26,10 @@ installPwToolsCoreTestHooks(); const { armFileUploadViaPlaywright } = await import("./pw-tools-core.downloads.js"); function createFileChooserPageMocks() { - const fileChooser = { setFiles: vi.fn(async () => {}) }; + const element = vi.fn(async () => { + throw new Error("manual upload event dispatch is forbidden"); + }); + const fileChooser = { setFiles: vi.fn(async () => {}), element }; const press = vi.fn(async () => {}); const waitForEvent = vi.fn(async () => fileChooser); setPwToolsCoreCurrentPage({ @@ -44,7 +47,7 @@ describe("armFileUploadViaPlaywright upload path validation", () => { }); }); - it("sets files using resolved inbound media paths", async () => { + it("sets resolved files once and leaves browser events to Playwright", async () => { const { fileChooser } = createFileChooserPageMocks(); await armFileUploadViaPlaywright({ @@ -59,6 +62,8 @@ describe("armFileUploadViaPlaywright upload path validation", () => { "/home/user/.openclaw/media/inbound/report.pdf", ]); }); + expect(fileChooser.setFiles).toHaveBeenCalledTimes(1); + expect(fileChooser.element).not.toHaveBeenCalled(); }); it("escapes the chooser when paths are outside managed upload roots", async () => {