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