use std::sync::Arc;
use std::time::Duration;
use ferridriver_test::config::{CliOverrides, TestConfig};
use ferridriver_test::model::*;
use ferridriver_test::runner::TestRunner;
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!("%{b:02X}"),
})
.collect::<String>()
)
}
fn make_navigation_test() -> TestCase {
TestCase {
id: TestId {
file: "runner_e2e.rs".into(),
suite: Some("navigation".into()),
name: "basic_navigation".into(),
line: None,
},
test_fn: Arc::new(|pool| {
Box::pin(async move {
let page: Arc<ferridriver::Page> = pool.get("page").await.map_err(TestFailure::from)?;
let url = data_url("<title>Test Page</title><body><h1>Hello World</h1></body>");
page.goto(&url, None).await.map_err(|e| TestFailure {
message: format!("goto failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
let title = page.title().await.map_err(|e| TestFailure {
message: format!("title failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
if !title.contains("Test Page") {
return Err(TestFailure {
message: format!("expected title to contain 'Test Page', got '{title}'"),
stack: None,
diff: Some(format!("- expected: \"Test Page\"\n+ received: \"{title}\"")),
screenshot: None,
});
}
Ok(())
})
}),
fixture_requests: vec!["page".into()],
annotations: Vec::new(),
timeout: Some(Duration::from_secs(15)),
retries: None,
expected_status: ExpectedStatus::Pass,
use_options: None,
}
}
fn make_click_test() -> TestCase {
TestCase {
id: TestId {
file: "runner_e2e.rs".into(),
suite: Some("interaction".into()),
name: "click_button".into(),
line: None,
},
test_fn: Arc::new(|pool| {
Box::pin(async move {
let page: Arc<ferridriver::Page> = pool.get("page").await.map_err(TestFailure::from)?;
let url = data_url("<button id='btn' onclick=\"this.textContent='clicked'\">Click Me</button>");
page.goto(&url, None).await.map_err(|e| TestFailure {
message: format!("goto failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
page.locator("#btn", None).click(None).await.map_err(|e| TestFailure {
message: format!("click failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
let text = page
.locator("#btn", None)
.text_content()
.await
.map_err(|e| TestFailure {
message: format!("text_content failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?
.unwrap_or_default();
if text != "clicked" {
return Err(TestFailure {
message: format!("expected button text 'clicked', got '{text}'"),
stack: None,
diff: Some(format!("- expected: \"clicked\"\n+ received: \"{text}\"")),
screenshot: None,
});
}
Ok(())
})
}),
fixture_requests: vec!["page".into()],
annotations: Vec::new(),
timeout: Some(Duration::from_secs(15)),
retries: None,
expected_status: ExpectedStatus::Pass,
use_options: None,
}
}
fn make_fill_test() -> TestCase {
TestCase {
id: TestId {
file: "runner_e2e.rs".into(),
suite: Some("interaction".into()),
name: "fill_input".into(),
line: None,
},
test_fn: Arc::new(|pool| {
Box::pin(async move {
let page: Arc<ferridriver::Page> = pool.get("page").await.map_err(TestFailure::from)?;
let url = data_url("<input id='inp' type='text' />");
page.goto(&url, None).await.map_err(|e| TestFailure {
message: format!("goto failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
page
.locator("#inp", None)
.fill("hello world", None)
.await
.map_err(|e| TestFailure {
message: format!("fill failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
let val = page
.locator("#inp", None)
.input_value()
.await
.map_err(|e| TestFailure {
message: format!("input_value failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
if val != "hello world" {
return Err(TestFailure {
message: format!("expected input value 'hello world', got '{val}'"),
stack: None,
diff: None,
screenshot: None,
});
}
Ok(())
})
}),
fixture_requests: vec!["page".into()],
annotations: Vec::new(),
timeout: Some(Duration::from_secs(15)),
retries: None,
expected_status: ExpectedStatus::Pass,
use_options: None,
}
}
fn make_expect_test() -> TestCase {
TestCase {
id: TestId {
file: "runner_e2e.rs".into(),
suite: Some("expect".into()),
name: "auto_retry_assertions".into(),
line: None,
},
test_fn: Arc::new(|pool| {
Box::pin(async move {
let page: Arc<ferridriver::Page> = pool.get("page").await.map_err(TestFailure::from)?;
let url = data_url(
"<title>Expect Test</title>\
<div id='msg'>Initial</div>\
<button id='btn' onclick=\"setTimeout(() => document.getElementById('msg').textContent = 'Updated', 200)\">Go</button>",
);
page.goto(&url, None).await.map_err(|e| TestFailure {
message: format!("goto failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
ferridriver_test::expect::expect(&page)
.to_have_title("Expect Test")
.await?;
page.locator("#btn", None).click(None).await.map_err(|e| TestFailure {
message: format!("click failed: {e}"),
stack: None,
diff: None,
screenshot: None,
})?;
ferridriver_test::expect::expect(&page.locator("#msg", None))
.to_have_text("Updated")
.await?;
ferridriver_test::expect::expect(&page.locator("#msg", None))
.not()
.to_have_text("Initial")
.await?;
Ok(())
})
}),
fixture_requests: vec!["page".into()],
annotations: Vec::new(),
timeout: Some(Duration::from_secs(15)),
retries: None,
expected_status: ExpectedStatus::Pass,
use_options: None,
}
}
fn make_skip_test() -> TestCase {
TestCase {
id: TestId {
file: "runner_e2e.rs".into(),
suite: None,
name: "skipped_test".into(),
line: None,
},
test_fn: Arc::new(|_pool| {
Box::pin(async move {
Err(TestFailure {
message: "this should never run".into(),
stack: None,
diff: None,
screenshot: None,
})
})
}),
fixture_requests: vec![],
annotations: vec![TestAnnotation::Skip {
reason: Some("testing skip".into()),
condition: None,
}],
timeout: None,
retries: None,
expected_status: ExpectedStatus::Pass,
use_options: None,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_runner_e2e() {
let plan = TestPlan {
suites: vec![
TestSuite {
name: "navigation".into(),
file: "runner_e2e.rs".into(),
tests: vec![make_navigation_test()],
hooks: Hooks::default(),
annotations: Vec::new(),
mode: ferridriver_test::model::SuiteMode::default(),
},
TestSuite {
name: "interaction".into(),
file: "runner_e2e.rs".into(),
tests: vec![make_click_test(), make_fill_test()],
hooks: Hooks::default(),
annotations: Vec::new(),
mode: ferridriver_test::model::SuiteMode::default(),
},
TestSuite {
name: "expect".into(),
file: "runner_e2e.rs".into(),
tests: vec![make_expect_test()],
hooks: Hooks::default(),
annotations: Vec::new(),
mode: ferridriver_test::model::SuiteMode::default(),
},
TestSuite {
name: "skip".into(),
file: "runner_e2e.rs".into(),
tests: vec![make_skip_test()],
hooks: Hooks::default(),
annotations: Vec::new(),
mode: ferridriver_test::model::SuiteMode::default(),
},
],
total_tests: 5,
shard: None,
};
let config = TestConfig {
workers: 2,
timeout: 15_000,
expect_timeout: 5_000,
..Default::default()
};
let overrides = CliOverrides::default();
let mut runner = TestRunner::new(config, overrides);
let exit_code = runner.run(plan).await;
assert_eq!(exit_code, 0, "test runner should pass all tests (exit code 0)");
}