1use std::{
2 fmt::{self, Display},
3 num::NonZeroUsize,
4 sync::Arc,
5};
6
7use chrome_for_testing_manager::{
8 Channel, Chromedriver, ChromedriverRunConfig, DriverOutputListener, PortRequest, VersionRequest,
9};
10use rootcause::Report;
11use rootcause::prelude::ResultExt;
12use thirtyfour::{ChromeCapabilities, error::WebDriverResult};
13
14use crate::driver_output::{
15 DriverOutputCapture, DriverOutputConfig, attach_browser_driver_output,
16 attach_browser_driver_output_to_result, browser_driver_output_config_from_env,
17};
18use crate::env::env_flag_enabled;
19use crate::execution::{ChromeCapabilitiesSetup, browser_test_executions};
20use crate::pause::{self, PauseConfig, PauseDecision};
21use crate::scheduler::{
22 BrowserTestFailurePolicy, BrowserTestParallelism, run_test_executions_parallel,
23 run_test_executions_sequential,
24};
25use crate::{BrowserTestError, BrowserTests, BrowserTimeouts, ElementQueryWaitConfig};
26
27pub(crate) const DEFAULT_VISIBLE_ENV: &str = "BROWSER_TEST_VISIBLE";
28
29#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
31pub enum BrowserTestVisibility {
32 #[default]
34 Headless,
35
36 Visible,
38
39 FromEnvVar(String),
43
44 FromEnv,
46}
47
48impl BrowserTestVisibility {
49 #[must_use]
51 pub const fn headless() -> Self {
52 Self::Headless
53 }
54
55 #[must_use]
57 pub const fn visible() -> Self {
58 Self::Visible
59 }
60
61 #[must_use]
63 pub const fn from_env() -> Self {
64 Self::FromEnv
65 }
66
67 #[must_use]
69 pub fn from_env_var(env_var: impl Into<String>) -> Self {
70 Self::FromEnvVar(env_var.into())
71 }
72
73 fn is_visible(&self) -> bool {
74 match self {
75 Self::Headless => false,
76 Self::Visible => true,
77 Self::FromEnvVar(env_var) => env_flag_enabled(env_var),
78 Self::FromEnv => env_flag_enabled(DEFAULT_VISIBLE_ENV),
79 }
80 }
81}
82
83#[derive(Clone)]
85pub struct BrowserTestRunner {
86 channel: Channel,
87 visible: bool,
88 pause: Option<PauseConfig>,
89 hint: Option<String>,
90 parallelism: BrowserTestParallelism,
91 failure_policy: BrowserTestFailurePolicy,
92 webdriver_timeouts: Option<BrowserTimeouts>,
93 element_query_wait: Option<ElementQueryWaitConfig>,
94 chrome_capabilities_setups: Vec<Arc<ChromeCapabilitiesSetup>>,
95 browser_driver_output: BrowserDriverOutputSetting,
96}
97
98#[derive(Debug, Clone)]
99enum BrowserDriverOutputSetting {
100 Disabled,
101 TailLines(NonZeroUsize),
102}
103
104impl Default for BrowserTestRunner {
105 fn default() -> Self {
106 Self {
107 channel: Channel::Stable,
108 visible: false,
109 pause: None,
110 hint: None,
111 parallelism: BrowserTestParallelism::Sequential,
112 failure_policy: BrowserTestFailurePolicy::FailFast,
113 webdriver_timeouts: None,
114 element_query_wait: None,
115 chrome_capabilities_setups: Vec::new(),
116 browser_driver_output: BrowserDriverOutputSetting::Disabled,
117 }
118 }
119}
120
121impl fmt::Debug for BrowserTestRunner {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 f.debug_struct("BrowserTestRunner")
124 .field("channel", &self.channel)
125 .field("visible", &self.visible)
126 .field("pause", &self.pause)
127 .field("hint", &self.hint)
128 .field("parallelism", &self.parallelism)
129 .field("failure_policy", &self.failure_policy)
130 .field("webdriver_timeouts", &self.webdriver_timeouts)
131 .field("element_query_wait", &self.element_query_wait)
132 .field(
133 "chrome_capabilities_setup_count",
134 &self.chrome_capabilities_setups.len(),
135 )
136 .field("browser_driver_output", &self.browser_driver_output)
137 .finish()
138 }
139}
140
141impl BrowserTestRunner {
142 #[must_use]
144 pub fn new() -> Self {
145 Self::default()
146 }
147
148 #[must_use]
150 pub fn with_channel(mut self, channel: Channel) -> Self {
151 self.channel = channel;
152 self
153 }
154
155 #[must_use]
157 pub fn with_visibility(mut self, visibility: impl Into<BrowserTestVisibility>) -> Self {
158 self.visible = visibility.into().is_visible();
159 self
160 }
161
162 #[must_use]
168 pub fn with_pause(mut self, pause: impl Into<PauseConfig>) -> Self {
169 self.pause = Some(pause.into());
170 self
171 }
172
173 #[must_use]
175 pub fn with_hint(mut self, hint: impl Display) -> Self {
176 self.hint = Some(hint.to_string());
177 self
178 }
179
180 #[must_use]
186 pub fn with_chrome_capabilities(
187 mut self,
188 setup: impl Fn(&mut ChromeCapabilities) -> WebDriverResult<()> + Send + Sync + 'static,
189 ) -> Self {
190 self.chrome_capabilities_setups.push(Arc::new(setup));
191 self
192 }
193
194 #[must_use]
199 pub fn with_timeouts(mut self, timeouts: BrowserTimeouts) -> Self {
200 self.webdriver_timeouts = Some(timeouts);
201 self
202 }
203
204 #[deprecated(since = "0.1.0", note = "use with_timeouts instead")]
208 #[must_use]
209 pub fn with_webdriver_timeouts(self, timeouts: BrowserTimeouts) -> Self {
210 self.with_timeouts(timeouts)
211 }
212
213 #[must_use]
218 pub const fn with_element_query_wait(mut self, wait: ElementQueryWaitConfig) -> Self {
219 self.element_query_wait = Some(wait);
220 self
221 }
222
223 #[must_use]
225 pub fn with_test_parallelism(mut self, parallelism: impl Into<BrowserTestParallelism>) -> Self {
226 self.parallelism = parallelism.into();
227 self
228 }
229
230 #[must_use]
232 pub fn with_failure_policy(
233 mut self,
234 failure_policy: impl Into<BrowserTestFailurePolicy>,
235 ) -> Self {
236 self.failure_policy = failure_policy.into();
237 self
238 }
239
240 #[must_use]
245 pub fn with_driver_output(mut self, config: impl Into<DriverOutputConfig>) -> Self {
246 self.browser_driver_output = browser_driver_output_setting(config.into());
247 self
248 }
249
250 #[deprecated(since = "0.1.0", note = "use with_driver_output instead")]
254 #[must_use]
255 pub fn with_browser_driver_output(self, config: impl Into<DriverOutputConfig>) -> Self {
256 self.with_driver_output(config)
257 }
258
259 pub async fn run<Context, TestError>(
282 &self,
283 context: &Context,
284 tests: BrowserTests<Context, TestError>,
285 ) -> Result<(), Report<BrowserTestError>>
286 where
287 Context: Sync + ?Sized,
288 TestError: ?Sized + 'static,
289 {
290 if tests.is_empty() {
291 tracing::info!("Skipping browser test run because no tests were provided.");
292 return Ok(());
293 }
294
295 if let Some(pause) = self.pause.clone()
296 && pause::pause_if_requested(pause, self.hint.as_deref()).await? == PauseDecision::Abort
297 {
298 tracing::info!("Browser test run aborted at manual pause.");
299 return Ok(());
300 }
301
302 tracing::info!("Starting webdriver...");
303 let browser_driver_output = self.browser_driver_output_capture_for_run();
304 let output_listener: Option<DriverOutputListener> = browser_driver_output
305 .as_ref()
306 .map(DriverOutputCapture::listener);
307 let chromedriver = match Chromedriver::run(
308 ChromedriverRunConfig::builder()
309 .version(VersionRequest::LatestIn(self.channel.clone()))
310 .port(PortRequest::Any)
311 .output_listener_opt(output_listener)
312 .build(),
313 )
314 .await
315 .context(BrowserTestError::StartWebdriver)
316 {
317 Ok(chromedriver) => chromedriver,
318 Err(mut err) => {
319 attach_browser_driver_output(&mut err, browser_driver_output.as_ref());
320 return Err(err);
321 }
322 };
323
324 let test_result = self.run_tests(&chromedriver, context, tests).await;
325
326 let termination_result = chromedriver
327 .terminate()
328 .await
329 .context(BrowserTestError::TerminateWebdriver);
330
331 if let Err(err) = termination_result {
332 return attach_browser_driver_output_to_result(
333 merge_termination_result(test_result, err),
334 browser_driver_output.as_ref(),
335 );
336 }
337
338 attach_browser_driver_output_to_result(test_result, browser_driver_output.as_ref())
339 }
340
341 async fn run_tests<Context, TestError>(
343 &self,
344 chromedriver: &Chromedriver,
345 context: &Context,
346 tests: BrowserTests<Context, TestError>,
347 ) -> Result<(), Report<BrowserTestError>>
348 where
349 Context: Sync + ?Sized,
350 TestError: ?Sized + 'static,
351 {
352 let max_parallel_tests = self.parallelism.max_parallel_tests();
353 let executions = browser_test_executions(
354 chromedriver,
355 self.visible,
356 self.webdriver_timeouts.as_ref(),
357 self.element_query_wait.as_ref(),
358 &self.chrome_capabilities_setups,
359 context,
360 tests,
361 );
362
363 if max_parallel_tests.get() == 1 {
364 run_test_executions_sequential(self.failure_policy, executions).await
365 } else {
366 run_test_executions_parallel(self.failure_policy, executions, max_parallel_tests).await
367 }
368 }
369
370 fn browser_driver_output_capture_for_run(&self) -> Option<DriverOutputCapture> {
371 match &self.browser_driver_output {
372 BrowserDriverOutputSetting::Disabled => None,
373 BrowserDriverOutputSetting::TailLines(tail_lines) => {
374 Some(DriverOutputCapture::new(*tail_lines))
375 }
376 }
377 }
378}
379
380fn browser_driver_output_setting(config: DriverOutputConfig) -> BrowserDriverOutputSetting {
381 match config {
382 DriverOutputConfig::Disabled => BrowserDriverOutputSetting::Disabled,
383 DriverOutputConfig::TailLines(tail_lines) => NonZeroUsize::new(tail_lines).map_or(
384 BrowserDriverOutputSetting::Disabled,
385 BrowserDriverOutputSetting::TailLines,
386 ),
387 DriverOutputConfig::FromEnv => {
388 browser_driver_output_setting(browser_driver_output_config_from_env())
389 }
390 }
391}
392
393fn merge_termination_result(
394 test_result: Result<(), Report<BrowserTestError>>,
395 termination_error: Report<BrowserTestError>,
396) -> Result<(), Report<BrowserTestError>> {
397 let Err(mut test_error) = test_result else {
398 return Err(termination_error);
399 };
400
401 tracing::error!(
402 "Failed to terminate chromedriver after browser test failure: {termination_error:?}"
403 );
404
405 test_error
406 .children_mut()
407 .push(termination_error.into_dynamic().into_cloneable());
408 Err(test_error)
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_ENV;
415 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES;
416 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV;
417 use crate::test_support::EnvVarGuard;
418 use assertr::prelude::*;
419 use chrome_for_testing_manager::{DriverOutputLine, DriverOutputSource};
420 use std::env;
421 use std::time::Duration;
422 use thirtyfour::ChromiumLikeCapabilities;
423
424 #[test]
425 fn runner_defaults_to_sequential_fail_fast_execution() {
426 let runner = BrowserTestRunner::new();
427
428 assert_that!(runner.parallelism).is_equal_to(BrowserTestParallelism::Sequential);
429 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::FailFast);
430 }
431
432 #[test]
433 fn runner_parallelism_builders_set_scheduling_mode() {
434 let max_parallel_tests =
435 NonZeroUsize::new(3).expect("literal parallelism should be non-zero");
436
437 let runner = BrowserTestRunner::new()
438 .with_test_parallelism(BrowserTestParallelism::Parallel(max_parallel_tests));
439 assert_that!(runner.parallelism)
440 .is_equal_to(BrowserTestParallelism::Parallel(max_parallel_tests));
441
442 let runner = runner.with_test_parallelism(BrowserTestParallelism::Sequential);
443 assert_that!(runner.parallelism).is_equal_to(BrowserTestParallelism::Sequential);
444 }
445
446 #[test]
447 fn runner_failure_policy_builders_set_failure_mode() {
448 let runner = BrowserTestRunner::new().with_failure_policy(BrowserTestFailurePolicy::RunAll);
449 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::RunAll);
450
451 let runner = runner.with_failure_policy(BrowserTestFailurePolicy::FailFast);
452 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::FailFast);
453 }
454
455 #[test]
456 fn runner_visibility_builder_sets_visible_mode() {
457 let runner = BrowserTestRunner::new().with_visibility(BrowserTestVisibility::Visible);
458 assert_that!(runner.visible).is_true();
459
460 let runner = runner.with_visibility(BrowserTestVisibility::Headless);
461 assert_that!(runner.visible).is_false();
462 }
463
464 #[test]
465 fn runner_visibility_builder_reads_default_env() {
466 let env = EnvVarGuard::new(DEFAULT_VISIBLE_ENV);
467 env.set("yes");
468
469 let runner = BrowserTestRunner::new().with_visibility(BrowserTestVisibility::from_env());
470
471 assert_that!(runner.visible).is_true();
472 }
473
474 #[test]
475 fn runner_browser_driver_output_builder_sets_capture() {
476 let runner =
477 BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(12));
478
479 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
480 panic!("browser driver output tail-line capture should be configured");
481 };
482 assert_that!(tail_lines.get()).is_equal_to(12);
483 }
484
485 #[allow(deprecated)]
486 #[test]
487 fn deprecated_browser_driver_output_builder_sets_capture() {
488 let runner = BrowserTestRunner::new()
489 .with_browser_driver_output(crate::BrowserDriverOutputConfig::new(12));
490
491 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
492 panic!("browser driver output tail-line capture should be configured");
493 };
494 assert_that!(tail_lines.get()).is_equal_to(12);
495 }
496
497 #[test]
498 fn runner_browser_driver_output_zero_tail_disables_capture() {
499 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(0));
500
501 assert_that!(matches!(
502 runner.browser_driver_output,
503 BrowserDriverOutputSetting::Disabled
504 ))
505 .is_true();
506 }
507
508 #[test]
509 fn browser_driver_output_from_env_uses_default_tail_lines() {
510 let env = EnvVarGuard::new(DEFAULT_BROWSER_DRIVER_OUTPUT_ENV);
511 let original_tail = env::var_os(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV);
512 env.set("1");
513 unsafe {
515 env::remove_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV);
516 }
517
518 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::from_env());
519
520 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
521 panic!("env browser driver output tail-line capture should be configured");
522 };
523 assert_that!(tail_lines.get()).is_equal_to(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES);
524
525 unsafe {
527 match original_tail {
528 Some(value) => env::set_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV, value),
529 None => env::remove_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV),
530 }
531 }
532 }
533
534 #[test]
535 fn browser_driver_output_tail_lines_creates_fresh_capture_per_run() {
536 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(1));
537
538 let first = runner
539 .browser_driver_output_capture_for_run()
540 .expect("tail-line capture should be enabled");
541 let second = runner
542 .browser_driver_output_capture_for_run()
543 .expect("tail-line capture should be enabled");
544
545 first.push(DriverOutputLine {
546 source: DriverOutputSource::Stdout,
547 sequence: 0,
548 line: "first run".to_owned(),
549 });
550
551 assert_that!(first.snapshot().total_lines).is_equal_to(1);
552 assert_that!(second.snapshot().total_lines).is_equal_to(0);
553 }
554
555 #[test]
556 fn browser_driver_output_disabled_creates_no_capture_for_run() {
557 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::disabled());
558
559 assert_that!(runner.browser_driver_output_capture_for_run().is_none()).is_true();
560 }
561
562 #[test]
563 fn runner_chrome_capabilities_builder_adds_setup() {
564 let runner =
565 BrowserTestRunner::new().with_chrome_capabilities(|caps| caps.add_arg("--no-sandbox"));
566
567 assert_that!(runner.chrome_capabilities_setups.len()).is_equal_to(1);
568 }
569
570 #[test]
571 fn runner_webdriver_timeouts_builder_sets_default_timeouts() {
572 let timeouts = BrowserTimeouts::builder()
573 .script_timeout(Duration::from_secs(10))
574 .page_load_timeout(Duration::from_secs(10))
575 .implicit_wait_timeout(Duration::from_secs(0))
576 .build();
577
578 let runner = BrowserTestRunner::new().with_timeouts(timeouts);
579
580 assert_that!(runner.webdriver_timeouts).is_equal_to(Some(timeouts));
581 }
582
583 #[allow(deprecated)]
584 #[test]
585 fn deprecated_webdriver_timeouts_builder_sets_default_timeouts() {
586 let timeouts = BrowserTimeouts::builder()
587 .script_timeout(Duration::from_secs(10))
588 .page_load_timeout(Duration::from_secs(10))
589 .implicit_wait_timeout(Duration::from_secs(0))
590 .build();
591
592 let runner = BrowserTestRunner::new().with_webdriver_timeouts(timeouts);
593
594 assert_that!(runner.webdriver_timeouts).is_equal_to(Some(timeouts));
595 }
596
597 #[test]
598 fn runner_element_query_wait_builder_sets_default_wait() {
599 let wait = ElementQueryWaitConfig::builder()
600 .timeout(Duration::from_secs(10))
601 .interval(Duration::from_millis(500))
602 .build();
603
604 let runner = BrowserTestRunner::new().with_element_query_wait(wait);
605
606 assert_that!(runner.element_query_wait).is_equal_to(Some(wait));
607 }
608
609 #[test]
610 fn runner_with_no_tests_returns_without_starting_webdriver_or_pausing() {
611 let runtime = tokio::runtime::Builder::new_current_thread()
612 .build()
613 .expect("current-thread runtime should build");
614
615 runtime.block_on(async {
616 BrowserTestRunner::new()
617 .with_pause(PauseConfig::enabled(true))
618 .run(&(), BrowserTests::<()>::new())
619 .await
620 .expect("empty test runs should be a no-op");
621 });
622 }
623
624 #[test]
625 fn termination_failure_is_attached_to_existing_test_failure() {
626 let test_result = Err(Report::new(BrowserTestError::RunTest {
627 test_name: "login".to_owned(),
628 }));
629 let termination_error = Report::new(BrowserTestError::TerminateWebdriver);
630
631 let err = merge_termination_result(test_result, termination_error)
632 .expect_err("test and termination failure should fail");
633
634 assert_that!(err.to_string()).contains(
635 BrowserTestError::RunTest {
636 test_name: "login".to_owned(),
637 }
638 .to_string(),
639 );
640 assert_that!(err.children().len()).is_equal_to(1);
641 }
642}