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}