Record — file transfer in agent-driven browsers
The automation surface of the in-app browser exposes clicks, typing, and DOM evaluation, but a
native file chooser is explicitly unsupported — a normal setFiles-style upload fails.
Separately, when a web app generates a download in page JavaScript (a Blob handed to
URL.createObjectURL and clicked through an anchor), the bytes never reach the agent's
filesystem, so the agent cannot verify what the page actually produced. A third annoyance: each
agent JavaScript call runs in a fresh kernel, so a variable captured in one call is gone in the next.
File through DataTransferConstruct the file in page context, assign it to the input's files list, and dispatch
change. This works for most JS-driven upload widgets (validated, drop-zone apps,
converters) because they all read input.files.
// inside page evaluate(); b64 = base64 of the local file, read by the agent beforehand
const bin = atob(b64);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
const file = new File([bytes], "sample.mp4", { type: "video/mp4" });
const dt = new DataTransfer();
dt.items.add(file);
const input = document.querySelector('input[type="file"]'); // or getElementById(...)
input.files = dt.files;
input.dispatchEvent(new Event("change", { bubbles: true }));
Verified against four different sites during a product-research session (validators, converters); each treated the injected file exactly like a user pick.
URL.createObjectURLPatch URL.createObjectURL before triggering the download button, read the
Blob back as base64, and write it to disk from the agent's shell in the same call.
const result = await page.evaluate(() => new Promise(resolve => {
const orig = URL.createObjectURL.bind(URL);
URL.createObjectURL = blob => {
const url = orig(blob);
if (blob && blob.type && blob.type.includes("zip")) {
const fr = new FileReader();
fr.onload = () => resolve({ b64: String(fr.result).split(",")[1] });
fr.readAsDataURL(blob);
}
return url;
};
document.querySelector("#generate").click();
setTimeout(() => resolve({ timeout: true }), 15000);
}));
// same call: fs.writeFileSync("out.zip", Buffer.from(result.b64, "base64"))
Verified by capturing generated ZIPs from a local tool and from a third-party converter, then hash-comparing the captured bytes.
Each automation call starts a fresh JavaScript kernel. A captured blob or a patched function is gone in the next call. Anything you need later must be written to disk inside the same call, or re-created in the next call. Treat every agent browser call as a separate process.
The official documentation for this browser states file-chooser automation is unsupported — and stops there. What was missing is the working set of workarounds with their exact boundaries: which upload widgets accept injection, how to verify generated downloads byte-for-byte, and the cross-call state rule that bites you the first time a capture "mysteriously" disappears.
<input type="file">) need the DataTransfer event dispatched on the drop target instead — not exercised here.Content-Disposition follow a different path (not exercised).