fix(browser): avoid duplicate upload events (#103777)

This commit is contained in:
Peter Steinberger
2026-07-10 16:50:36 +01:00
committed by GitHub
parent db39fe8072
commit 911f8ec68f
4 changed files with 20 additions and 32 deletions
@@ -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.
@@ -45,16 +45,22 @@ vi.mock("./paths.js", () => {
const { setInputFilesViaPlaywright } = await import("./pw-tools-core.interactions.js");
function seedSingleLocatorPage(): { setInputFiles: ReturnType<typeof vi.fn> } {
function seedSingleLocatorPage(): {
setInputFiles: ReturnType<typeof vi.fn>;
elementHandle: ReturnType<typeof vi.fn>;
} {
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 () => {
@@ -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(
@@ -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 () => {