#![allow(
clippy::too_many_lines,
clippy::uninlined_format_args,
clippy::single_char_pattern,
clippy::redundant_closure_for_method_calls,
clippy::get_first
)]
use ferridriver::chromium;
use ferridriver::options::*;
async fn eval_str(page: &std::sync::Arc<ferridriver::Page>, expr: &str) -> ferridriver::Result<String> {
let v = page
.evaluate(expr, ferridriver::protocol::SerializedArgument::default(), None)
.await?;
Ok(v.as_string_lossy())
}
async fn eval_json(
page: &std::sync::Arc<ferridriver::Page>,
expr: &str,
) -> ferridriver::Result<Option<serde_json::Value>> {
let v = page
.evaluate(expr, ferridriver::protocol::SerializedArgument::default(), None)
.await?;
Ok(v.to_json_like())
}
fn data_url(html: &str) -> String {
format!(
"data:text/html,{}",
html
.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
(b as char).to_string()
},
_ => format!("%{:02X}", b),
})
.collect::<String>()
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn page_api_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.goto(&data_url("<title>Hello</title><body>World</body>"), None)
.await
.unwrap();
let title = page.title().await.unwrap();
assert!(title.contains("Hello"), "title: {title}");
let url = page.url();
assert!(url.starts_with("data:"), "url: {url}");
let val = eval_json(&page, "1 + 2").await.unwrap();
assert_eq!(val, Some(serde_json::json!(3)));
let s = eval_str(&page, "'hello'").await.unwrap();
assert!(s.contains("hello"), "evaluate_str: {s}");
page
.goto(
&data_url("<button id='b' onclick=\"this.textContent='clicked'\">Go</button>"),
None,
)
.await
.unwrap();
page.locator("#b", None).click(None).await.unwrap();
let t = eval_str(&page, "document.getElementById('b').textContent")
.await
.unwrap();
assert!(t.contains("clicked"), "locator click: {t}");
page.goto(&data_url("<input id='i' type='text'>"), None).await.unwrap();
page.locator("#i", None).fill("hello", None).await.unwrap();
let v = page.locator("#i", None).input_value().await.unwrap();
assert!(v.contains("hello"), "fill + input_value: {v}");
page
.goto(&data_url("<button>Save</button><button>Cancel</button>"), None)
.await
.unwrap();
let count = page
.get_by_role("button", &RoleOptions::default())
.count()
.await
.unwrap();
assert_eq!(count, 2, "get_by_role count");
page
.goto(&data_url("<p>Hello World</p><p>Goodbye</p>"), None)
.await
.unwrap();
let count = page
.get_by_text(&"Hello".into(), &TextOptions::default())
.count()
.await
.unwrap();
assert_eq!(count, 1, "get_by_text count");
page
.goto(
&data_url("<label for='e'>Email</label><input id='e' type='email'>"),
None,
)
.await
.unwrap();
page
.get_by_label(&"Email".into(), &TextOptions::default())
.fill("a@b.com", None)
.await
.unwrap();
let v = eval_str(&page, "document.getElementById('e').value").await.unwrap();
assert!(v.contains("a@b.com"), "get_by_label fill: {v}");
page
.goto(&data_url("<div data-testid='card'>Content</div>"), None)
.await
.unwrap();
let t = page.get_by_test_id(&"card".into()).text_content().await.unwrap();
assert!(t.unwrap_or_default().contains("Content"), "get_by_test_id");
page
.goto(
&data_url("<div class='a'><span>Inside A</span></div><div class='b'><span>Inside B</span></div>"),
None,
)
.await
.unwrap();
let t = page
.locator("css=.a", None)
.locator("css=span", None)
.text_content()
.await
.unwrap();
assert!(t.unwrap_or_default().contains("Inside A"), "chain");
page
.goto(&data_url("<ul><li>A</li><li>B</li><li>C</li></ul>"), None)
.await
.unwrap();
let first = page.locator("css=li", None).first().text_content().await.unwrap();
assert!(first.unwrap_or_default().contains("A"), "first");
let last = page.locator("css=li", None).last().text_content().await.unwrap();
assert!(last.unwrap_or_default().contains("C"), "last");
let second = page.locator("css=li", None).nth(1).text_content().await.unwrap();
assert!(second.unwrap_or_default().contains("B"), "nth(1)");
page
.goto(
&data_url("<div id='v'>visible</div><div id='h' style='display:none'>hidden</div>"),
None,
)
.await
.unwrap();
assert!(page.locator("#v", None).is_visible().await.unwrap(), "visible");
assert!(page.locator("#h", None).is_hidden().await.unwrap(), "hidden");
page
.goto(&data_url("<input id='e'><input id='d' disabled>"), None)
.await
.unwrap();
assert!(page.locator("#e", None).is_enabled().await.unwrap(), "enabled");
assert!(page.locator("#d", None).is_disabled().await.unwrap(), "disabled");
page
.goto(&data_url("<input type='checkbox' id='c'>"), None)
.await
.unwrap();
assert!(!page.locator("#c", None).is_checked().await.unwrap(), "unchecked");
page.locator("#c", None).check(None).await.unwrap();
assert!(page.locator("#c", None).is_checked().await.unwrap(), "checked");
page.locator("#c", None).uncheck(None).await.unwrap();
assert!(!page.locator("#c", None).is_checked().await.unwrap(), "unchecked again");
page
.goto(&data_url("<input id='e'><input id='d' disabled>"), None)
.await
.unwrap();
assert!(page.locator("#e", None).is_editable().await.unwrap(), "editable");
assert!(
!page.locator("#d", None).is_editable().await.unwrap(),
"disabled not editable"
);
page
.goto(&data_url("<div id='d'><b>Bold</b> text</div>"), None)
.await
.unwrap();
let inner = page.locator("#d", None).inner_html().await.unwrap();
assert!(inner.contains("<b>"), "innerHTML: {inner}");
let text = page.locator("#d", None).inner_text().await.unwrap();
assert!(text.contains("Bold"), "innerText: {text}");
page
.goto(&data_url("<h1>Title</h1><p>Body text</p>"), None)
.await
.unwrap();
let html = page.content().await.unwrap();
assert!(html.contains("Title"), "content");
let md = page.markdown().await.unwrap();
assert!(md.contains("# Title"), "markdown heading: {md}");
page
.goto(&data_url("<ul><li>Alpha</li><li>Beta</li><li>Gamma</li></ul>"), None)
.await
.unwrap();
let texts = page.locator("css=li", None).all_text_contents().await.unwrap();
assert_eq!(texts.len(), 3);
assert!(texts[0].contains("Alpha"));
assert!(texts[2].contains("Gamma"));
let count = page.locator("css=li", None).count().await.unwrap();
assert_eq!(count, 3, "count");
page.goto(&data_url("<div id='d'></div><script>setTimeout(function(){document.getElementById('d').innerHTML='<span id=\"s\">loaded</span>'},200)</script>"), None).await.unwrap();
page
.wait_for_selector(
"#s",
WaitOptions {
timeout: Some(5000),
..Default::default()
},
)
.await
.unwrap();
page
.goto(
&data_url("<script>setTimeout(function(){window.ready=true},200)</script>"),
None,
)
.await
.unwrap();
let val = page.wait_for_function("window.ready", Some(5000)).await.unwrap();
assert_eq!(val, serde_json::json!(true));
page.goto(&data_url("<h1>Screenshot</h1>"), None).await.unwrap();
let bytes = page.screenshot(ScreenshotOptions::default()).await.unwrap();
assert!(bytes.len() > 100, "screenshot bytes");
assert_eq!(&bytes[0..4], &[0x89, 0x50, 0x4E, 0x47], "PNG magic");
page.goto(&data_url("<h1 id='h'>0</h1><button id='b' onclick=\"document.getElementById('h').textContent=Number(document.getElementById('h').textContent)+1\">+</button>"), None).await.unwrap();
page.locator("#b", None).dblclick(None).await.unwrap();
let t = eval_str(&page, "document.getElementById('h').textContent")
.await
.unwrap();
assert!(t.contains("2"), "dblclick: {t}");
page.goto(&data_url("<input id='i'>"), None).await.unwrap();
page.locator("#i", None).focus().await.unwrap();
let active = eval_str(&page, "document.activeElement?.id||''").await.unwrap();
assert!(active.contains("i"), "focus: {active}");
page.locator("#i", None).blur().await.unwrap();
let active = eval_str(&page, "document.activeElement?.tagName||''").await.unwrap();
assert!(!active.contains("INPUT"), "blur: {active}");
page
.goto(
&data_url("<select id='s'><option value='a'>Apple</option><option value='b'>Banana</option></select>"),
None,
)
.await
.unwrap();
page
.locator("#s", None)
.select_option(vec![SelectOptionValue::by_label("Banana")], None)
.await
.unwrap();
let v = eval_str(&page, "document.getElementById('s').value").await.unwrap();
assert!(v.contains("b"), "select_option: {v}");
let r = page.locator("#s", None).click(None).await;
assert!(r.is_err(), "clicking select should error");
page
.goto(&data_url("<div><p>Keep</p></div><div><p>Remove</p></div>"), None)
.await
.unwrap();
let count = page
.locator("css=div", None)
.filter(&FilterOptions {
has_text: Some("Keep".into()),
..Default::default()
})
.count()
.await
.unwrap();
assert_eq!(count, 1, "filter has_text");
page
.goto(
&data_url("<body><script>document.title=window.innerWidth+'x'+window.innerHeight</script></body>"),
None,
)
.await
.unwrap();
let initial = page.title().await.unwrap();
let parts: Vec<&str> = initial.split('x').collect();
let initial_w: i64 = parts[0].parse().unwrap_or(0);
let initial_h: i64 = parts[1].parse().unwrap_or(0);
assert!(initial_w > 0 && initial_h > 0, "initial viewport: {initial}");
page.set_viewport_size(1024, 768).await.unwrap();
page
.goto(
&data_url("<body><script>document.title=window.innerWidth+'x'+window.innerHeight</script></body>"),
None,
)
.await
.unwrap();
let resized = page.title().await.unwrap();
assert!(resized.contains("1024"), "viewport width should be 1024: {resized}");
assert!(resized.contains("768"), "viewport height should be 768: {resized}");
page.set_viewport_size(375, 812).await.unwrap();
page
.goto(
&data_url("<body><script>document.title=window.innerWidth+'x'+window.innerHeight</script></body>"),
None,
)
.await
.unwrap();
let mobile = page.title().await.unwrap();
assert!(mobile.contains("375"), "mobile width should be 375: {mobile}");
page.set_viewport_size(initial_w, initial_h).await.unwrap();
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn snapshot_for_ai_tests() {
use ferridriver::snapshot::SnapshotOptions;
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.goto(
&data_url("<h1>Hello World</h1><button>Submit</button><a href='#'>Link</a>"),
None,
)
.await
.unwrap();
let result = page.snapshot_for_ai(SnapshotOptions::default()).await.unwrap();
assert!(result.full.contains("### Page"), "should have page header");
assert!(
result.full.contains("heading"),
"should contain heading role: {}",
result.full
);
assert!(result.full.contains("Hello World"), "should contain heading text");
assert!(result.full.contains("button"), "should contain button role");
assert!(result.full.contains("Submit"), "should contain button text");
assert!(result.full.contains("link"), "should contain link role");
assert!(
result.incremental.is_none(),
"no incremental on first call without track"
);
assert!(!result.ref_map.is_empty(), "ref_map should have entries");
page
.goto(&data_url("<title>Test Title</title><body>Content</body>"), None)
.await
.unwrap();
let result = page.snapshot_for_ai(SnapshotOptions::default()).await.unwrap();
assert!(
result.full.contains("Title: Test Title"),
"should contain page title: {}",
result.full
);
assert!(result.full.contains("URL: data:"), "should contain URL");
page
.goto(
&data_url("<div><ul><li><a href='#'>Deep Link</a></li></ul></div>"),
None,
)
.await
.unwrap();
let deep = page
.snapshot_for_ai(SnapshotOptions {
depth: None,
..Default::default()
})
.await
.unwrap();
let shallow = page
.snapshot_for_ai(SnapshotOptions {
depth: Some(2),
..Default::default()
})
.await
.unwrap();
assert!(
deep.full.len() >= shallow.full.len(),
"unlimited depth ({}) should be >= depth=2 ({})",
deep.full.len(),
shallow.full.len()
);
page
.goto(&data_url("<h1>V1</h1><button>Click</button>"), None)
.await
.unwrap();
let r1 = page
.snapshot_for_ai(SnapshotOptions {
track: Some("t1".to_string()),
..Default::default()
})
.await
.unwrap();
assert!(r1.full.contains("V1"), "first call should have V1");
assert!(
r1.incremental.is_none(),
"first call with track should have no incremental"
);
page
.goto(&data_url("<h1>V2</h1><button>Click</button>"), None)
.await
.unwrap();
let r2 = page
.snapshot_for_ai(SnapshotOptions {
track: Some("t1".to_string()),
..Default::default()
})
.await
.unwrap();
assert!(r2.full.contains("V2"), "second call should have V2");
assert!(r2.incremental.is_some(), "should have incremental after change");
let inc = r2.incremental.unwrap();
assert!(inc.contains("V2"), "incremental should contain changed heading: {inc}");
let r3 = page
.snapshot_for_ai(SnapshotOptions {
track: Some("t1".to_string()),
..Default::default()
})
.await
.unwrap();
assert!(r3.incremental.is_none(), "no incremental when nothing changed");
page
.goto(&data_url("<button id='b1'>Save</button><a href='#'>Help</a>"), None)
.await
.unwrap();
let result = page.snapshot_for_ai(SnapshotOptions::default()).await.unwrap();
let has_refs = result.full.contains("[ref=");
assert!(has_refs, "snapshot should contain ref labels: {}", result.full);
for (ref_label, node_id) in &result.ref_map {
assert!(ref_label.starts_with('e'), "ref should start with 'e': {ref_label}");
assert!(*node_id > 0, "backend node ID should be positive: {node_id}");
}
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn add_init_script_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
let id = page
.add_init_script("window.__test_init = 'injected'".into(), None)
.await
.unwrap();
assert!(!id.is_empty(), "should return identifier");
page
.goto(
&data_url("<script>document.title = window.__test_init || 'missing'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(
title, "injected",
"init script should set window.__test_init before page script runs"
);
page
.goto(
&data_url("<script>document.title = window.__test_init || 'missing'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "injected", "init script should persist across navigations");
page
.add_init_script("window.__test_init2 = 'second'".into(), None)
.await
.unwrap();
page
.goto(
&data_url("<script>document.title = (window.__test_init || '') + ':' + (window.__test_init2 || '')</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "injected:second", "multiple init scripts should all run");
page.remove_init_script(&id).await.unwrap();
page
.goto(
&data_url("<script>document.title = (window.__test_init || 'gone') + ':' + (window.__test_init2 || '')</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "gone:second", "removed init script should no longer run");
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dialog_handling_tests() {
use ferridriver::events::PageEvent;
use std::sync::Arc;
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.goto(
&data_url("<script>alert('hello'); document.title = 'after_alert'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "after_alert", "alert should be auto-closed so script continues");
page
.goto(
&data_url("<script>var r = confirm('sure?'); document.title = r ? 'yes' : 'no'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "no", "confirm should be auto-dismissed when no listener");
page
.goto(
&data_url("<script>var r = prompt('name?', 'default'); document.title = r || 'null'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "null", "prompt should be auto-dismissed when no listener");
page.events().on(
"dialog",
Arc::new(|event: PageEvent| {
if let PageEvent::Dialog(dialog) = event {
tokio::spawn(async move {
let _ = dialog.accept(None).await;
});
}
}),
);
page
.goto(
&data_url("<script>var r = confirm('sure?'); document.title = r ? 'yes' : 'no'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "yes", "confirm should be accepted by listener");
page.events().remove_all_listeners();
page.events().on(
"dialog",
Arc::new(|event: PageEvent| {
if let PageEvent::Dialog(dialog) = event {
tokio::spawn(async move {
if dialog.dialog_type() == ferridriver::dialog::DialogType::Prompt {
let _ = dialog.accept(Some("custom_answer".into())).await;
} else {
let _ = dialog.accept(None).await;
}
});
}
}),
);
page
.goto(
&data_url("<script>var r = prompt('name?'); document.title = r || 'null'</script>"),
None,
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "custom_answer", "prompt should get custom answer from listener");
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn add_script_style_tag_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page.goto(&data_url("<body></body>"), None).await.unwrap();
page
.add_script_tag(None, Some("document.title = 'injected'"), None)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "injected", "inline script tag should execute");
page.goto(&data_url("<div id='box'>text</div>"), None).await.unwrap();
page.add_style_tag(None, Some("#box { color: red }")).await.unwrap();
let color = eval_str(&page, "getComputedStyle(document.getElementById('box')).color")
.await
.unwrap();
assert_eq!(color, "rgb(255, 0, 0)", "inline style tag should apply: {color}");
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn expose_function_tests() {
use std::sync::Arc;
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.expose_function(
"double",
Arc::new(|args| {
Box::pin(async move {
let x = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
serde_json::json!(x * 2.0)
})
}),
)
.await
.unwrap();
page.goto(&data_url("<body></body>"), None).await.unwrap();
let result = eval_str(
&page,
"(async () => { const r = await window.double(21); return String(r); })()",
)
.await
.unwrap();
assert_eq!(result, "42", "exposed function should return doubled value: {result}");
page
.expose_function(
"greet",
Arc::new(|args| {
Box::pin(async move {
let name = args.first().and_then(|v| v.as_str()).unwrap_or("world");
serde_json::json!(format!("Hello, {}!", name))
})
}),
)
.await
.unwrap();
let result = eval_str(&page, "(async () => { return await window.greet('Rust'); })()")
.await
.unwrap();
assert_eq!(result, "Hello, Rust!", "greet function should work: {result}");
page.goto(&data_url("<body></body>"), None).await.unwrap();
let result = eval_str(&page, "(async () => { return String(await window.double(5)); })()")
.await
.unwrap();
assert_eq!(
result, "10",
"exposed function should persist across navigations: {result}"
);
page
.expose_function(
"add",
Arc::new(|args| {
Box::pin(async move {
let a = args.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
let b = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0);
serde_json::json!(a + b)
})
}),
)
.await
.unwrap();
let result = eval_str(&page, "(async () => { return String(await window.add(3, 4)); })()")
.await
.unwrap();
assert_eq!(result, "7", "multi-arg function should work: {result}");
page.remove_exposed_function("double").await.unwrap();
let result = eval_str(&page, "typeof window.double").await.unwrap();
assert_eq!(result, "undefined", "removed function should be gone");
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn wait_for_load_state_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page.goto(&data_url("<body>content</body>"), None).await.unwrap();
page.wait_for_load_state(Some("load")).await.unwrap();
let state = eval_str(&page, "document.readyState").await.unwrap();
assert_eq!(state, "complete", "should be complete after load state");
page.goto(&data_url("<body>content</body>"), None).await.unwrap();
page.wait_for_load_state(Some("domcontentloaded")).await.unwrap();
let state = eval_str(&page, "document.readyState").await.unwrap();
assert!(
state == "interactive" || state == "complete",
"should be at least interactive after domcontentloaded: {state}"
);
page.goto(&data_url("<body>content</body>"), None).await.unwrap();
page.wait_for_load_state(None).await.unwrap();
let state = eval_str(&page, "document.readyState").await.unwrap();
assert_eq!(state, "complete");
page.goto(&data_url("<body>idle</body>"), None).await.unwrap();
page.wait_for_load_state(Some("networkidle")).await.unwrap();
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn locator_evaluate_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page.goto(&data_url("<ul><li class='item'>Alpha</li><li class='item'>Beta</li><li class='item'>Gamma</li></ul><h1 id='title'>Hello</h1>"), None).await.unwrap();
let tag = page
.locator("#title", None)
.evaluate(
"el => el.tagName",
ferridriver::protocol::SerializedArgument::default(),
None,
None,
)
.await
.unwrap()
.to_json_like();
assert_eq!(tag, Some(serde_json::json!("H1")));
let text = page
.locator("#title", None)
.evaluate(
"el => el.textContent",
ferridriver::protocol::SerializedArgument::default(),
None,
None,
)
.await
.unwrap()
.to_json_like();
assert_eq!(text, Some(serde_json::json!("Hello")));
let count = page
.locator("css=.item", None)
.evaluate_all(
"elements => elements.length",
ferridriver::protocol::SerializedArgument::default(),
None,
)
.await
.unwrap()
.to_json_like();
assert_eq!(count, Some(serde_json::json!(3)));
let texts = page
.locator("css=.item", None)
.evaluate_all(
"elements => elements.map(function(e){return e.textContent})",
ferridriver::protocol::SerializedArgument::default(),
None,
)
.await
.unwrap()
.to_json_like();
assert_eq!(texts, Some(serde_json::json!(["Alpha", "Beta", "Gamma"])));
let rect = page
.locator("#title", None)
.evaluate(
"el => ({w: el.offsetWidth, h: el.offsetHeight})",
ferridriver::protocol::SerializedArgument::default(),
None,
None,
)
.await
.unwrap()
.to_json_like();
assert!(rect.is_some());
let r = rect.unwrap();
assert!(
r.get("w").and_then(|v| v.as_f64()).unwrap_or(0.0) > 0.0,
"should have width"
);
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn locator_set_checked_tap_select_text() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.goto(
&data_url("<input id='cb' type='checkbox'><input id='inp' type='text' value='select me'>"),
None,
)
.await
.unwrap();
assert!(!page.locator("#cb", None).is_checked().await.unwrap());
page.locator("#cb", None).set_checked(true, None).await.unwrap();
assert!(
page.locator("#cb", None).is_checked().await.unwrap(),
"should be checked after set_checked(true)"
);
page.locator("#cb", None).set_checked(false, None).await.unwrap();
assert!(
!page.locator("#cb", None).is_checked().await.unwrap(),
"should be unchecked after set_checked(false)"
);
page.locator("#cb", None).set_checked(true, None).await.unwrap();
page.locator("#cb", None).set_checked(true, None).await.unwrap(); assert!(
page.locator("#cb", None).is_checked().await.unwrap(),
"double set_checked(true) should still be checked"
);
page.locator("#inp", None).select_text().await.unwrap();
let selected = eval_str(&page, "window.getSelection().toString()").await.unwrap();
assert_eq!(selected, "select me", "select_text should select all text in input");
page.goto(&data_url("<button id='btn'>tap me</button><script>var b=document.getElementById('btn');b.addEventListener('touchend',function(){this.textContent='tapped'});b.addEventListener('pointerup',function(e){if(e.pointerType==='touch')this.textContent='tapped'})</script>"), None).await.unwrap();
page.locator("#btn", None).tap(None).await.unwrap();
let text = page.locator("#btn", None).text_content().await.unwrap();
assert_eq!(
text.unwrap_or_default(),
"tapped",
"tap should fire touch/pointer events"
);
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn storage_state_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page.goto(&data_url("<body>storage</body>"), None).await.unwrap();
let state = page.storage_state().await.unwrap();
assert!(state.get("cookies").is_some(), "state should have cookies key");
assert!(state.get("cookies").unwrap().is_array(), "cookies should be an array");
assert!(state.get("origins").is_some(), "state should have origins key");
assert!(state.get("origins").unwrap().is_array(), "origins should be an array");
let manual_state = serde_json::json!({
"cookies": [
{"name": "test", "value": "val123", "domain": "localhost", "path": "/", "secure": false, "httpOnly": false}
],
"origins": []
});
page.set_storage_state(&manual_state).await.unwrap();
let state2 = page.storage_state().await.unwrap();
assert!(state2.get("cookies").unwrap().is_array());
assert!(state2.get("origins").unwrap().is_array());
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn page_close_is_closed_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.new_page_with_url("about:blank").await.unwrap();
assert!(!page.is_closed(), "new page should not be closed");
page.close(None).await.unwrap();
assert!(page.is_closed(), "page should be closed after close()");
page.close(None).await.unwrap();
assert!(page.is_closed());
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn locator_or_and_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.goto(
&data_url("<button id='a'>Alpha</button><span id='b'>Beta</span><div id='c'>Gamma</div>"),
None,
)
.await
.unwrap();
let combined = page.locator("#a", None).or(&page.locator("#b", None));
let count = combined.count().await.unwrap();
assert_eq!(count, 2, "or() should match both selectors: count={count}");
let text = combined.first().text_content().await.unwrap();
assert_eq!(text, Some("Alpha".into()), "first() of or() should be Alpha");
page
.goto(
&data_url("<p class='a b'>Both</p><p class='a'>A only</p><p class='b'>B only</p>"),
None,
)
.await
.unwrap();
let and_loc = page.locator("css=.a", None).and(&page.locator("css=.b", None));
let count = and_loc.count().await.unwrap();
assert_eq!(count, 1, "and() should match only the element with both classes");
let text = and_loc.text_content().await.unwrap();
assert_eq!(
text,
Some("Both".into()),
"and() should return the intersection element"
);
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn network_interception_tests() {
use ferridriver::route::FulfillResponse;
use std::sync::Arc;
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
page
.route(
ferridriver::UrlMatcher::glob("**/mock-page").unwrap(),
Arc::new(|route| {
route.fulfill(FulfillResponse {
status: 200,
body: b"<html><head><title>Mocked</title></head><body>This page is mocked</body></html>".to_vec(),
content_type: Some("text/html".into()),
..Default::default()
});
}),
)
.await
.unwrap();
page.goto("http://mock.test/mock-page", None).await.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "Mocked", "navigated page should show mocked content: {title}");
let body = eval_str(&page, "document.body.textContent").await.unwrap();
assert!(
body.contains("This page is mocked"),
"body should contain mocked text: {body}"
);
page
.route(
ferridriver::UrlMatcher::glob("**/api/data").unwrap(),
Arc::new(|route| {
route.fulfill(FulfillResponse {
status: 200,
body: br#"{"mocked":true,"value":42}"#.to_vec(),
content_type: Some("application/json".into()),
..Default::default()
});
}),
)
.await
.unwrap();
let result = eval_str(
&page,
"(async () => { const r = await fetch('/api/data'); return await r.text(); })()",
)
.await
.unwrap();
assert!(
result.contains("\"mocked\":true"),
"API should return mocked JSON: {result}"
);
page
.route(
ferridriver::UrlMatcher::glob("**/blocked").unwrap(),
Arc::new(|route| {
route.abort("blockedbyclient");
}),
)
.await
.unwrap();
let result = eval_str(
&page,
"(async () => { try { await fetch('/blocked'); return 'ok'; } catch(e) { return 'error:' + e.message; } })()",
)
.await
.unwrap();
assert!(result.starts_with("error:"), "blocked request should throw: {result}");
page
.unroute(&ferridriver::UrlMatcher::glob("**/api/data").unwrap())
.await
.unwrap();
browser.close(None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn quick_wins_tests() {
let browser = chromium()
.launch(LaunchOptions::default())
.await
.expect("launch browser");
let page = browser.page().await.expect("get page");
assert!(browser.is_connected(), "browser should be connected");
assert!(!browser.version().is_empty(), "version should not be empty");
let contexts = browser.contexts();
assert!(!contexts.is_empty(), "should have at least one context");
page.goto(&data_url("<div id='target' oncontextmenu=\"document.title='context_menu';return false\" style='padding:20px'>Right click me</div>"), None).await.unwrap();
page.locator("#target", None).right_click().await.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "context_menu", "right_click should fire contextmenu: {title}");
page.goto(&data_url("<div id='exists'>here</div>"), None).await.unwrap();
assert!(
page.locator("#exists", None).is_attached().await.unwrap(),
"existing element should be attached"
);
assert!(
!page.locator("#gone", None).is_attached().await.unwrap(),
"missing element should not be attached"
);
page
.goto(
&data_url("<title>Opts</title><body>content</body>"),
Some(GotoOptions {
wait_until: Some("domcontentloaded".into()),
timeout: Some(10000),
referer: None,
}),
)
.await
.unwrap();
let title = page.title().await.unwrap();
assert_eq!(title, "Opts", "goto_with_options should work");
let (w, h) = page.viewport_size().await.unwrap();
assert!(w > 0 && h > 0, "viewport should have positive dimensions: {w}x{h}");
let page2 = browser.new_page_with_url("about:blank").await.unwrap();
assert!(!page2.is_closed());
page2.close(None).await.unwrap();
assert!(page2.is_closed());
browser.close(None).await.unwrap();
assert!(!browser.is_connected(), "browser should be disconnected after close");
}