1use std::fmt;
5use std::future::Future;
6use std::path::PathBuf;
7use std::pin::Pin;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12use tokio::sync::Mutex;
13
14use crate::fixture::FixturePool;
15use crate::reporter::EventBus;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct TestId {
22 pub file: String,
23 pub suite: Option<String>,
24 pub name: String,
25 pub line: Option<usize>,
27}
28
29impl TestId {
30 #[must_use]
32 pub fn full_name(&self) -> String {
33 match &self.suite {
34 Some(s) => format!("{} > {} > {}", self.file, s, self.name),
35 None => format!("{} > {}", self.file, self.name),
36 }
37 }
38
39 #[must_use]
41 pub fn file_location(&self) -> String {
42 match self.line {
43 Some(line) => format!("{}:{}", self.file, line),
44 None => self.file.clone(),
45 }
46 }
47}
48
49impl fmt::Display for TestId {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 f.write_str(&self.full_name())
52 }
53}
54
55pub type TestFn =
60 Arc<dyn Fn(FixturePool) -> Pin<Box<dyn Future<Output = Result<(), TestFailure>> + Send>> + Send + Sync>;
61
62#[derive(Clone)]
66pub struct TestCase {
67 pub id: TestId,
68 pub test_fn: TestFn,
69 pub fixture_requests: Vec<String>,
71 pub annotations: Vec<TestAnnotation>,
73 pub timeout: Option<Duration>,
75 pub retries: Option<u32>,
77 pub expected_status: ExpectedStatus,
79 pub use_options: Option<serde_json::Value>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
87pub enum SuiteMode {
88 #[default]
90 Parallel,
91 Serial,
94}
95
96#[derive(Clone)]
98pub struct TestSuite {
99 pub name: String,
100 pub file: String,
101 pub tests: Vec<TestCase>,
102 pub hooks: Hooks,
103 pub annotations: Vec<TestAnnotation>,
105 pub mode: SuiteMode,
107}
108
109#[derive(Clone)]
111pub struct Hooks {
112 pub before_all: Vec<SuiteHookFn>,
114 pub after_all: Vec<SuiteHookFn>,
116 pub before_each: Vec<HookFn>,
118 pub after_each: Vec<HookFn>,
120}
121
122impl Default for Hooks {
123 fn default() -> Self {
124 Self {
125 before_all: Vec::new(),
126 after_all: Vec::new(),
127 before_each: Vec::new(),
128 after_each: Vec::new(),
129 }
130 }
131}
132
133pub type SuiteHookFn =
136 Arc<dyn Fn(FixturePool) -> Pin<Box<dyn Future<Output = Result<(), TestFailure>> + Send>> + Send + Sync>;
137
138pub type HookFn = Arc<
141 dyn Fn(FixturePool, Arc<TestInfo>) -> Pin<Box<dyn Future<Output = Result<(), TestFailure>> + Send>> + Send + Sync,
142>;
143
144#[derive(Clone, Default)]
150pub struct TestHooks {
151 pub global_setup_fns: Vec<SuiteHookFn>,
153 pub global_teardown_fns: Vec<SuiteHookFn>,
155}
156
157impl std::fmt::Debug for TestHooks {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct("TestHooks")
160 .field("global_setup_fns", &format!("[{} fn(s)]", self.global_setup_fns.len()))
161 .field(
162 "global_teardown_fns",
163 &format!("[{} fn(s)]", self.global_teardown_fns.len()),
164 )
165 .finish()
166 }
167}
168
169#[derive(Clone)]
173pub struct TestPlan {
174 pub suites: Vec<TestSuite>,
175 pub total_tests: usize,
177 pub shard: Option<ShardInfo>,
179}
180
181#[derive(Debug, Clone)]
182pub struct ShardInfo {
183 pub current: u32,
184 pub total: u32,
185}
186
187pub struct SuiteDef {
191 pub id: String,
193 pub name: String,
194 pub file: String,
195 pub mode: SuiteMode,
196}
197
198pub struct HookDef {
200 pub suite_id: String,
202 pub kind: HookKind,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum HookPhase {
208 Before,
209 After,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum HookScope {
215 Suite,
216 Scenario,
217 Step,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum HookOwner {
223 Root,
224 Suite(String),
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct HookRegistration {
230 pub phase: HookPhase,
231 pub scope: HookScope,
232 pub owner: HookOwner,
233 pub tags: Option<String>,
234 pub requested_fixtures: Vec<String>,
235}
236
237pub enum HookKind {
239 BeforeAll(SuiteHookFn),
240 AfterAll(SuiteHookFn),
241 BeforeEach(HookFn),
242 AfterEach(HookFn),
243}
244
245pub struct TestPlanBuilder {
252 tests: Vec<TestCase>,
253 suites: Vec<SuiteDef>,
254 hooks: Vec<HookDef>,
255}
256
257impl Default for TestPlanBuilder {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263impl TestPlanBuilder {
264 pub fn new() -> Self {
265 Self {
266 tests: Vec::new(),
267 suites: Vec::new(),
268 hooks: Vec::new(),
269 }
270 }
271
272 pub fn add_test(&mut self, test: TestCase) {
273 self.tests.push(test);
274 }
275
276 pub fn add_suite(&mut self, suite: SuiteDef) {
277 self.suites.push(suite);
278 }
279
280 pub fn add_hook(&mut self, hook: HookDef) {
281 self.hooks.push(hook);
282 }
283
284 pub fn build(self) -> TestPlan {
290 use rustc_hash::FxHashMap;
291
292 let suite_meta: FxHashMap<String, (String, String, SuiteMode)> = self
294 .suites
295 .into_iter()
296 .map(|s| (s.id, (s.name, s.file, s.mode)))
297 .collect();
298
299 let mut grouped: FxHashMap<String, Vec<TestCase>> = FxHashMap::default();
301 for tc in self.tests {
302 let key = tc.id.suite.clone().unwrap_or_default();
303 grouped.entry(key).or_default().push(tc);
304 }
305
306 let mut hook_map: FxHashMap<String, Hooks> = FxHashMap::default();
308 for h in self.hooks {
309 let hooks = hook_map.entry(h.suite_id).or_default();
310 match h.kind {
311 HookKind::BeforeAll(f) => hooks.before_all.push(f),
312 HookKind::AfterAll(f) => hooks.after_all.push(f),
313 HookKind::BeforeEach(f) => hooks.before_each.push(f),
314 HookKind::AfterEach(f) => hooks.after_each.push(f),
315 }
316 }
317
318 let mut plan_suites: Vec<TestSuite> = Vec::new();
320 let mut total = 0usize;
321
322 for (suite_key, tests) in grouped {
323 total += tests.len();
324 let (name, file, mode) = if suite_key.is_empty() {
325 ("tests".to_string(), String::new(), SuiteMode::Parallel)
326 } else if let Some((n, f, m)) = suite_meta.get(&suite_key) {
327 (n.clone(), f.clone(), *m)
328 } else {
329 (suite_key.clone(), String::new(), SuiteMode::Parallel)
331 };
332 let hooks = hook_map.remove(&suite_key).unwrap_or_default();
333 plan_suites.push(TestSuite {
334 name,
335 file,
336 tests,
337 hooks,
338 annotations: Vec::new(),
339 mode,
340 });
341 }
342
343 TestPlan {
344 suites: plan_suites,
345 total_tests: total,
346 shard: None,
347 }
348 }
349}
350
351#[derive(Clone)]
356pub struct TestInfo {
357 pub test_id: TestId,
359 pub title_path: Vec<String>,
361 pub retry: u32,
363 pub worker_index: u32,
365 pub parallel_index: u32,
367 pub repeat_each_index: u32,
369 pub output_dir: PathBuf,
371 pub snapshot_dir: PathBuf,
373 pub snapshot_path_template: Option<String>,
375 pub update_snapshots: crate::config::UpdateSnapshotsMode,
377 pub ignore_snapshots: bool,
380 pub attachments: Arc<Mutex<Vec<Attachment>>>,
382 pub steps: Arc<Mutex<Vec<TestStep>>>,
384 pub soft_errors: Arc<Mutex<Vec<TestFailure>>>,
386 pub errors: Arc<Mutex<Vec<TestFailure>>>,
391 pub snapshot_suffix: Arc<Mutex<String>>,
394 pub column: Option<u32>,
399 pub project: Option<crate::config::ProjectConfig>,
403 pub config_snapshot: Option<Arc<crate::config::TestConfig>>,
406 pub timeout: Duration,
408 pub tags: Vec<String>,
410 pub start_time: Instant,
412 pub event_bus: Option<EventBus>,
414 pub annotations: Arc<Mutex<Vec<TestAnnotation>>>,
416}
417
418impl TestInfo {
419 pub fn new_anonymous() -> Self {
421 Self {
422 test_id: TestId {
423 file: String::new(),
424 suite: None,
425 name: "anonymous".into(),
426 line: None,
427 },
428 title_path: Vec::new(),
429 retry: 0,
430 worker_index: 0,
431 parallel_index: 0,
432 repeat_each_index: 0,
433 output_dir: PathBuf::new(),
434 snapshot_dir: PathBuf::new(),
435 snapshot_path_template: None,
436 update_snapshots: crate::config::UpdateSnapshotsMode::default(),
437 ignore_snapshots: false,
438 attachments: Arc::new(Mutex::new(Vec::new())),
439 steps: Arc::new(Mutex::new(Vec::new())),
440 soft_errors: Arc::new(Mutex::new(Vec::new())),
441 errors: Arc::new(Mutex::new(Vec::new())),
442 snapshot_suffix: Arc::new(Mutex::new(String::new())),
443 column: None,
444 project: None,
445 config_snapshot: None,
446 timeout: Duration::from_secs(30),
447 tags: Vec::new(),
448 start_time: Instant::now(),
449 event_bus: None,
450 annotations: Arc::new(Mutex::new(Vec::new())),
451 }
452 }
453
454 pub async fn annotate(&self, type_name: impl Into<String>, description: impl Into<String>) {
456 let mut annotations = self.annotations.lock().await;
457 annotations.push(TestAnnotation::Info {
458 type_name: type_name.into(),
459 description: description.into(),
460 });
461 }
462
463 pub async fn get_annotations(&self) -> Vec<TestAnnotation> {
465 let annotations = self.annotations.lock().await;
466 annotations.clone()
467 }
468 pub async fn attach(&self, name: String, content_type: String, body: AttachmentBody) {
470 let mut attachments = self.attachments.lock().await;
471 attachments.push(Attachment {
472 name,
473 content_type,
474 body,
475 });
476 }
477
478 pub async fn add_soft_error(&self, error: TestFailure) {
480 let mut errors = self.soft_errors.lock().await;
481 errors.push(error);
482 }
483
484 pub async fn has_soft_errors(&self) -> bool {
486 let errors = self.soft_errors.lock().await;
487 !errors.is_empty()
488 }
489
490 pub async fn drain_soft_errors(&self) -> Vec<TestFailure> {
492 let mut errors = self.soft_errors.lock().await;
493 errors.drain(..).collect()
494 }
495
496 pub async fn push_step(&self, step: TestStep) {
498 let mut steps = self.steps.lock().await;
499 steps.push(step);
500 }
501
502 pub fn elapsed(&self) -> Duration {
504 self.start_time.elapsed()
505 }
506
507 pub async fn begin_step(&self, title: impl Into<String>, category: StepCategory) -> StepHandle {
512 let title = title.into();
513 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
514
515 if let Some(bus) = &self.event_bus {
516 bus.emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
517 crate::reporter::StepStartedEvent {
518 test_id: self.test_id.clone(),
519 step_id: step_id.clone(),
520 parent_step_id: None,
521 title: title.clone(),
522 category: category.clone(),
523 },
524 )));
525 }
526
527 StepHandle {
528 step_id,
529 test_id: self.test_id.clone(),
530 title,
531 category,
532 parent_step_id: None,
533 start: Instant::now(),
534 metadata: None,
535 event_bus: self.event_bus.clone(),
536 steps: Arc::clone(&self.steps),
537 }
538 }
539
540 pub async fn begin_child_step(
542 &self,
543 title: impl Into<String>,
544 category: StepCategory,
545 parent_step_id: &str,
546 ) -> StepHandle {
547 let title = title.into();
548 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
549
550 if let Some(bus) = &self.event_bus {
551 bus.emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
552 crate::reporter::StepStartedEvent {
553 test_id: self.test_id.clone(),
554 step_id: step_id.clone(),
555 parent_step_id: Some(parent_step_id.to_string()),
556 title: title.clone(),
557 category: category.clone(),
558 },
559 )));
560 }
561
562 StepHandle {
563 step_id,
564 test_id: self.test_id.clone(),
565 title,
566 category,
567 parent_step_id: Some(parent_step_id.to_string()),
568 start: Instant::now(),
569 metadata: None,
570 event_bus: self.event_bus.clone(),
571 steps: Arc::clone(&self.steps),
572 }
573 }
574
575 pub async fn record_step(
578 &self,
579 title: impl Into<String>,
580 category: StepCategory,
581 status: StepStatus,
582 duration: Duration,
583 error: Option<String>,
584 metadata: Option<serde_json::Value>,
585 ) {
586 let title = title.into();
587 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
588
589 if let Some(bus) = &self.event_bus {
590 bus.emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
591 crate::reporter::StepStartedEvent {
592 test_id: self.test_id.clone(),
593 step_id: step_id.clone(),
594 parent_step_id: None,
595 title: title.clone(),
596 category: category.clone(),
597 },
598 )));
599 bus.emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
600 crate::reporter::StepFinishedEvent {
601 test_id: self.test_id.clone(),
602 step_id: step_id.clone(),
603 title: title.clone(),
604 category: category.clone(),
605 duration,
606 error: error.clone(),
607 metadata: metadata.clone(),
608 },
609 )));
610 }
611
612 self.steps.lock().await.push(TestStep {
613 step_id,
614 title,
615 category,
616 duration,
617 status,
618 error,
619 location: None,
620 parent_step_id: None,
621 metadata,
622 steps: Vec::new(),
623 });
624 }
625}
626
627static STEP_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
629
630pub struct StepHandle {
636 pub step_id: String,
637 pub test_id: TestId,
638 pub title: String,
639 pub category: StepCategory,
640 pub parent_step_id: Option<String>,
641 pub start: Instant,
642 pub metadata: Option<serde_json::Value>,
644 event_bus: Option<EventBus>,
645 steps: Arc<Mutex<Vec<TestStep>>>,
646}
647
648impl StepHandle {
649 pub async fn end(self, error: Option<String>) {
651 let duration = self.start.elapsed();
652 let status = if error.is_some() {
653 StepStatus::Failed
654 } else {
655 StepStatus::Passed
656 };
657
658 if let Some(bus) = &self.event_bus {
660 bus.emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
661 crate::reporter::StepFinishedEvent {
662 test_id: self.test_id.clone(),
663 step_id: self.step_id.clone(),
664 title: self.title.clone(),
665 category: self.category.clone(),
666 duration,
667 error: error.clone(),
668 metadata: self.metadata.clone(),
669 },
670 )));
671 }
672
673 let step = TestStep {
675 step_id: self.step_id,
676 title: self.title,
677 category: self.category,
678 duration,
679 status,
680 error,
681 location: None,
682 parent_step_id: self.parent_step_id,
683 metadata: self.metadata.clone(),
684 steps: Vec::new(),
685 };
686 self.steps.lock().await.push(step);
687 }
688
689 pub async fn skip(self, reason: Option<String>) {
691 self.finish_with_status(StepStatus::Skipped, reason).await;
692 }
693
694 pub async fn pending(self, reason: Option<String>) {
696 self.finish_with_status(StepStatus::Pending, reason).await;
697 }
698
699 async fn finish_with_status(self, status: StepStatus, error: Option<String>) {
700 let duration = self.start.elapsed();
701
702 if let Some(bus) = &self.event_bus {
703 bus.emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
704 crate::reporter::StepFinishedEvent {
705 test_id: self.test_id.clone(),
706 step_id: self.step_id.clone(),
707 title: self.title.clone(),
708 category: self.category.clone(),
709 duration,
710 error: error.clone(),
711 metadata: self.metadata.clone(),
712 },
713 )));
714 }
715
716 let step = TestStep {
717 step_id: self.step_id,
718 title: self.title,
719 category: self.category,
720 duration,
721 status,
722 error,
723 location: None,
724 parent_step_id: self.parent_step_id,
725 metadata: self.metadata,
726 steps: Vec::new(),
727 };
728 self.steps.lock().await.push(step);
729 }
730}
731
732#[derive(Debug, Clone)]
736pub struct TestStep {
737 pub step_id: String,
739 pub title: String,
740 pub category: StepCategory,
741 pub duration: Duration,
742 pub status: StepStatus,
744 pub error: Option<String>,
745 pub location: Option<String>,
747 pub parent_step_id: Option<String>,
749 pub metadata: Option<serde_json::Value>,
752 pub steps: Vec<TestStep>,
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Eq)]
757pub enum StepStatus {
758 Passed,
759 Failed,
760 Skipped,
761 Pending,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq)]
767pub enum StepCategory {
768 TestStep,
770 Expect,
772 Fixture,
774 Hook,
776 PwApi,
778}
779
780impl StepCategory {
781 pub fn is_visible(&self) -> bool {
785 matches!(self, Self::TestStep | Self::Hook)
786 }
787}
788
789impl fmt::Display for StepCategory {
790 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
791 match self {
792 Self::TestStep => write!(f, "test.step"),
793 Self::Expect => write!(f, "expect"),
794 Self::Fixture => write!(f, "fixture"),
795 Self::Hook => write!(f, "hook"),
796 Self::PwApi => write!(f, "pw:api"),
797 }
798 }
799}
800
801#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
804#[serde(rename_all = "lowercase")]
805pub enum TestAnnotation {
806 Skip {
809 reason: Option<String>,
810 condition: Option<String>,
811 },
812 Slow {
815 reason: Option<String>,
816 condition: Option<String>,
817 },
818 Fixme {
821 reason: Option<String>,
822 condition: Option<String>,
823 },
824 Fail {
827 reason: Option<String>,
828 condition: Option<String>,
829 },
830 Only,
831 Tag(String),
832 Info {
834 type_name: String,
835 description: String,
836 },
837}
838
839#[derive(Debug, Clone, Default, PartialEq, Eq)]
840pub enum ExpectedStatus {
841 #[default]
842 Pass,
843 Fail,
844}
845
846pub struct TestModifiers {
855 pub skipped: AtomicBool,
857 pub skip_reason: std::sync::Mutex<Option<String>>,
859 pub expected_failure: AtomicBool,
861 pub slow: AtomicBool,
863 pub timeout_override: std::sync::Mutex<Option<u64>>,
865}
866
867impl Default for TestModifiers {
868 fn default() -> Self {
869 Self {
870 skipped: AtomicBool::new(false),
871 skip_reason: std::sync::Mutex::new(None),
872 expected_failure: AtomicBool::new(false),
873 slow: AtomicBool::new(false),
874 timeout_override: std::sync::Mutex::new(None),
875 }
876 }
877}
878
879impl std::fmt::Debug for TestModifiers {
880 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
881 f.debug_struct("TestModifiers")
882 .field("skipped", &self.skipped.load(Ordering::Relaxed))
883 .field("expected_failure", &self.expected_failure.load(Ordering::Relaxed))
884 .field("slow", &self.slow.load(Ordering::Relaxed))
885 .finish_non_exhaustive()
886 }
887}
888
889#[derive(Debug, Clone, PartialEq, Eq)]
893pub enum TestStatus {
894 Passed,
895 Failed,
896 TimedOut,
897 Skipped,
898 Flaky,
900 Interrupted,
902}
903
904impl fmt::Display for TestStatus {
905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906 match self {
907 Self::Passed => write!(f, "passed"),
908 Self::Failed => write!(f, "failed"),
909 Self::TimedOut => write!(f, "timed out"),
910 Self::Skipped => write!(f, "skipped"),
911 Self::Flaky => write!(f, "flaky"),
912 Self::Interrupted => write!(f, "interrupted"),
913 }
914 }
915}
916
917#[derive(Debug, Clone)]
919pub struct TestOutcome {
920 pub test_id: TestId,
921 pub status: TestStatus,
922 pub duration: Duration,
923 pub attempt: u32,
924 pub max_attempts: u32,
925 pub error: Option<TestFailure>,
926 pub attachments: Vec<Attachment>,
927 pub steps: Vec<TestStep>,
928 pub stdout: String,
929 pub stderr: String,
930 pub annotations: Vec<TestAnnotation>,
932 pub metadata: serde_json::Value,
934}
935
936#[derive(Debug, Clone)]
938pub struct TestFailure {
939 pub message: String,
940 pub stack: Option<String>,
941 pub diff: Option<String>,
942 pub screenshot: Option<Vec<u8>>,
944}
945
946impl fmt::Display for TestFailure {
947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948 write!(f, "{}", self.message)?;
949 if let Some(diff) = &self.diff {
950 write!(f, "\n{diff}")?;
951 }
952 Ok(())
953 }
954}
955
956impl std::error::Error for TestFailure {}
957
958impl TestFailure {
959 #[must_use]
965 pub fn wrap(prefix: impl std::fmt::Display, err: ferridriver::FerriError) -> Self {
966 Self {
967 message: format!("{prefix}: {}", err.display_named()),
968 stack: None,
969 diff: None,
970 screenshot: None,
971 }
972 }
973}
974
975impl From<String> for TestFailure {
980 fn from(message: String) -> Self {
981 Self {
982 message,
983 stack: None,
984 diff: None,
985 screenshot: None,
986 }
987 }
988}
989
990impl From<&str> for TestFailure {
991 fn from(message: &str) -> Self {
992 Self::from(message.to_string())
993 }
994}
995
996impl From<ferridriver::FerriError> for TestFailure {
1003 fn from(err: ferridriver::FerriError) -> Self {
1004 Self {
1005 message: err.display_named(),
1006 stack: None,
1007 diff: None,
1008 screenshot: None,
1009 }
1010 }
1011}
1012
1013#[derive(Debug, Clone)]
1015pub struct Attachment {
1016 pub name: String,
1017 pub content_type: String,
1018 pub body: AttachmentBody,
1019}
1020
1021#[derive(Debug, Clone)]
1022pub enum AttachmentBody {
1023 Bytes(Vec<u8>),
1024 Path(PathBuf),
1025}
1026
1027#[derive(Clone)]
1035pub struct TestFixtures {
1036 pub browser: Arc<ferridriver::Browser>,
1037 pub page: Arc<ferridriver::Page>,
1038 pub context: Arc<ferridriver::context::ContextRef>,
1039 pub request: Arc<ferridriver::http_client::HttpClient>,
1040 pub test_info: Arc<TestInfo>,
1041 pub modifiers: Arc<TestModifiers>,
1042 pub browser_config: crate::config::BrowserConfig,
1043 pub bdd_args: Option<Vec<serde_json::Value>>,
1045 pub bdd_data_table: Option<Vec<Vec<String>>>,
1046 pub bdd_doc_string: Option<String>,
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use super::*;
1052 use ferridriver::FerriError;
1053
1054 #[test]
1055 fn testfailure_from_timeout_keeps_class_prefix() {
1056 let tf = TestFailure::from(FerriError::timeout("navigating", 30_000));
1057 assert_eq!(tf.message, "TimeoutError: Timeout 30000ms exceeded while navigating");
1058 }
1059
1060 #[test]
1061 fn testfailure_from_target_closed_keeps_class_prefix() {
1062 let tf = TestFailure::from(FerriError::target_closed(Some("crashed".into())));
1063 assert_eq!(
1064 tf.message,
1065 "TargetClosedError: Target page, context or browser has been closed: crashed"
1066 );
1067 }
1068
1069 #[test]
1070 fn testfailure_from_backend_has_no_prefix() {
1071 let tf = TestFailure::from(FerriError::backend("launch failed"));
1072 assert_eq!(tf.message, "backend error: launch failed");
1073 }
1074
1075 #[test]
1076 fn testfailure_wrap_preserves_timeout_class_after_prefix() {
1077 let tf = TestFailure::wrap("fixture 'browser' failed", FerriError::timeout("launch", 30_000));
1078 assert_eq!(
1079 tf.message,
1080 "fixture 'browser' failed: TimeoutError: Timeout 30000ms exceeded while launch"
1081 );
1082 }
1083
1084 #[test]
1085 fn testfailure_wrap_unnamed_keeps_message_only() {
1086 let tf = TestFailure::wrap("fixture 'page' failed", FerriError::backend("oops"));
1087 assert_eq!(tf.message, "fixture 'page' failed: backend error: oops");
1088 }
1089}