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
517 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
518 crate::reporter::StepStartedEvent {
519 test_id: self.test_id.clone(),
520 step_id: step_id.clone(),
521 parent_step_id: None,
522 title: title.clone(),
523 category: category.clone(),
524 },
525 )))
526 .await;
527 }
528
529 StepHandle {
530 step_id,
531 test_id: self.test_id.clone(),
532 title,
533 category,
534 parent_step_id: None,
535 start: Instant::now(),
536 metadata: None,
537 event_bus: self.event_bus.clone(),
538 steps: Arc::clone(&self.steps),
539 }
540 }
541
542 pub async fn begin_child_step(
544 &self,
545 title: impl Into<String>,
546 category: StepCategory,
547 parent_step_id: &str,
548 ) -> StepHandle {
549 let title = title.into();
550 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
551
552 if let Some(bus) = &self.event_bus {
553 bus
554 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
555 crate::reporter::StepStartedEvent {
556 test_id: self.test_id.clone(),
557 step_id: step_id.clone(),
558 parent_step_id: Some(parent_step_id.to_string()),
559 title: title.clone(),
560 category: category.clone(),
561 },
562 )))
563 .await;
564 }
565
566 StepHandle {
567 step_id,
568 test_id: self.test_id.clone(),
569 title,
570 category,
571 parent_step_id: Some(parent_step_id.to_string()),
572 start: Instant::now(),
573 metadata: None,
574 event_bus: self.event_bus.clone(),
575 steps: Arc::clone(&self.steps),
576 }
577 }
578
579 pub async fn record_step(
582 &self,
583 title: impl Into<String>,
584 category: StepCategory,
585 status: StepStatus,
586 duration: Duration,
587 error: Option<String>,
588 metadata: Option<serde_json::Value>,
589 ) {
590 let title = title.into();
591 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
592
593 if let Some(bus) = &self.event_bus {
594 bus
595 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
596 crate::reporter::StepStartedEvent {
597 test_id: self.test_id.clone(),
598 step_id: step_id.clone(),
599 parent_step_id: None,
600 title: title.clone(),
601 category: category.clone(),
602 },
603 )))
604 .await;
605 bus
606 .emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
607 crate::reporter::StepFinishedEvent {
608 test_id: self.test_id.clone(),
609 step_id: step_id.clone(),
610 title: title.clone(),
611 category: category.clone(),
612 duration,
613 error: error.clone(),
614 metadata: metadata.clone(),
615 },
616 )))
617 .await;
618 }
619
620 self.steps.lock().await.push(TestStep {
621 step_id,
622 title,
623 category,
624 duration,
625 status,
626 error,
627 location: None,
628 parent_step_id: None,
629 metadata,
630 steps: Vec::new(),
631 });
632 }
633}
634
635static STEP_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
637
638pub struct StepHandle {
644 pub step_id: String,
645 pub test_id: TestId,
646 pub title: String,
647 pub category: StepCategory,
648 pub parent_step_id: Option<String>,
649 pub start: Instant,
650 pub metadata: Option<serde_json::Value>,
652 event_bus: Option<EventBus>,
653 steps: Arc<Mutex<Vec<TestStep>>>,
654}
655
656impl StepHandle {
657 pub async fn end(self, error: Option<String>) {
659 let duration = self.start.elapsed();
660 let status = if error.is_some() {
661 StepStatus::Failed
662 } else {
663 StepStatus::Passed
664 };
665
666 if let Some(bus) = &self.event_bus {
668 bus
669 .emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
670 crate::reporter::StepFinishedEvent {
671 test_id: self.test_id.clone(),
672 step_id: self.step_id.clone(),
673 title: self.title.clone(),
674 category: self.category.clone(),
675 duration,
676 error: error.clone(),
677 metadata: self.metadata.clone(),
678 },
679 )))
680 .await;
681 }
682
683 let step = TestStep {
685 step_id: self.step_id,
686 title: self.title,
687 category: self.category,
688 duration,
689 status,
690 error,
691 location: None,
692 parent_step_id: self.parent_step_id,
693 metadata: self.metadata.clone(),
694 steps: Vec::new(),
695 };
696 self.steps.lock().await.push(step);
697 }
698
699 pub async fn skip(self, reason: Option<String>) {
701 self.finish_with_status(StepStatus::Skipped, reason).await;
702 }
703
704 pub async fn pending(self, reason: Option<String>) {
706 self.finish_with_status(StepStatus::Pending, reason).await;
707 }
708
709 async fn finish_with_status(self, status: StepStatus, error: Option<String>) {
710 let duration = self.start.elapsed();
711
712 if let Some(bus) = &self.event_bus {
713 bus
714 .emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
715 crate::reporter::StepFinishedEvent {
716 test_id: self.test_id.clone(),
717 step_id: self.step_id.clone(),
718 title: self.title.clone(),
719 category: self.category.clone(),
720 duration,
721 error: error.clone(),
722 metadata: self.metadata.clone(),
723 },
724 )))
725 .await;
726 }
727
728 let step = TestStep {
729 step_id: self.step_id,
730 title: self.title,
731 category: self.category,
732 duration,
733 status,
734 error,
735 location: None,
736 parent_step_id: self.parent_step_id,
737 metadata: self.metadata,
738 steps: Vec::new(),
739 };
740 self.steps.lock().await.push(step);
741 }
742}
743
744#[derive(Debug, Clone)]
748pub struct TestStep {
749 pub step_id: String,
751 pub title: String,
752 pub category: StepCategory,
753 pub duration: Duration,
754 pub status: StepStatus,
756 pub error: Option<String>,
757 pub location: Option<String>,
759 pub parent_step_id: Option<String>,
761 pub metadata: Option<serde_json::Value>,
764 pub steps: Vec<TestStep>,
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq)]
769pub enum StepStatus {
770 Passed,
771 Failed,
772 Skipped,
773 Pending,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq)]
779pub enum StepCategory {
780 TestStep,
782 Expect,
784 Fixture,
786 Hook,
788 PwApi,
790}
791
792impl StepCategory {
793 pub fn is_visible(&self) -> bool {
797 matches!(self, Self::TestStep | Self::Hook)
798 }
799}
800
801impl fmt::Display for StepCategory {
802 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
803 match self {
804 Self::TestStep => write!(f, "test.step"),
805 Self::Expect => write!(f, "expect"),
806 Self::Fixture => write!(f, "fixture"),
807 Self::Hook => write!(f, "hook"),
808 Self::PwApi => write!(f, "pw:api"),
809 }
810 }
811}
812
813#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
816#[serde(rename_all = "lowercase")]
817pub enum TestAnnotation {
818 Skip {
821 reason: Option<String>,
822 condition: Option<String>,
823 },
824 Slow {
827 reason: Option<String>,
828 condition: Option<String>,
829 },
830 Fixme {
833 reason: Option<String>,
834 condition: Option<String>,
835 },
836 Fail {
839 reason: Option<String>,
840 condition: Option<String>,
841 },
842 Only,
843 Tag(String),
844 Info {
846 type_name: String,
847 description: String,
848 },
849}
850
851#[derive(Debug, Clone, Default, PartialEq, Eq)]
852pub enum ExpectedStatus {
853 #[default]
854 Pass,
855 Fail,
856}
857
858pub struct TestModifiers {
867 pub skipped: AtomicBool,
869 pub skip_reason: std::sync::Mutex<Option<String>>,
871 pub expected_failure: AtomicBool,
873 pub slow: AtomicBool,
875 pub timeout_override: std::sync::Mutex<Option<u64>>,
877}
878
879impl Default for TestModifiers {
880 fn default() -> Self {
881 Self {
882 skipped: AtomicBool::new(false),
883 skip_reason: std::sync::Mutex::new(None),
884 expected_failure: AtomicBool::new(false),
885 slow: AtomicBool::new(false),
886 timeout_override: std::sync::Mutex::new(None),
887 }
888 }
889}
890
891impl std::fmt::Debug for TestModifiers {
892 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
893 f.debug_struct("TestModifiers")
894 .field("skipped", &self.skipped.load(Ordering::Relaxed))
895 .field("expected_failure", &self.expected_failure.load(Ordering::Relaxed))
896 .field("slow", &self.slow.load(Ordering::Relaxed))
897 .finish_non_exhaustive()
898 }
899}
900
901#[derive(Debug, Clone, PartialEq, Eq)]
905pub enum TestStatus {
906 Passed,
907 Failed,
908 TimedOut,
909 Skipped,
910 Flaky,
912 Interrupted,
914}
915
916impl fmt::Display for TestStatus {
917 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
918 match self {
919 Self::Passed => write!(f, "passed"),
920 Self::Failed => write!(f, "failed"),
921 Self::TimedOut => write!(f, "timed out"),
922 Self::Skipped => write!(f, "skipped"),
923 Self::Flaky => write!(f, "flaky"),
924 Self::Interrupted => write!(f, "interrupted"),
925 }
926 }
927}
928
929#[derive(Debug, Clone)]
931pub struct TestOutcome {
932 pub test_id: TestId,
933 pub status: TestStatus,
934 pub duration: Duration,
935 pub attempt: u32,
936 pub max_attempts: u32,
937 pub error: Option<TestFailure>,
938 pub attachments: Vec<Attachment>,
939 pub steps: Vec<TestStep>,
940 pub stdout: String,
941 pub stderr: String,
942 pub annotations: Vec<TestAnnotation>,
944 pub metadata: serde_json::Value,
946}
947
948#[derive(Debug, Clone)]
950pub struct TestFailure {
951 pub message: String,
952 pub stack: Option<String>,
953 pub diff: Option<String>,
954 pub screenshot: Option<Vec<u8>>,
956}
957
958impl fmt::Display for TestFailure {
959 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
960 write!(f, "{}", self.message)?;
961 if let Some(diff) = &self.diff {
962 write!(f, "\n{diff}")?;
963 }
964 Ok(())
965 }
966}
967
968impl std::error::Error for TestFailure {}
969
970impl TestFailure {
971 #[must_use]
977 pub fn wrap(prefix: impl std::fmt::Display, err: ferridriver::FerriError) -> Self {
978 Self {
979 message: format!("{prefix}: {}", err.display_named()),
980 stack: None,
981 diff: None,
982 screenshot: None,
983 }
984 }
985}
986
987impl From<String> for TestFailure {
992 fn from(message: String) -> Self {
993 Self {
994 message,
995 stack: None,
996 diff: None,
997 screenshot: None,
998 }
999 }
1000}
1001
1002impl From<&str> for TestFailure {
1003 fn from(message: &str) -> Self {
1004 Self::from(message.to_string())
1005 }
1006}
1007
1008impl From<ferridriver::FerriError> for TestFailure {
1015 fn from(err: ferridriver::FerriError) -> Self {
1016 Self {
1017 message: err.display_named(),
1018 stack: None,
1019 diff: None,
1020 screenshot: None,
1021 }
1022 }
1023}
1024
1025#[derive(Debug, Clone)]
1027pub struct Attachment {
1028 pub name: String,
1029 pub content_type: String,
1030 pub body: AttachmentBody,
1031}
1032
1033#[derive(Debug, Clone)]
1034pub enum AttachmentBody {
1035 Bytes(Vec<u8>),
1036 Path(PathBuf),
1037}
1038
1039#[derive(Clone)]
1047pub struct TestFixtures {
1048 pub browser: Arc<ferridriver::Browser>,
1049 pub page: Arc<ferridriver::Page>,
1050 pub context: Arc<ferridriver::context::ContextRef>,
1051 pub request: Arc<ferridriver::http_client::HttpClient>,
1052 pub test_info: Arc<TestInfo>,
1053 pub modifiers: Arc<TestModifiers>,
1054 pub browser_config: crate::config::BrowserConfig,
1055 pub bdd_args: Option<Vec<serde_json::Value>>,
1057 pub bdd_data_table: Option<Vec<Vec<String>>>,
1058 pub bdd_doc_string: Option<String>,
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063 use super::*;
1064 use ferridriver::FerriError;
1065
1066 #[test]
1067 fn testfailure_from_timeout_keeps_class_prefix() {
1068 let tf = TestFailure::from(FerriError::timeout("navigating", 30_000));
1069 assert_eq!(tf.message, "TimeoutError: Timeout 30000ms exceeded while navigating");
1070 }
1071
1072 #[test]
1073 fn testfailure_from_target_closed_keeps_class_prefix() {
1074 let tf = TestFailure::from(FerriError::target_closed(Some("crashed".into())));
1075 assert_eq!(
1076 tf.message,
1077 "TargetClosedError: Target page, context or browser has been closed: crashed"
1078 );
1079 }
1080
1081 #[test]
1082 fn testfailure_from_backend_has_no_prefix() {
1083 let tf = TestFailure::from(FerriError::backend("launch failed"));
1084 assert_eq!(tf.message, "backend error: launch failed");
1085 }
1086
1087 #[test]
1088 fn testfailure_wrap_preserves_timeout_class_after_prefix() {
1089 let tf = TestFailure::wrap("fixture 'browser' failed", FerriError::timeout("launch", 30_000));
1090 assert_eq!(
1091 tf.message,
1092 "fixture 'browser' failed: TimeoutError: Timeout 30000ms exceeded while launch"
1093 );
1094 }
1095
1096 #[test]
1097 fn testfailure_wrap_unnamed_keeps_message_only() {
1098 let tf = TestFailure::wrap("fixture 'page' failed", FerriError::backend("oops"));
1099 assert_eq!(tf.message, "fixture 'page' failed: backend error: oops");
1100 }
1101}