#![allow(
clippy::too_many_lines,
clippy::doc_markdown,
clippy::uninlined_format_args,
clippy::single_char_pattern,
clippy::cast_precision_loss,
clippy::unwrap_used,
clippy::expect_used,
clippy::needless_pass_by_value
)]
use super::client::McpClient;
use serde_json::json;
pub fn test_script_dblclick_options(c: &mut McpClient) {
c.nav(
"<button id='b'>b</button><div id='out'>no</div>\
<script>document.getElementById('b').addEventListener('dblclick',()=>document.getElementById('out').textContent='yes')</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick();\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
assert_eq!(v, json!("yes"), "dblclick() should fire ondblclick: {v}");
c.nav(
"<button id='b'>b</button><div id='out'>no</div>\
<script>document.getElementById('b').addEventListener('dblclick',e=>document.getElementById('out').textContent=e.shiftKey?'shift':'none')</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick({ modifiers: ['Shift'] });\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
assert_eq!(v, json!("shift"), "dblclick modifiers:['Shift'] sets shiftKey: {v}");
c.nav(
"<div id='b' style='width:200px;height:100px;background:#ccc'></div><div id='out'>none</div>\
<script>document.getElementById('b').addEventListener('dblclick',e=>{var r=e.currentTarget.getBoundingClientRect();document.getElementById('out').textContent=(Math.round(e.clientX-r.left))+','+(Math.round(e.clientY-r.top))})</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick({ position: { x: 15, y: 25 } });\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
assert_eq!(v, json!("15,25"), "dblclick position offsets the event coords: {v}");
c.nav(
"<button id='b'>b</button><div id='out'>0</div>\
<script>\
let downAt = 0;\
let gap = null;\
const b = document.getElementById('b');\
b.addEventListener('mousedown', () => { downAt = Date.now(); });\
b.addEventListener('mouseup', () => { \
if (gap === null) { gap = Date.now() - downAt; document.getElementById('out').textContent = String(gap); } \
});\
</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick({ delay: 120 });\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
let ms = v.as_str().and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
assert!(ms >= 80, "dblclick delay:120 held mousedown ≥ 80ms; got {ms} ({v})");
c.nav(
"<button id='b'>b</button><div id='dbl'>no</div><div id='kd'>none</div>\
<script>\
document.getElementById('b').addEventListener('dblclick',()=>document.getElementById('dbl').textContent='yes');\
document.addEventListener('keydown',e=>{if(e.key==='Shift')document.getElementById('kd').textContent='shift'});\
</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick({ trial: true, modifiers: ['Shift'] });\
return {\
dbl: await page.evaluate('document.getElementById(\"dbl\").textContent'),\
kd: await page.evaluate('document.getElementById(\"kd\").textContent'),\
};",
);
assert_eq!(v["dbl"], json!("no"), "dblclick trial:true skips dispatch: {v}");
assert_eq!(
v["kd"],
json!("shift"),
"dblclick trial:true still presses modifier: {v}"
);
c.nav(
"<button id='b' oncontextmenu='event.preventDefault()'>b</button><div id='count'>0</div><div id='btn'>-1</div>\
<script>\
const b = document.getElementById('b');\
const cnt = document.getElementById('count');\
const btn = document.getElementById('btn');\
b.addEventListener('contextmenu', e => { \
cnt.textContent = String(parseInt(cnt.textContent,10) + 1); \
btn.textContent = String(e.button); \
});\
</script>",
);
let v = c.script_value(
"await page.locator('#b').dblclick({ button: 'right' });\
return {\
count: await page.evaluate('document.getElementById(\"count\").textContent'),\
btn: await page.evaluate('document.getElementById(\"btn\").textContent'),\
};",
);
let count = v["count"].as_str().and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
assert!(count >= 1, "dblclick button:right should fire ≥ 1 contextmenu: {v}");
assert_eq!(v["btn"], json!("2"), "contextmenu should report button 2: {v}");
}
pub fn test_script_press_options(c: &mut McpClient) {
c.nav(
"<input id='i'><div id='out'>0</div>\
<script>\
let downAt = 0;\
const i = document.getElementById('i');\
i.addEventListener('keydown', () => { downAt = performance.now(); });\
i.addEventListener('keyup', () => { \
document.getElementById('out').textContent = String(Math.round(performance.now() - downAt)); \
});\
</script>",
);
let v = c.script_value(
"await page.locator('#i').click();\
await page.locator('#i').press('A', { delay: 120 });\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
let ms = v.as_str().and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
assert!(ms >= 80, "press delay:120 held key ≥ 80ms; got {ms} ({v})");
c.nav(
"<input id='i'><div id='out'>0</div>\
<script>\
let downAt = 0;\
const i = document.getElementById('i');\
i.addEventListener('keydown', () => { downAt = performance.now(); });\
i.addEventListener('keyup', () => { \
document.getElementById('out').textContent = String(Math.round(performance.now() - downAt)); \
});\
</script>",
);
let v = c.script_value(
"await page.locator('#i').click();\
await page.locator('#i').press('B');\
return await page.evaluate('document.getElementById(\"out\").textContent');",
);
let ms = v.as_str().and_then(|s| s.parse::<i64>().ok()).unwrap_or(0);
assert!(
ms < 80,
"press without delay should complete in <80ms; got {ms} ({v}) — suggests delay defaulted non-zero"
);
c.nav("<input id='i'>");
let v = c.script_value(
"await page.locator('#i').click();\
const t0 = Date.now();\
await page.locator('#i').press('C', { noWaitAfter: true });\
return Date.now() - t0;",
);
let elapsed = v.as_i64().unwrap_or(99_999);
assert!(
elapsed < 2_000,
"press noWaitAfter:true should return promptly; got {elapsed}ms"
);
}
pub fn test_script_type_options(c: &mut McpClient) {
c.nav(
"<input id='i'><div id='marks'>[]</div>\
<script>\
const marks = [];\
document.getElementById('i').addEventListener('keydown', () => { \
marks.push(performance.now()); \
document.getElementById('marks').textContent = JSON.stringify(marks); \
});\
</script>",
);
let v = c.script_value(
"await page.locator('#i').click();\
await page.locator('#i').type('abc', { delay: 50 });\
return await page.evaluate('document.getElementById(\"marks\").textContent');",
);
let raw = v.as_str().unwrap_or("[]");
let marks: Vec<f64> = serde_json::from_str(raw).unwrap_or_default();
assert_eq!(
marks.len(),
3,
"type('abc', delay:50) should fire 3 keydown events: {v}"
);
let g1 = marks[1] - marks[0];
let g2 = marks[2] - marks[1];
let min_gap = g1.min(g2);
assert!(
min_gap >= 35.0,
"type delay:50 should hold ≥ 35ms between keystrokes; got g1={g1}ms g2={g2}ms ({v})"
);
let after = c.script_value("return await page.inputValue('#i');");
assert_eq!(after, json!("abc"), "type('abc') should fill the input: {after}");
c.nav("<input id='i'>");
let v = c.script_value(
"await page.locator('#i').click();\
const t0 = Date.now();\
await page.locator('#i').type('xyz');\
return Date.now() - t0;",
);
let elapsed = v.as_i64().unwrap_or(99_999);
assert!(
elapsed < 1_000,
"type() without delay should complete in <1s; got {elapsed}ms — suggests delay defaulted non-zero"
);
let after = c.script_value("return await page.inputValue('#i');");
assert_eq!(after, json!("xyz"), "type('xyz') should fill the input: {after}");
}
pub fn test_script_set_input_files_polymorphism(c: &mut McpClient) {
let tmp_dir = std::env::temp_dir();
let path1 = tmp_dir.join(format!("ferridriver_opts_a_{}.txt", std::process::id()));
let path2 = tmp_dir.join(format!("ferridriver_opts_b_{}.txt", std::process::id()));
std::fs::write(&path1, b"alpha").unwrap();
std::fs::write(&path2, b"beta-beta").unwrap();
c.nav("<input type='file' id='f'>");
let v = c.script_value_with_args(
"await page.locator('#f').setInputFiles(args[0]); \
return { \
count: await page.evaluate(\"document.getElementById('f').files.length\"), \
name: await page.evaluate(\"document.getElementById('f').files[0].name\"), \
size: await page.evaluate(\"document.getElementById('f').files[0].size\"), \
};",
json!([path1.to_str().unwrap()]),
);
assert_eq!(v["count"], json!(1), "single path string: file count=1: {v}");
assert!(
v["name"]
.as_str()
.unwrap_or("")
.contains(&format!("ferridriver_opts_a_{}", std::process::id())),
"single path: name matches fixture: {v}"
);
assert_eq!(v["size"], json!(5), "single path: size==5 (alpha): {v}");
c.nav("<input type='file' id='f' multiple>");
let v = c.script_value_with_args(
"await page.locator('#f').setInputFiles([args[0], args[1]]); \
return { \
count: await page.evaluate(\"document.getElementById('f').files.length\"), \
n0: await page.evaluate(\"document.getElementById('f').files[0].name\"), \
n1: await page.evaluate(\"document.getElementById('f').files[1].name\"), \
s0: await page.evaluate(\"document.getElementById('f').files[0].size\"), \
s1: await page.evaluate(\"document.getElementById('f').files[1].size\"), \
};",
json!([path1.to_str().unwrap(), path2.to_str().unwrap()]),
);
assert_eq!(v["count"], json!(2), "path array: file count=2: {v}");
assert_eq!(v["s0"], json!(5), "path array: first size==5 (alpha): {v}");
assert_eq!(v["s1"], json!(9), "path array: second size==9 (beta-beta): {v}");
c.nav("<input type='file' id='f'>");
let payload_bytes: Vec<u8> = b"payload-body".to_vec();
let v = c.script_value_with_args(
"await page.locator('#f').setInputFiles(args[0]); \
return { \
count: await page.evaluate(\"document.getElementById('f').files.length\"), \
name: await page.evaluate(\"document.getElementById('f').files[0].name\"), \
type: await page.evaluate(\"document.getElementById('f').files[0].type\"), \
size: await page.evaluate(\"document.getElementById('f').files[0].size\"), \
};",
json!([{ "name": "payload.txt", "mimeType": "text/plain", "buffer": payload_bytes }]),
);
assert_eq!(v["count"], json!(1), "single payload: file count=1: {v}");
assert_eq!(v["name"], json!("payload.txt"), "single payload: name survives: {v}");
assert_eq!(v["type"], json!("text/plain"), "single payload: mimeType survives: {v}");
assert_eq!(
v["size"],
json!(payload_bytes.len()),
"single payload: byte count survives: {v}"
);
c.nav("<input type='file' id='f' multiple>");
let a_bytes: Vec<u8> = b"one".to_vec();
let b_bytes: Vec<u8> = b"twelvebytes!".to_vec();
let v = c.script_value_with_args(
"await page.locator('#f').setInputFiles(args[0]); \
return { \
count: await page.evaluate(\"document.getElementById('f').files.length\"), \
n0: await page.evaluate(\"document.getElementById('f').files[0].name\"), \
t0: await page.evaluate(\"document.getElementById('f').files[0].type\"), \
s0: await page.evaluate(\"document.getElementById('f').files[0].size\"), \
n1: await page.evaluate(\"document.getElementById('f').files[1].name\"), \
t1: await page.evaluate(\"document.getElementById('f').files[1].type\"), \
s1: await page.evaluate(\"document.getElementById('f').files[1].size\"), \
};",
json!([[
{ "name": "a.txt", "mimeType": "text/plain", "buffer": a_bytes },
{ "name": "b.json", "mimeType": "application/json", "buffer": b_bytes },
]]),
);
assert_eq!(v["count"], json!(2), "payload array: file count=2: {v}");
assert_eq!(v["n0"], json!("a.txt"), "payload array: first name: {v}");
assert_eq!(v["t0"], json!("text/plain"), "payload array: first type: {v}");
assert_eq!(v["s0"], json!(3), "payload array: first size==3: {v}");
assert_eq!(v["n1"], json!("b.json"), "payload array: second name: {v}");
assert_eq!(v["t1"], json!("application/json"), "payload array: second type: {v}");
assert_eq!(v["s1"], json!(12), "payload array: second size==12: {v}");
let _ = std::fs::remove_file(&path1);
let _ = std::fs::remove_file(&path2);
}