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>(
283 &self,
284 context: &Context,
285 tests: BrowserTests<Context, TestError>,
286 ) -> Result<(), Report<BrowserTestError>>
287 where
288 Context: Sync + ?Sized,
289 TestError: ?Sized + 'static,
290 {
291 if tests.is_empty() {
292 tracing::info!("Skipping browser test run because no tests were provided.");
293 return Ok(());
294 }
295
296 if let Some(pause) = self.pause.clone()
297 && pause::pause_if_requested(pause, self.hint.as_deref()).await? == PauseDecision::Abort
298 {
299 tracing::info!("Browser test run aborted at manual pause.");
300 return Ok(());
301 }
302
303 tracing::info!("Starting webdriver...");
304 let browser_driver_output = self.browser_driver_output_capture_for_run();
305 let output_listener: Option<DriverOutputListener> = browser_driver_output
306 .as_ref()
307 .map(DriverOutputCapture::listener);
308 let chromedriver = match Chromedriver::run(
309 ChromedriverRunConfig::builder()
310 .version(VersionRequest::LatestIn(self.channel.clone()))
311 .port(PortRequest::Any)
312 .output_listener_opt(output_listener)
313 .build(),
314 )
315 .await
316 .context(BrowserTestError::StartWebdriver)
317 {
318 Ok(chromedriver) => chromedriver,
319 Err(mut err) => {
320 attach_browser_driver_output(&mut err, browser_driver_output.as_ref());
321 return Err(err);
322 }
323 };
324
325 let test_result = self.run_tests(&chromedriver, context, tests).await;
326
327 let termination_result = chromedriver
328 .terminate()
329 .await
330 .context(BrowserTestError::TerminateWebdriver);
331
332 if let Err(err) = termination_result {
333 return attach_browser_driver_output_to_result(
334 merge_termination_result(test_result, err),
335 browser_driver_output.as_ref(),
336 );
337 }
338
339 attach_browser_driver_output_to_result(test_result, browser_driver_output.as_ref())
340 }
341
342 async fn run_tests<Context, TestError>(
344 &self,
345 chromedriver: &Chromedriver,
346 context: &Context,
347 tests: BrowserTests<Context, TestError>,
348 ) -> Result<(), Report<BrowserTestError>>
349 where
350 Context: Sync + ?Sized,
351 TestError: ?Sized + 'static,
352 {
353 let max_parallel_tests = self.parallelism.max_parallel_tests();
354 let executions = browser_test_executions(
355 chromedriver,
356 self.visible,
357 self.webdriver_timeouts.as_ref(),
358 self.element_query_wait.as_ref(),
359 &self.chrome_capabilities_setups,
360 context,
361 tests,
362 );
363
364 if max_parallel_tests.get() == 1 {
365 run_test_executions_sequential(self.failure_policy, executions).await
366 } else {
367 run_test_executions_parallel(self.failure_policy, executions, max_parallel_tests).await
368 }
369 }
370
371 fn browser_driver_output_capture_for_run(&self) -> Option<DriverOutputCapture> {
372 match &self.browser_driver_output {
373 BrowserDriverOutputSetting::Disabled => None,
374 BrowserDriverOutputSetting::TailLines(tail_lines) => {
375 Some(DriverOutputCapture::new(*tail_lines))
376 }
377 }
378 }
379}
380
381fn browser_driver_output_setting(config: DriverOutputConfig) -> BrowserDriverOutputSetting {
382 match config {
383 DriverOutputConfig::Disabled => BrowserDriverOutputSetting::Disabled,
384 DriverOutputConfig::TailLines(tail_lines) => NonZeroUsize::new(tail_lines).map_or(
385 BrowserDriverOutputSetting::Disabled,
386 BrowserDriverOutputSetting::TailLines,
387 ),
388 DriverOutputConfig::FromEnv => {
389 browser_driver_output_setting(browser_driver_output_config_from_env())
390 }
391 }
392}
393
394fn merge_termination_result(
395 test_result: Result<(), Report<BrowserTestError>>,
396 termination_error: Report<BrowserTestError>,
397) -> Result<(), Report<BrowserTestError>> {
398 let Err(mut test_error) = test_result else {
399 return Err(termination_error);
400 };
401
402 tracing::error!(
403 "Failed to terminate chromedriver after browser test failure: {termination_error:?}"
404 );
405
406 test_error
407 .children_mut()
408 .push(termination_error.into_dynamic().into_cloneable());
409 Err(test_error)
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_ENV;
416 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES;
417 use crate::driver_output::DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV;
418 use crate::test_support::EnvVarGuard;
419 use assertr::prelude::*;
420 use chrome_for_testing_manager::{DriverOutputLine, DriverOutputSource};
421 use std::env;
422 use std::time::Duration;
423 use thirtyfour::ChromiumLikeCapabilities;
424
425 #[test]
426 fn runner_defaults_to_sequential_fail_fast_execution() {
427 let runner = BrowserTestRunner::new();
428
429 assert_that!(runner.parallelism).is_equal_to(BrowserTestParallelism::Sequential);
430 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::FailFast);
431 }
432
433 #[test]
434 fn runner_parallelism_builders_set_scheduling_mode() {
435 let max_parallel_tests =
436 NonZeroUsize::new(3).expect("literal parallelism should be non-zero");
437
438 let runner = BrowserTestRunner::new()
439 .with_test_parallelism(BrowserTestParallelism::Parallel(max_parallel_tests));
440 assert_that!(runner.parallelism)
441 .is_equal_to(BrowserTestParallelism::Parallel(max_parallel_tests));
442
443 let runner = runner.with_test_parallelism(BrowserTestParallelism::Sequential);
444 assert_that!(runner.parallelism).is_equal_to(BrowserTestParallelism::Sequential);
445 }
446
447 #[test]
448 fn runner_failure_policy_builders_set_failure_mode() {
449 let runner = BrowserTestRunner::new().with_failure_policy(BrowserTestFailurePolicy::RunAll);
450 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::RunAll);
451
452 let runner = runner.with_failure_policy(BrowserTestFailurePolicy::FailFast);
453 assert_that!(runner.failure_policy).is_equal_to(BrowserTestFailurePolicy::FailFast);
454 }
455
456 #[test]
457 fn runner_visibility_builder_sets_visible_mode() {
458 let runner = BrowserTestRunner::new().with_visibility(BrowserTestVisibility::Visible);
459 assert_that!(runner.visible).is_true();
460
461 let runner = runner.with_visibility(BrowserTestVisibility::Headless);
462 assert_that!(runner.visible).is_false();
463 }
464
465 #[test]
466 fn runner_visibility_builder_reads_default_env() {
467 let env = EnvVarGuard::new(DEFAULT_VISIBLE_ENV);
468 env.set("yes");
469
470 let runner = BrowserTestRunner::new().with_visibility(BrowserTestVisibility::from_env());
471
472 assert_that!(runner.visible).is_true();
473 }
474
475 #[test]
476 fn runner_browser_driver_output_builder_sets_capture() {
477 let runner =
478 BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(12));
479
480 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
481 panic!("browser driver output tail-line capture should be configured");
482 };
483 assert_that!(tail_lines.get()).is_equal_to(12);
484 }
485
486 #[allow(deprecated)]
487 #[test]
488 fn deprecated_browser_driver_output_builder_sets_capture() {
489 let runner = BrowserTestRunner::new()
490 .with_browser_driver_output(crate::BrowserDriverOutputConfig::new(12));
491
492 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
493 panic!("browser driver output tail-line capture should be configured");
494 };
495 assert_that!(tail_lines.get()).is_equal_to(12);
496 }
497
498 #[test]
499 fn runner_browser_driver_output_zero_tail_disables_capture() {
500 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(0));
501
502 assert_that!(matches!(
503 runner.browser_driver_output,
504 BrowserDriverOutputSetting::Disabled
505 ))
506 .is_true();
507 }
508
509 #[test]
510 fn browser_driver_output_from_env_uses_default_tail_lines() {
511 let env = EnvVarGuard::new(DEFAULT_BROWSER_DRIVER_OUTPUT_ENV);
512 let original_tail = env::var_os(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV);
513 env.set("1");
514 unsafe {
516 env::remove_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV);
517 }
518
519 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::from_env());
520
521 let BrowserDriverOutputSetting::TailLines(tail_lines) = runner.browser_driver_output else {
522 panic!("env browser driver output tail-line capture should be configured");
523 };
524 assert_that!(tail_lines.get()).is_equal_to(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES);
525
526 unsafe {
528 match original_tail {
529 Some(value) => env::set_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV, value),
530 None => env::remove_var(DEFAULT_BROWSER_DRIVER_OUTPUT_TAIL_LINES_ENV),
531 }
532 }
533 }
534
535 #[test]
536 fn browser_driver_output_tail_lines_creates_fresh_capture_per_run() {
537 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::tail_lines(1));
538
539 let first = runner
540 .browser_driver_output_capture_for_run()
541 .expect("tail-line capture should be enabled");
542 let second = runner
543 .browser_driver_output_capture_for_run()
544 .expect("tail-line capture should be enabled");
545
546 first.push(DriverOutputLine {
547 source: DriverOutputSource::Stdout,
548 sequence: 0,
549 line: "first run".to_owned(),
550 });
551
552 assert_that!(first.snapshot().total_lines).is_equal_to(1);
553 assert_that!(second.snapshot().total_lines).is_equal_to(0);
554 }
555
556 #[test]
557 fn browser_driver_output_disabled_creates_no_capture_for_run() {
558 let runner = BrowserTestRunner::new().with_driver_output(DriverOutputConfig::disabled());
559
560 assert_that!(runner.browser_driver_output_capture_for_run().is_none()).is_true();
561 }
562
563 #[test]
564 fn runner_chrome_capabilities_builder_adds_setup() {
565 let runner =
566 BrowserTestRunner::new().with_chrome_capabilities(|caps| caps.add_arg("--no-sandbox"));
567
568 assert_that!(runner.chrome_capabilities_setups.len()).is_equal_to(1);
569 }
570
571 #[test]
572 fn runner_webdriver_timeouts_builder_sets_default_timeouts() {
573 let timeouts = BrowserTimeouts::builder()
574 .script_timeout(Duration::from_secs(10))
575 .page_load_timeout(Duration::from_secs(10))
576 .implicit_wait_timeout(Duration::from_secs(0))
577 .build();
578
579 let runner = BrowserTestRunner::new().with_timeouts(timeouts);
580
581 assert_that!(runner.webdriver_timeouts).is_equal_to(Some(timeouts));
582 }
583
584 #[allow(deprecated)]
585 #[test]
586 fn deprecated_webdriver_timeouts_builder_sets_default_timeouts() {
587 let timeouts = BrowserTimeouts::builder()
588 .script_timeout(Duration::from_secs(10))
589 .page_load_timeout(Duration::from_secs(10))
590 .implicit_wait_timeout(Duration::from_secs(0))
591 .build();
592
593 let runner = BrowserTestRunner::new().with_webdriver_timeouts(timeouts);
594
595 assert_that!(runner.webdriver_timeouts).is_equal_to(Some(timeouts));
596 }
597
598 #[test]
599 fn runner_element_query_wait_builder_sets_default_wait() {
600 let wait = ElementQueryWaitConfig::builder()
601 .timeout(Duration::from_secs(10))
602 .interval(Duration::from_millis(500))
603 .build();
604
605 let runner = BrowserTestRunner::new().with_element_query_wait(wait);
606
607 assert_that!(runner.element_query_wait).is_equal_to(Some(wait));
608 }
609
610 #[test]
611 fn runner_with_no_tests_returns_without_starting_webdriver_or_pausing() {
612 let runtime = tokio::runtime::Builder::new_current_thread()
613 .build()
614 .expect("current-thread runtime should build");
615
616 runtime.block_on(async {
617 BrowserTestRunner::new()
618 .with_pause(PauseConfig::enabled(true))
619 .run(&(), BrowserTests::<()>::new())
620 .await
621 .expect("empty test runs should be a no-op");
622 });
623 }
624
625 #[test]
626 fn termination_failure_is_attached_to_existing_test_failure() {
627 let test_result = Err(Report::new(BrowserTestError::RunTest {
628 test_name: "login".to_owned(),
629 }));
630 let termination_error = Report::new(BrowserTestError::TerminateWebdriver);
631
632 let err = merge_termination_result(test_result, termination_error)
633 .expect_err("test and termination failure should fail");
634
635 assert_that!(err.to_string()).contains(
636 BrowserTestError::RunTest {
637 test_name: "login".to_owned(),
638 }
639 .to_string(),
640 );
641 assert_that!(err.children().len()).is_equal_to(1);
642 }
643}