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}