#![cfg(feature = "integration-tests")]
#![allow(clippy::panic, clippy::unwrap_used)]
use std::time::Duration;
use serial_test::serial;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use zendriver::{Browser, Cookie, SameSite};
async fn fixture_with_html(html: &str) -> MockServer {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200).set_body_raw(html.as_bytes().to_vec(), "text/html"),
)
.mount(&mock)
.await;
mock
}
#[tokio::test]
#[serial]
async fn new_tab_opens_second_tab() {
let browser = Browser::builder().headless(true).launch().await.unwrap();
assert_eq!(browser.tab_count().await, 1);
let _tab2 = browser.new_tab().await.unwrap();
assert_eq!(
browser.tabs().await.len(),
2,
"new_tab should add a second entry to the tab registry"
);
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn tab_close_removes_from_registry() {
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab2 = browser.new_tab().await.unwrap();
assert_eq!(browser.tab_count().await, 2);
tab2.close().await.unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
if browser.tab_count().await == 1 {
break;
}
if std::time::Instant::now() >= deadline {
panic!("tab.close did not remove tab from registry within 5s");
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn cookies_set_and_all_roundtrip() {
let browser = Browser::builder().headless(true).launch().await.unwrap();
let jar = browser.cookies();
jar.set(Cookie {
name: "alpha".into(),
value: "1".into(),
domain: "example.test".into(),
path: "/".into(),
expires: None,
http_only: false,
secure: false,
same_site: Some(SameSite::Lax),
url: None,
..Default::default()
})
.await
.unwrap();
jar.set(Cookie {
name: "beta".into(),
value: "2".into(),
domain: "example.test".into(),
path: "/".into(),
expires: None,
http_only: true,
secure: false,
same_site: None,
url: None,
..Default::default()
})
.await
.unwrap();
let all = jar.all().await.unwrap();
let names: std::collections::HashSet<_> = all.iter().map(|c| c.name.clone()).collect();
assert!(names.contains("alpha"), "alpha cookie missing from all()");
assert!(names.contains("beta"), "beta cookie missing from all()");
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn cookies_save_and_load_roundtrip() {
let browser = Browser::builder().headless(true).launch().await.unwrap();
let jar = browser.cookies();
jar.set(Cookie {
name: "saved_a".into(),
value: "v1".into(),
domain: "example.test".into(),
path: "/".into(),
expires: None,
http_only: false,
secure: false,
same_site: Some(SameSite::Lax),
url: None,
..Default::default()
})
.await
.unwrap();
jar.set(Cookie {
name: "saved_b".into(),
value: "v2".into(),
domain: "example.test".into(),
path: "/api".into(),
expires: None,
http_only: true,
secure: false,
same_site: None,
url: None,
..Default::default()
})
.await
.unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().to_path_buf();
jar.save_to_file(&path).await.unwrap();
jar.clear().await.unwrap();
let after_clear = jar.all().await.unwrap();
assert!(
!after_clear.iter().any(|c| c.name == "saved_a"),
"clear() should have removed saved_a"
);
jar.load_from_file(&path).await.unwrap();
let reloaded = jar.all().await.unwrap();
let names: std::collections::HashSet<_> = reloaded.iter().map(|c| c.name.clone()).collect();
assert!(
names.contains("saved_a"),
"saved_a missing after load_from_file"
);
assert!(
names.contains("saved_b"),
"saved_b missing after load_from_file"
);
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn local_storage_set_get_clear() {
let mock =
fixture_with_html(r#"<!doctype html><html><body><div id="d">x</div></body></html>"#).await;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
let storage = tab.local_storage();
storage.set("theme", "dark").await.unwrap();
storage.set("lang", "en").await.unwrap();
let theme = storage.get("theme").await.unwrap();
assert_eq!(theme.as_deref(), Some("dark"));
let lang = storage.get("lang").await.unwrap();
assert_eq!(lang.as_deref(), Some("en"));
storage.clear().await.unwrap();
let all = storage.get_all().await.unwrap();
assert!(all.is_empty(), "storage should be empty after clear()");
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn back_forward_navigation_history() {
let mock_a =
fixture_with_html(r#"<!doctype html><html><body><div id="a">A</div></body></html>"#).await;
let mock_b =
fixture_with_html(r#"<!doctype html><html><body><div id="b">B</div></body></html>"#).await;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock_a.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
tab.goto(&mock_b.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
tab.back().await.unwrap();
tab.wait_for_load().await.unwrap();
let url_after_back = tab.url().await.unwrap().to_string();
assert!(
url_after_back.starts_with(&mock_a.uri()),
"back should land on A, got: {url_after_back}"
);
tab.forward().await.unwrap();
tab.wait_for_load().await.unwrap();
let url_after_fwd = tab.url().await.unwrap().to_string();
assert!(
url_after_fwd.starts_with(&mock_b.uri()),
"forward should land on B, got: {url_after_fwd}"
);
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn reload_dispatches_page_reload() {
let mock =
fixture_with_html(r#"<!doctype html><html><body><div id="d">x</div></body></html>"#).await;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
tab.evaluate_main::<serde_json::Value>("window.sentinel = 'pre-reload'; null")
.await
.unwrap();
let before: String = tab.evaluate_main("window.sentinel").await.unwrap();
assert_eq!(before, "pre-reload");
tab.reload().await.unwrap();
tab.wait_for_load().await.unwrap();
let after: Option<String> = tab
.evaluate_main("typeof window.sentinel === 'undefined' ? null : window.sentinel")
.await
.unwrap();
assert_eq!(after, None, "reload should have cleared window.sentinel");
let id: Option<String> = tab
.find()
.css("#d")
.one()
.await
.unwrap()
.attr("id")
.await
.unwrap();
assert_eq!(id.as_deref(), Some("d"));
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn wait_for_idle_on_spa_with_delayed_xhr() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/data"))
.respond_with(ResponseTemplate::new(200).set_body_string("ok"))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/"))
.respond_with(
ResponseTemplate::new(200).set_body_raw(
r#"<!doctype html><html><body>
<script>
setTimeout(() => {
fetch("/data").then(r => r.text()).then(t => { window.x = t; });
}, 500);
</script>
</body></html>"#
.as_bytes()
.to_vec(),
"text/html",
),
)
.mount(&mock)
.await;
let mock_page = mock;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock_page.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
let start = std::time::Instant::now();
tab.wait_for_idle().await.unwrap();
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(800),
"wait_for_idle returned too early ({elapsed:?}); should wait for delayed XHR + quiet window"
);
let x: String = tab.evaluate_main("window.x || ''").await.unwrap();
assert_eq!(x, "ok", "XHR result should be present once idle resolves");
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn frame_find_inside_iframe() {
let mock = fixture_with_html(
r#"<!doctype html><html><body>
<iframe id="f" srcdoc="<button id='b'>x</button>"></iframe>
</body></html>"#,
)
.await;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let child = loop {
let frames = tab.frames().await.unwrap();
if let Some(child) = frames.into_iter().find(|f| !f.is_main()) {
break child;
}
if std::time::Instant::now() >= deadline {
panic!("iframe never registered as a child frame within 5s");
}
tokio::time::sleep(Duration::from_millis(50)).await;
};
let btn = child.find().css("#b").one().await.unwrap();
let id: Option<String> = btn.attr("id").await.unwrap();
assert_eq!(id.as_deref(), Some("b"));
browser.close().await.unwrap();
}
#[tokio::test]
#[serial]
async fn traversal_refresh_survives_reload() {
let mock = fixture_with_html(
r#"<!doctype html><html><body>
<div id="root"><span id="child">x</span></div>
</body></html>"#,
)
.await;
let browser = Browser::builder().headless(true).launch().await.unwrap();
let tab = browser.main_tab();
tab.goto(&mock.uri()).await.unwrap();
tab.wait_for_load().await.unwrap();
let child = tab.find().css("#child").one().await.unwrap();
tab.evaluate_main::<serde_json::Value>("location.reload(); null")
.await
.ok(); tab.wait_for_load().await.unwrap();
let parent = child.parent().await.unwrap().expect("#child has a parent");
let parent_id: Option<String> = parent.attr("id").await.unwrap();
assert_eq!(
parent_id.as_deref(),
Some("root"),
"traversal parent should still resolve to #root after reload"
);
browser.close().await.unwrap();
}