Skip to main content

ferridriver_test/
lib.rs

1#![allow(
2  clippy::missing_errors_doc,
3  clippy::missing_panics_doc,
4  clippy::must_use_candidate,
5  clippy::must_use_unit,
6  clippy::return_self_not_must_use,
7  clippy::doc_markdown,
8  clippy::doc_link_with_quotes,
9  clippy::module_name_repetitions,
10  clippy::cast_possible_truncation,
11  clippy::cast_precision_loss,
12  clippy::redundant_closure_for_method_calls,
13  clippy::implicit_clone,
14  clippy::struct_excessive_bools,
15  clippy::large_enum_variant,
16  clippy::needless_raw_string_hashes,
17  clippy::should_implement_trait,
18  clippy::match_same_arms,
19  clippy::uninlined_format_args,
20  clippy::single_char_pattern,
21  clippy::unused_self,
22  clippy::unused_async,
23  clippy::bool_to_int_with_if,
24  clippy::manual_let_else,
25  clippy::too_many_lines,
26  clippy::impl_trait_in_params,
27  clippy::needless_pass_by_value,
28  clippy::match_wildcard_for_single_variants,
29  clippy::manual_string_new,
30  clippy::format_push_string,
31  clippy::trivially_copy_pass_by_ref,
32  clippy::unnecessary_wraps,
33  clippy::default_trait_access,
34  clippy::wildcard_imports,
35  clippy::items_after_statements,
36  clippy::field_reassign_with_default,
37  clippy::map_unwrap_or,
38  clippy::iter_on_single_items,
39  clippy::similar_names,
40  clippy::semicolon_if_nothing_returned,
41  clippy::inconsistent_struct_constructor,
42  clippy::derivable_impls,
43  clippy::used_underscore_items,
44  clippy::explicit_iter_loop,
45  clippy::iter_on_empty_collections,
46  clippy::wrong_self_convention,
47  clippy::unnecessary_sort_by,
48  clippy::iter_over_hash_type,
49  clippy::manual_assert,
50  clippy::explicit_deref_methods,
51  clippy::option_if_let_else,
52  clippy::match_bool,
53  clippy::ref_option,
54  clippy::needless_lifetimes,
55  clippy::type_complexity,
56  clippy::expect_used,
57  clippy::duration_subsec,
58  clippy::verbose_file_reads,
59  clippy::if_not_else,
60  clippy::implicit_hasher,
61  clippy::stable_sort_primitive
62)]
63//! ferridriver-test -- High-performance E2E test runner for browser automation.
64//!
65//! Provides a Playwright Test-compatible API for writing and running browser tests
66//! with automatic fixture injection, parallel execution, and rich reporting.
67//!
68//! # Quick Start (Rust)
69//!
70//! ```ignore
71//! use ferridriver_test::prelude::*;
72//!
73//! #[ferritest]
74//! async fn basic_navigation(ctx: TestContext) {
75//!     let page = ctx.page().await?;
76//!     page.goto("https://example.com", None).await?;
77//!     expect(&*page).to_have_title("Example Domain").await?;
78//! }
79//!
80//! #[ferritest(retries = 2, tag = "smoke")]
81//! async fn login_test(ctx: TestContext) {
82//!     let page = ctx.page().await?;
83//!     page.goto("https://app.example.com/login", None).await?;
84//!     page.locator("#email", None).fill("user@example.com", None).await?;
85//!     page.locator("#password", None).fill("password", None).await?;
86//!     page.locator("button[type=submit]", None).click(None).await?;
87//!     expect(&*page).to_have_url("https://app.example.com/dashboard").await?;
88//! }
89//! ```
90
91/// Result type for `#[fixture]` bodies and other test helpers: a
92/// `std::result::Result` whose error is [`TestFailure`].
93pub type Result<T> = std::result::Result<T, model::TestFailure>;
94
95// -- Core modules --
96pub mod config;
97pub mod context;
98pub mod ct;
99pub mod discovery;
100pub mod dispatcher;
101pub mod expect;
102pub mod fixture;
103pub mod git_info;
104pub mod interactive;
105pub mod logging;
106pub mod model;
107pub mod reporter;
108pub mod retry;
109pub mod runner;
110pub mod server;
111pub mod shard;
112pub mod snapshot;
113pub mod tracing;
114pub mod tui;
115pub mod tui_reporter;
116pub mod watch;
117pub mod worker;
118
119// -- Re-exports --
120pub use config::{CliOverrides, TestConfig, parse_common_cli_args};
121pub use context::TestContext;
122pub use discovery::{
123  FixtureRegistration, HookKindTag, HookRegistration as InventoryHookRegistration, SuiteModeRegistration,
124  TestRegistration, collect_rust_fixtures,
125};
126pub use expect::{ToPassOptions, expect, expect_configured, expect_poll, to_pass, to_pass_with_options};
127pub use fixture::{FixtureDef, FixturePool, FixtureScope};
128pub use model::{
129  HookDef, HookKind, HookOwner, HookPhase, HookRegistration, HookScope, SuiteDef, SuiteMode, TestAnnotation, TestCase,
130  TestFailure, TestFixtures, TestFn, TestId, TestInfo, TestModifiers, TestOutcome, TestPlan, TestPlanBuilder,
131  TestStatus, TestStep,
132};
133pub use reporter::{EventBus, EventBusBuilder, Reporter, ReporterDriver, ReporterEvent, ReporterSet, Subscription};
134pub use runner::TestRunner;
135
136// Re-export proc macros.
137pub use ferridriver_test_macros::{
138  after_all, after_each, before_all, before_each, ferritest, ferritest_each, ferritest_suite, fixture,
139};
140
141// Re-export inventory for the proc macro expansion.
142pub use inventory;
143
144/// Run all `#[ferritest]` tests in this binary.
145///
146/// Reads config from `ferridriver.config.toml` (auto-discovered),
147/// applies CLI args (`-- --headed --backend webkit --workers 1`),
148/// and runs all registered tests through the parallel runner.
149///
150/// ```ignore
151/// use ferridriver_test::prelude::*;
152///
153/// #[ferritest]
154/// async fn my_test(ctx: TestContext) {
155///     let page = ctx.page().await?;
156///     page.goto("https://example.com", None).await?;
157/// }
158///
159/// ferridriver_test::main!();
160/// ```
161#[macro_export]
162macro_rules! main {
163  () => {
164    fn main() {
165      $crate::run_harness();
166    }
167  };
168}
169
170/// Entry point called by `main!()`. Parses CLI args, loads config,
171/// discovers tests, and runs them.
172pub fn run_harness() {
173  logging::init_from_env();
174
175  let rt = tokio::runtime::Builder::new_multi_thread()
176    .enable_all()
177    .build()
178    .expect("failed to build tokio runtime");
179
180  let exit_code = rt.block_on(async {
181    let overrides = config::parse_common_cli_args();
182    let config = config::resolve_config(&overrides).unwrap_or_else(|e| {
183      eprintln!("config error: {e}");
184      std::process::exit(1);
185    });
186    let plan = discovery::collect_rust_tests(&config);
187    let mut runner = runner::TestRunner::new(config, overrides);
188    Box::pin(runner.run(plan)).await
189  });
190
191  // Drain any still-running tasks so that child processes spawned via
192  // `tokio::process::Command::kill_on_drop(true)` actually get their `Drop`
193  // impls run — `std::process::exit` below would otherwise abort the process
194  // without destructors, leaving browser zombies.
195  rt.shutdown_timeout(std::time::Duration::from_secs(5));
196
197  std::process::exit(exit_code);
198}
199
200/// Prelude for convenient imports in test files.
201pub mod prelude {
202  pub use ferridriver::{Browser, ContextRef as BrowserContext, Locator, Page};
203
204  pub use crate::context::TestContext;
205  pub use crate::expect::{expect, expect_configured, expect_poll, to_pass};
206  pub use crate::fixture::FixturePool;
207  pub use crate::model::{TestFailure, TestInfo};
208  pub use ferridriver_test_macros::{
209    after_all, after_each, before_all, before_each, ferritest, ferritest_each, ferritest_suite, fixture,
210  };
211}