#![allow(
clippy::too_many_lines,
clippy::doc_markdown,
clippy::uninlined_format_args,
clippy::unwrap_used,
clippy::expect_used,
clippy::needless_pass_by_value
)]
use super::client::McpClient;
const SINGLE_FORM_HTML: &str = "<form id=\"f\">\
<input id=\"i\" type=\"file\" name=\"f\" />\
<button id=\"b\" type=\"button\">pick</button>\
</form>\
<script>\
const i = document.getElementById('i');\
const b = document.getElementById('b');\
b.addEventListener('click', () => i.click());\
i.addEventListener('change', () => {\
const files = i.files;\
const count = files.length;\
const first = count > 0 ? files[0].name : '';\
document.title = `count=${count};first=${first}`;\
});\
</script>";
const MULTIPLE_FORM_HTML: &str = "<form id=\"f\">\
<input id=\"i\" type=\"file\" name=\"f\" multiple />\
<button id=\"b\" type=\"button\">pick</button>\
</form>\
<script>\
const i = document.getElementById('i');\
const b = document.getElementById('b');\
b.addEventListener('click', () => i.click());\
i.addEventListener('change', () => {\
const files = i.files;\
const names = [];\
for (let k = 0; k < files.length; k++) names.push(files[k].name);\
document.title = `count=${files.length};names=${names.join('|')}`;\
});\
</script>";
const PAYLOAD_FORM_HTML: &str = "<form id=\"f\">\
<input id=\"i\" type=\"file\" name=\"f\" />\
<button id=\"b\" type=\"button\">pick</button>\
</form>\
<script>\
const i = document.getElementById('i');\
const b = document.getElementById('b');\
b.addEventListener('click', () => i.click());\
i.addEventListener('change', async () => {\
const f = i.files[0];\
const text = await f.text();\
document.title = `name=${f.name};size=${f.size};text=${text}`;\
});\
</script>";
const WAIT_FOR_TITLE_CALL: &str = r"
await page.evaluate(async (prefix) => {
for (let i = 0; i < 200; i++) {
const t = document.title;
if (t && t.startsWith(prefix)) return t;
await new Promise(r => setTimeout(r, 10));
}
return document.title;
}, PREFIX_JSON)
";
fn tmp_file(name: &str, content: &str) -> String {
let dir = std::env::temp_dir().join(format!("ferridriver-fc-tests-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create temp dir");
let path = dir.join(name);
std::fs::write(&path, content).expect("write temp file");
path.display().to_string()
}
pub fn test_file_chooser_single_string_path(c: &mut McpClient) {
if c.backend == "webkit" {
return;
}
let html = SINGLE_FORM_HTML.to_string();
c.nav_url(&format!("data:text/html,{}", urlencoding(&html)));
let path = tmp_file("a.txt", "alpha");
let script = format!(
r##"
const p = page.waitForEvent("filechooser", 10000);
await page.click("#b");
const chooser = await p;
const isMult = chooser.isMultiple();
const samePage = chooser.page().url() === page.url();
await chooser.setFiles({path});
const title = {wait};
return {{ isMult, samePage, title }};
"##,
path = serde_json::to_string(&path).unwrap(),
wait = WAIT_FOR_TITLE_CALL.replace("PREFIX_JSON", "\"count=\"").trim(),
);
let v = c.script_value(&script);
assert_eq!(
v["isMult"].as_bool(),
Some(false),
"single-file input reports isMultiple=false: {v}"
);
assert_eq!(
v["samePage"].as_bool(),
Some(true),
"fileChooser.page() resolves to the owning page: {v}"
);
assert_eq!(
v["title"].as_str(),
Some("count=1;first=a.txt"),
"page saw exactly the uploaded file: {v}"
);
}
pub fn test_file_chooser_multiple_string_array(c: &mut McpClient) {
if c.backend == "webkit" {
return;
}
let html = MULTIPLE_FORM_HTML.to_string();
c.nav_url(&format!("data:text/html,{}", urlencoding(&html)));
let p1 = tmp_file("a-multi.txt", "alpha");
let p2 = tmp_file("b-multi.txt", "beta");
let script = format!(
r##"
const p = page.waitForEvent("filechooser", 10000);
await page.click("#b");
const chooser = await p;
const isMult = chooser.isMultiple();
await chooser.setFiles([{p1}, {p2}]);
const title = {wait};
return {{ isMult, title }};
"##,
p1 = serde_json::to_string(&p1).unwrap(),
p2 = serde_json::to_string(&p2).unwrap(),
wait = WAIT_FOR_TITLE_CALL.replace("PREFIX_JSON", "\"count=\"").trim(),
);
let v = c.script_value(&script);
assert_eq!(
v["isMult"].as_bool(),
Some(true),
"multiple input reports isMultiple=true: {v}"
);
let title = v["title"].as_str().unwrap_or("");
assert!(
title == "count=2;names=a-multi.txt|b-multi.txt" || title == "count=2;names=b-multi.txt|a-multi.txt",
"both names present in input.files: {v}"
);
}
pub fn test_file_chooser_file_payload_single(c: &mut McpClient) {
if c.backend == "webkit" {
return;
}
let html = PAYLOAD_FORM_HTML.to_string();
c.nav_url(&format!("data:text/html,{}", urlencoding(&html)));
let script = format!(
r##"
const p = page.waitForEvent("filechooser", 10000);
await page.click("#b");
const chooser = await p;
const bytes = [104, 101, 108, 108, 111]; // 'hello'
await chooser.setFiles({{ name: "greeting.txt", mimeType: "text/plain", buffer: bytes }});
// The page-side change handler awaits `f.text()`; poll for the
// title from the page context so the browser's real `setTimeout`
// is used (see WAIT_FOR_TITLE_CALL docstring).
const title = {wait};
return {{ title }};
"##,
wait = WAIT_FOR_TITLE_CALL.replace("PREFIX_JSON", "\"name=\"").trim(),
);
let v = c.script_value(&script);
let title = v["title"].as_str().unwrap_or("");
assert!(
title.contains("name=greeting.txt"),
"page saw the declared FilePayload name: {v}"
);
assert!(title.contains("size=5"), "page saw the payload byte length: {v}");
assert!(
title.contains("text=hello"),
"page decoded the payload bytes back to the original string via `await f.text()`: {v}"
);
}
pub fn test_file_chooser_unclaimed_disposes(c: &mut McpClient) {
if c.backend == "webkit" || c.backend == "bidi" {
return;
}
let html = SINGLE_FORM_HTML.to_string();
c.nav_url(&format!("data:text/html,{}", urlencoding(&html)));
let script = r##"
// No waitForEvent. Click the button; the intercept suppresses
// the native picker and our listener disposes the captured
// element behind the scenes. The click should resolve promptly
// without hanging.
const started = Date.now();
await page.click("#b");
return { elapsed_ms: Date.now() - started };
"##;
let v = c.script_value(script);
let elapsed = v["elapsed_ms"].as_u64().unwrap_or(u64::MAX);
assert!(
elapsed < 2000,
"click with no filechooser listener should not hang (elapsed={}ms): {v}",
elapsed
);
}
fn urlencoding(s: &str) -> String {
s.replace(' ', "%20").replace('#', "%23").replace('"', "%22")
}