viewpoint_test/expect/
soft.rs

1//! Soft assertions that collect failures without stopping test execution.
2//!
3//! Soft assertions allow you to make multiple assertions and collect all failures,
4//! rather than failing on the first assertion. This is useful when you want to
5//! check multiple things in a test and see all failures at once.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use viewpoint_test::{expect, SoftAssertions};
11//!
12//! let soft = SoftAssertions::new();
13//!
14//! // These assertions don't fail immediately
15//! soft.expect(&locator1).to_be_visible().await;
16//! soft.expect(&locator2).to_have_text("Expected").await;
17//! soft.expect(&locator3).to_be_enabled().await;
18//!
19//! // Check if all assertions passed
20//! if !soft.passed() {
21//!     println!("Failures: {:?}", soft.errors());
22//! }
23//!
24//! // Or assert all at the end (fails if any assertion failed)
25//! soft.assert_all()?;
26//! ```
27
28use std::sync::{Arc, Mutex};
29
30use viewpoint_core::{Locator, Page};
31
32use super::soft_locator::SoftLocatorAssertions;
33use super::soft_page::SoftPageAssertions;
34use super::{LocatorAssertions, PageAssertions};
35use crate::error::TestError;
36
37/// Collection of soft assertion errors.
38#[derive(Debug, Clone)]
39pub struct SoftAssertionError {
40    /// The assertion that failed.
41    pub assertion: String,
42    /// The error message.
43    pub message: String,
44    /// Optional expected value.
45    pub expected: Option<String>,
46    /// Optional actual value.
47    pub actual: Option<String>,
48}
49
50impl SoftAssertionError {
51    /// Create a new soft assertion error.
52    pub fn new(assertion: impl Into<String>, message: impl Into<String>) -> Self {
53        Self {
54            assertion: assertion.into(),
55            message: message.into(),
56            expected: None,
57            actual: None,
58        }
59    }
60
61    /// Set the expected value.
62    #[must_use]
63    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
64        self.expected = Some(expected.into());
65        self
66    }
67
68    /// Set the actual value.
69    #[must_use]
70    pub fn with_actual(mut self, actual: impl Into<String>) -> Self {
71        self.actual = Some(actual.into());
72        self
73    }
74}
75
76impl std::fmt::Display for SoftAssertionError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}: {}", self.assertion, self.message)?;
79        if let Some(ref expected) = self.expected {
80            write!(f, "\n  Expected: {expected}")?;
81        }
82        if let Some(ref actual) = self.actual {
83            write!(f, "\n  Actual: {actual}")?;
84        }
85        Ok(())
86    }
87}
88
89/// A context for collecting soft assertions.
90///
91/// Soft assertions allow you to make multiple assertions without stopping
92/// on the first failure. All failures are collected and can be checked at the end.
93#[derive(Debug, Clone)]
94pub struct SoftAssertions {
95    errors: Arc<Mutex<Vec<SoftAssertionError>>>,
96}
97
98impl Default for SoftAssertions {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl SoftAssertions {
105    /// Create a new soft assertion context.
106    pub fn new() -> Self {
107        Self {
108            errors: Arc::new(Mutex::new(Vec::new())),
109        }
110    }
111
112    /// Create soft assertions for a locator.
113    ///
114    /// Assertions made through this will not fail immediately, but will be
115    /// collected for later inspection.
116    ///
117    /// # Example
118    ///
119    /// ```ignore
120    /// let soft = SoftAssertions::new();
121    /// soft.expect(&locator).to_be_visible().await;
122    /// soft.expect(&locator).to_have_text("Hello").await;
123    /// soft.assert_all()?;
124    /// ```
125    pub fn expect<'a>(&self, locator: &'a Locator<'a>) -> SoftLocatorAssertions<'a> {
126        SoftLocatorAssertions {
127            assertions: LocatorAssertions::new(locator),
128            errors: self.errors.clone(),
129        }
130    }
131
132    /// Create soft assertions for a page.
133    ///
134    /// # Example
135    ///
136    /// ```ignore
137    /// let soft = SoftAssertions::new();
138    /// soft.expect_page(&page).to_have_url("https://example.com").await;
139    /// soft.assert_all()?;
140    /// ```
141    pub fn expect_page<'a>(&self, page: &'a Page) -> SoftPageAssertions<'a> {
142        SoftPageAssertions {
143            assertions: PageAssertions::new(page),
144            errors: self.errors.clone(),
145        }
146    }
147
148    /// Check if all soft assertions passed.
149    pub fn passed(&self) -> bool {
150        self.errors.lock().unwrap().is_empty()
151    }
152
153    /// Get all collected errors.
154    pub fn errors(&self) -> Vec<SoftAssertionError> {
155        self.errors.lock().unwrap().clone()
156    }
157
158    /// Get the number of failed assertions.
159    pub fn failure_count(&self) -> usize {
160        self.errors.lock().unwrap().len()
161    }
162
163    /// Clear all collected errors.
164    pub fn clear(&self) {
165        self.errors.lock().unwrap().clear();
166    }
167
168    /// Assert that all soft assertions passed.
169    ///
170    /// This will fail with a combined error message if any assertions failed.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error containing all assertion failures if any occurred.
175    pub fn assert_all(&self) -> Result<(), TestError> {
176        let errors = self.errors.lock().unwrap();
177        if errors.is_empty() {
178            return Ok(());
179        }
180
181        let mut message = format!("{} soft assertion(s) failed:", errors.len());
182        for (i, error) in errors.iter().enumerate() {
183            message.push_str(&format!("\n{}. {}", i + 1, error));
184        }
185
186        Err(TestError::Assertion(crate::error::AssertionError::new(
187            message,
188            format!("{} assertions to pass", errors.len()),
189            format!("{} assertions failed", errors.len()),
190        )))
191    }
192
193    /// Add an error to the collection.
194    pub fn add_error(&self, error: SoftAssertionError) {
195        self.errors.lock().unwrap().push(error);
196    }
197}