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)]
148pub struct TestPlan {
149 pub suites: Vec<TestSuite>,
150 pub total_tests: usize,
152 pub shard: Option<ShardInfo>,
154}
155
156#[derive(Debug, Clone)]
157pub struct ShardInfo {
158 pub current: u32,
159 pub total: u32,
160}
161
162pub struct SuiteDef {
166 pub id: String,
168 pub name: String,
169 pub file: String,
170 pub mode: SuiteMode,
171}
172
173pub struct HookDef {
175 pub suite_id: String,
177 pub kind: HookKind,
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum HookPhase {
183 Before,
184 After,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum HookScope {
190 Suite,
191 Scenario,
192 Step,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub enum HookOwner {
198 Root,
199 Suite(String),
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct HookRegistration {
205 pub phase: HookPhase,
206 pub scope: HookScope,
207 pub owner: HookOwner,
208 pub tags: Option<String>,
209 pub requested_fixtures: Vec<String>,
210}
211
212pub enum HookKind {
214 BeforeAll(SuiteHookFn),
215 AfterAll(SuiteHookFn),
216 BeforeEach(HookFn),
217 AfterEach(HookFn),
218}
219
220pub struct TestPlanBuilder {
227 tests: Vec<TestCase>,
228 suites: Vec<SuiteDef>,
229 hooks: Vec<HookDef>,
230}
231
232impl Default for TestPlanBuilder {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238impl TestPlanBuilder {
239 pub fn new() -> Self {
240 Self {
241 tests: Vec::new(),
242 suites: Vec::new(),
243 hooks: Vec::new(),
244 }
245 }
246
247 pub fn add_test(&mut self, test: TestCase) {
248 self.tests.push(test);
249 }
250
251 pub fn add_suite(&mut self, suite: SuiteDef) {
252 self.suites.push(suite);
253 }
254
255 pub fn add_hook(&mut self, hook: HookDef) {
256 self.hooks.push(hook);
257 }
258
259 pub fn build(self) -> TestPlan {
265 use rustc_hash::FxHashMap;
266
267 let suite_meta: FxHashMap<String, (String, String, SuiteMode)> = self
269 .suites
270 .into_iter()
271 .map(|s| (s.id, (s.name, s.file, s.mode)))
272 .collect();
273
274 let mut grouped: FxHashMap<String, Vec<TestCase>> = FxHashMap::default();
276 for tc in self.tests {
277 let key = tc.id.suite.clone().unwrap_or_default();
278 grouped.entry(key).or_default().push(tc);
279 }
280
281 let mut hook_map: FxHashMap<String, Hooks> = FxHashMap::default();
283 for h in self.hooks {
284 let hooks = hook_map.entry(h.suite_id).or_default();
285 match h.kind {
286 HookKind::BeforeAll(f) => hooks.before_all.push(f),
287 HookKind::AfterAll(f) => hooks.after_all.push(f),
288 HookKind::BeforeEach(f) => hooks.before_each.push(f),
289 HookKind::AfterEach(f) => hooks.after_each.push(f),
290 }
291 }
292
293 let mut plan_suites: Vec<TestSuite> = Vec::new();
295 let mut total = 0usize;
296
297 for (suite_key, tests) in grouped {
298 total += tests.len();
299 let (name, file, mode) = if suite_key.is_empty() {
300 ("tests".to_string(), String::new(), SuiteMode::Parallel)
301 } else if let Some((n, f, m)) = suite_meta.get(&suite_key) {
302 (n.clone(), f.clone(), *m)
303 } else {
304 (suite_key.clone(), String::new(), SuiteMode::Parallel)
306 };
307 let hooks = hook_map.remove(&suite_key).unwrap_or_default();
308 plan_suites.push(TestSuite {
309 name,
310 file,
311 tests,
312 hooks,
313 annotations: Vec::new(),
314 mode,
315 });
316 }
317
318 TestPlan {
319 suites: plan_suites,
320 total_tests: total,
321 shard: None,
322 }
323 }
324}
325
326#[derive(Clone)]
331pub struct TestInfo {
332 pub test_id: TestId,
334 pub title_path: Vec<String>,
336 pub retry: u32,
338 pub worker_index: u32,
340 pub parallel_index: u32,
342 pub repeat_each_index: u32,
344 pub output_dir: PathBuf,
346 pub snapshot_dir: PathBuf,
348 pub snapshot_path_template: Option<String>,
350 pub update_snapshots: crate::config::UpdateSnapshotsMode,
352 pub attachments: Arc<Mutex<Vec<Attachment>>>,
354 pub steps: Arc<Mutex<Vec<TestStep>>>,
356 pub soft_errors: Arc<Mutex<Vec<TestFailure>>>,
358 pub timeout: Duration,
360 pub tags: Vec<String>,
362 pub start_time: Instant,
364 pub event_bus: Option<EventBus>,
366 pub annotations: Arc<Mutex<Vec<TestAnnotation>>>,
368}
369
370impl TestInfo {
371 pub fn new_anonymous() -> Self {
373 Self {
374 test_id: TestId {
375 file: String::new(),
376 suite: None,
377 name: "anonymous".into(),
378 line: None,
379 },
380 title_path: Vec::new(),
381 retry: 0,
382 worker_index: 0,
383 parallel_index: 0,
384 repeat_each_index: 0,
385 output_dir: PathBuf::new(),
386 snapshot_dir: PathBuf::new(),
387 snapshot_path_template: None,
388 update_snapshots: crate::config::UpdateSnapshotsMode::default(),
389 attachments: Arc::new(Mutex::new(Vec::new())),
390 steps: Arc::new(Mutex::new(Vec::new())),
391 soft_errors: Arc::new(Mutex::new(Vec::new())),
392 timeout: Duration::from_secs(30),
393 tags: Vec::new(),
394 start_time: Instant::now(),
395 event_bus: None,
396 annotations: Arc::new(Mutex::new(Vec::new())),
397 }
398 }
399
400 pub async fn annotate(&self, type_name: impl Into<String>, description: impl Into<String>) {
402 let mut annotations = self.annotations.lock().await;
403 annotations.push(TestAnnotation::Info {
404 type_name: type_name.into(),
405 description: description.into(),
406 });
407 }
408
409 pub async fn get_annotations(&self) -> Vec<TestAnnotation> {
411 let annotations = self.annotations.lock().await;
412 annotations.clone()
413 }
414 pub async fn attach(&self, name: String, content_type: String, body: AttachmentBody) {
416 let mut attachments = self.attachments.lock().await;
417 attachments.push(Attachment {
418 name,
419 content_type,
420 body,
421 });
422 }
423
424 pub async fn add_soft_error(&self, error: TestFailure) {
426 let mut errors = self.soft_errors.lock().await;
427 errors.push(error);
428 }
429
430 pub async fn has_soft_errors(&self) -> bool {
432 let errors = self.soft_errors.lock().await;
433 !errors.is_empty()
434 }
435
436 pub async fn drain_soft_errors(&self) -> Vec<TestFailure> {
438 let mut errors = self.soft_errors.lock().await;
439 errors.drain(..).collect()
440 }
441
442 pub async fn push_step(&self, step: TestStep) {
444 let mut steps = self.steps.lock().await;
445 steps.push(step);
446 }
447
448 pub fn elapsed(&self) -> Duration {
450 self.start_time.elapsed()
451 }
452
453 pub async fn begin_step(&self, title: impl Into<String>, category: StepCategory) -> StepHandle {
458 let title = title.into();
459 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
460
461 if let Some(bus) = &self.event_bus {
462 bus
463 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
464 crate::reporter::StepStartedEvent {
465 test_id: self.test_id.clone(),
466 step_id: step_id.clone(),
467 parent_step_id: None,
468 title: title.clone(),
469 category: category.clone(),
470 },
471 )))
472 .await;
473 }
474
475 StepHandle {
476 step_id,
477 test_id: self.test_id.clone(),
478 title,
479 category,
480 parent_step_id: None,
481 start: Instant::now(),
482 metadata: None,
483 event_bus: self.event_bus.clone(),
484 steps: Arc::clone(&self.steps),
485 }
486 }
487
488 pub async fn begin_child_step(
490 &self,
491 title: impl Into<String>,
492 category: StepCategory,
493 parent_step_id: &str,
494 ) -> StepHandle {
495 let title = title.into();
496 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
497
498 if let Some(bus) = &self.event_bus {
499 bus
500 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
501 crate::reporter::StepStartedEvent {
502 test_id: self.test_id.clone(),
503 step_id: step_id.clone(),
504 parent_step_id: Some(parent_step_id.to_string()),
505 title: title.clone(),
506 category: category.clone(),
507 },
508 )))
509 .await;
510 }
511
512 StepHandle {
513 step_id,
514 test_id: self.test_id.clone(),
515 title,
516 category,
517 parent_step_id: Some(parent_step_id.to_string()),
518 start: Instant::now(),
519 metadata: None,
520 event_bus: self.event_bus.clone(),
521 steps: Arc::clone(&self.steps),
522 }
523 }
524
525 pub async fn record_step(
528 &self,
529 title: impl Into<String>,
530 category: StepCategory,
531 status: StepStatus,
532 duration: Duration,
533 error: Option<String>,
534 metadata: Option<serde_json::Value>,
535 ) {
536 let title = title.into();
537 let step_id = format!("{}@{}", category, STEP_ID_COUNTER.fetch_add(1, Ordering::Relaxed));
538
539 if let Some(bus) = &self.event_bus {
540 bus
541 .emit(crate::reporter::ReporterEvent::StepStarted(Box::new(
542 crate::reporter::StepStartedEvent {
543 test_id: self.test_id.clone(),
544 step_id: step_id.clone(),
545 parent_step_id: None,
546 title: title.clone(),
547 category: category.clone(),
548 },
549 )))
550 .await;
551 bus
552 .emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
553 crate::reporter::StepFinishedEvent {
554 test_id: self.test_id.clone(),
555 step_id: step_id.clone(),
556 title: title.clone(),
557 category: category.clone(),
558 duration,
559 error: error.clone(),
560 metadata: metadata.clone(),
561 },
562 )))
563 .await;
564 }
565
566 self.steps.lock().await.push(TestStep {
567 step_id,
568 title,
569 category,
570 duration,
571 status,
572 error,
573 location: None,
574 parent_step_id: None,
575 metadata,
576 steps: Vec::new(),
577 });
578 }
579}
580
581static STEP_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
583
584pub struct StepHandle {
590 pub step_id: String,
591 pub test_id: TestId,
592 pub title: String,
593 pub category: StepCategory,
594 pub parent_step_id: Option<String>,
595 pub start: Instant,
596 pub metadata: Option<serde_json::Value>,
598 event_bus: Option<EventBus>,
599 steps: Arc<Mutex<Vec<TestStep>>>,
600}
601
602impl StepHandle {
603 pub async fn end(self, error: Option<String>) {
605 let duration = self.start.elapsed();
606 let status = if error.is_some() {
607 StepStatus::Failed
608 } else {
609 StepStatus::Passed
610 };
611
612 if let Some(bus) = &self.event_bus {
614 bus
615 .emit(crate::reporter::ReporterEvent::StepFinished(Box::new(
616 crate::reporter::StepFinishedEvent {
617 test_id: self.test_id.clone(),
618 step_id: self.step_id.clone(),
619 title: self.title.clone(),
620 category: self.category.clone(),
621 duration,
622 error: error.clone(),
623 metadata: self.metadata.clone(),
624 },
625 )))
626 .await;
627 }
628
629 let step = TestStep {
631 step_id: self.step_id,
632 title: self.title,
633 category: self.category,
634 duration,
635 status,
636 error,
637 location: None,
638 parent_step_id: self.parent_step_id,
639 metadata: self.metadata.clone(),
640 steps: Vec::new(),
641 };
642 self.steps.lock().await.push(step);
643 }
644
645 pub async fn skip(self, reason: Option<String>) {
647 self.finish_with_status(StepStatus::Skipped, reason).await;
648 }
649
650 pub async fn pending(self, reason: Option<String>) {
652 self.finish_with_status(StepStatus::Pending, reason).await;
653 }
654
655 async fn finish_with_status(self, status: StepStatus, error: Option<String>) {
656 let duration = self.start.elapsed();
657
658 if let Some(bus) = &self.event_bus {
659 bus
660 .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 .await;
672 }
673
674 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,
684 steps: Vec::new(),
685 };
686 self.steps.lock().await.push(step);
687 }
688}
689
690#[derive(Debug, Clone)]
694pub struct TestStep {
695 pub step_id: String,
697 pub title: String,
698 pub category: StepCategory,
699 pub duration: Duration,
700 pub status: StepStatus,
702 pub error: Option<String>,
703 pub location: Option<String>,
705 pub parent_step_id: Option<String>,
707 pub metadata: Option<serde_json::Value>,
710 pub steps: Vec<TestStep>,
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
715pub enum StepStatus {
716 Passed,
717 Failed,
718 Skipped,
719 Pending,
721}
722
723#[derive(Debug, Clone, PartialEq, Eq)]
725pub enum StepCategory {
726 TestStep,
728 Expect,
730 Fixture,
732 Hook,
734 PwApi,
736}
737
738impl StepCategory {
739 pub fn is_visible(&self) -> bool {
743 matches!(self, Self::TestStep | Self::Hook)
744 }
745}
746
747impl fmt::Display for StepCategory {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 match self {
750 Self::TestStep => write!(f, "test.step"),
751 Self::Expect => write!(f, "expect"),
752 Self::Fixture => write!(f, "fixture"),
753 Self::Hook => write!(f, "hook"),
754 Self::PwApi => write!(f, "pw:api"),
755 }
756 }
757}
758
759#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
762#[serde(rename_all = "lowercase")]
763pub enum TestAnnotation {
764 Skip {
767 reason: Option<String>,
768 condition: Option<String>,
769 },
770 Slow {
773 reason: Option<String>,
774 condition: Option<String>,
775 },
776 Fixme {
779 reason: Option<String>,
780 condition: Option<String>,
781 },
782 Fail {
785 reason: Option<String>,
786 condition: Option<String>,
787 },
788 Only,
789 Tag(String),
790 Info {
792 type_name: String,
793 description: String,
794 },
795}
796
797#[derive(Debug, Clone, Default, PartialEq, Eq)]
798pub enum ExpectedStatus {
799 #[default]
800 Pass,
801 Fail,
802}
803
804pub struct TestModifiers {
813 pub skipped: AtomicBool,
815 pub skip_reason: std::sync::Mutex<Option<String>>,
817 pub expected_failure: AtomicBool,
819 pub slow: AtomicBool,
821 pub timeout_override: std::sync::Mutex<Option<u64>>,
823}
824
825impl Default for TestModifiers {
826 fn default() -> Self {
827 Self {
828 skipped: AtomicBool::new(false),
829 skip_reason: std::sync::Mutex::new(None),
830 expected_failure: AtomicBool::new(false),
831 slow: AtomicBool::new(false),
832 timeout_override: std::sync::Mutex::new(None),
833 }
834 }
835}
836
837impl std::fmt::Debug for TestModifiers {
838 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
839 f.debug_struct("TestModifiers")
840 .field("skipped", &self.skipped.load(Ordering::Relaxed))
841 .field("expected_failure", &self.expected_failure.load(Ordering::Relaxed))
842 .field("slow", &self.slow.load(Ordering::Relaxed))
843 .finish_non_exhaustive()
844 }
845}
846
847#[derive(Debug, Clone, PartialEq, Eq)]
851pub enum TestStatus {
852 Passed,
853 Failed,
854 TimedOut,
855 Skipped,
856 Flaky,
858 Interrupted,
860}
861
862impl fmt::Display for TestStatus {
863 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
864 match self {
865 Self::Passed => write!(f, "passed"),
866 Self::Failed => write!(f, "failed"),
867 Self::TimedOut => write!(f, "timed out"),
868 Self::Skipped => write!(f, "skipped"),
869 Self::Flaky => write!(f, "flaky"),
870 Self::Interrupted => write!(f, "interrupted"),
871 }
872 }
873}
874
875#[derive(Debug, Clone)]
877pub struct TestOutcome {
878 pub test_id: TestId,
879 pub status: TestStatus,
880 pub duration: Duration,
881 pub attempt: u32,
882 pub max_attempts: u32,
883 pub error: Option<TestFailure>,
884 pub attachments: Vec<Attachment>,
885 pub steps: Vec<TestStep>,
886 pub stdout: String,
887 pub stderr: String,
888 pub annotations: Vec<TestAnnotation>,
890 pub metadata: serde_json::Value,
892}
893
894#[derive(Debug, Clone)]
896pub struct TestFailure {
897 pub message: String,
898 pub stack: Option<String>,
899 pub diff: Option<String>,
900 pub screenshot: Option<Vec<u8>>,
902}
903
904impl fmt::Display for TestFailure {
905 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
906 write!(f, "{}", self.message)?;
907 if let Some(diff) = &self.diff {
908 write!(f, "\n{diff}")?;
909 }
910 Ok(())
911 }
912}
913
914impl std::error::Error for TestFailure {}
915
916impl From<String> for TestFailure {
919 fn from(message: String) -> Self {
920 Self {
921 message,
922 stack: None,
923 diff: None,
924 screenshot: None,
925 }
926 }
927}
928
929impl From<&str> for TestFailure {
930 fn from(message: &str) -> Self {
931 Self::from(message.to_string())
932 }
933}
934
935#[derive(Debug, Clone)]
937pub struct Attachment {
938 pub name: String,
939 pub content_type: String,
940 pub body: AttachmentBody,
941}
942
943#[derive(Debug, Clone)]
944pub enum AttachmentBody {
945 Bytes(Vec<u8>),
946 Path(PathBuf),
947}
948
949#[derive(Clone)]
957pub struct TestFixtures {
958 pub browser: Arc<ferridriver::Browser>,
959 pub page: Arc<ferridriver::Page>,
960 pub context: Arc<ferridriver::context::ContextRef>,
961 pub request: Arc<ferridriver::api_request::APIRequestContext>,
962 pub test_info: Arc<TestInfo>,
963 pub modifiers: Arc<TestModifiers>,
964 pub browser_config: crate::config::BrowserConfig,
965 pub bdd_args: Option<Vec<serde_json::Value>>,
967 pub bdd_data_table: Option<Vec<Vec<String>>>,
968 pub bdd_doc_string: Option<String>,
969}