1use std::time::Duration;
4
5use viewpoint_core::Locator;
6
7use super::count::CountAssertions;
8use super::state::StateAssertions;
9use super::text::TextAssertions;
10use crate::error::AssertionError;
11
12const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
14
15pub struct LocatorAssertions<'a> {
17 locator: &'a Locator<'a>,
18 timeout: Duration,
19 is_negated: bool,
20}
21
22impl<'a> LocatorAssertions<'a> {
23 pub fn new(locator: &'a Locator<'a>) -> Self {
25 Self {
26 locator,
27 timeout: DEFAULT_TIMEOUT,
28 is_negated: false,
29 }
30 }
31
32 #[must_use]
34 pub fn timeout(mut self, timeout: Duration) -> Self {
35 self.timeout = timeout;
36 self
37 }
38
39 #[must_use]
43 pub fn negated(mut self) -> Self {
44 self.is_negated = !self.is_negated;
45 self
46 }
47
48 #[must_use]
53 #[allow(clippy::should_implement_trait)]
54 pub fn not(self) -> Self {
55 self.negated()
56 }
57
58 pub async fn to_be_visible(&self) -> Result<(), AssertionError> {
64 StateAssertions::new(self.locator, self.timeout, self.is_negated)
65 .to_be_visible()
66 .await
67 }
68
69 pub async fn to_be_hidden(&self) -> Result<(), AssertionError> {
75 StateAssertions::new(self.locator, self.timeout, self.is_negated)
76 .to_be_hidden()
77 .await
78 }
79
80 pub async fn to_have_text(&self, expected: &str) -> Result<(), AssertionError> {
86 TextAssertions::new(self.locator, self.timeout, self.is_negated)
87 .to_have_text(expected)
88 .await
89 }
90
91 pub async fn to_contain_text(&self, expected: &str) -> Result<(), AssertionError> {
97 TextAssertions::new(self.locator, self.timeout, self.is_negated)
98 .to_contain_text(expected)
99 .await
100 }
101
102 pub async fn to_have_attribute(&self, name: &str, value: &str) -> Result<(), AssertionError> {
108 let start = std::time::Instant::now();
109
110 loop {
111 let actual = self.get_attribute(name).await?;
112 let matches = actual.as_deref() == Some(value);
113 let expected_match = !self.is_negated;
114
115 if matches == expected_match {
116 return Ok(());
117 }
118
119 if start.elapsed() >= self.timeout {
120 return Err(AssertionError::new(
121 if self.is_negated {
122 format!("Element should not have attribute {name}=\"{value}\"")
123 } else {
124 format!("Element should have attribute {name}=\"{value}\"")
125 },
126 if self.is_negated {
127 format!("not {name}=\"{value}\"")
128 } else {
129 format!("{name}=\"{value}\"")
130 },
131 match actual {
132 Some(v) => format!("{name}=\"{v}\""),
133 None => format!("{name} not present"),
134 },
135 ));
136 }
137
138 tokio::time::sleep(Duration::from_millis(100)).await;
139 }
140 }
141
142 pub async fn to_have_class(&self, class_name: &str) -> Result<(), AssertionError> {
148 let start = std::time::Instant::now();
149
150 loop {
151 let class_attr = self.get_attribute("class").await?;
152 let classes = class_attr.as_deref().unwrap_or("");
153 let has_class = classes.split_whitespace().any(|c| c == class_name);
154 let expected_match = !self.is_negated;
155
156 if has_class == expected_match {
157 return Ok(());
158 }
159
160 if start.elapsed() >= self.timeout {
161 return Err(AssertionError::new(
162 if self.is_negated {
163 format!("Element should not have class \"{class_name}\"")
164 } else {
165 format!("Element should have class \"{class_name}\"")
166 },
167 if self.is_negated {
168 format!("not containing class \"{class_name}\"")
169 } else {
170 format!("class \"{class_name}\"")
171 },
172 format!("classes: \"{classes}\""),
173 ));
174 }
175
176 tokio::time::sleep(Duration::from_millis(100)).await;
177 }
178 }
179
180 pub async fn to_be_enabled(&self) -> Result<(), AssertionError> {
186 StateAssertions::new(self.locator, self.timeout, self.is_negated)
187 .to_be_enabled()
188 .await
189 }
190
191 pub async fn to_be_disabled(&self) -> Result<(), AssertionError> {
197 StateAssertions::new(self.locator, self.timeout, self.is_negated)
198 .to_be_disabled()
199 .await
200 }
201
202 pub async fn to_be_checked(&self) -> Result<(), AssertionError> {
208 StateAssertions::new(self.locator, self.timeout, self.is_negated)
209 .to_be_checked()
210 .await
211 }
212
213 pub async fn to_have_value(&self, expected: &str) -> Result<(), AssertionError> {
219 let start = std::time::Instant::now();
220
221 loop {
222 let actual = self.get_input_value().await?;
223 let matches = actual == expected;
224 let expected_match = !self.is_negated;
225
226 if matches == expected_match {
227 return Ok(());
228 }
229
230 if start.elapsed() >= self.timeout {
231 return Err(AssertionError::new(
232 if self.is_negated {
233 "Element should not have value"
234 } else {
235 "Element should have value"
236 },
237 if self.is_negated {
238 format!("not \"{expected}\"")
239 } else {
240 format!("\"{expected}\"")
241 },
242 format!("\"{actual}\""),
243 ));
244 }
245
246 tokio::time::sleep(Duration::from_millis(100)).await;
247 }
248 }
249
250 pub async fn to_have_values(&self, expected: &[&str]) -> Result<(), AssertionError> {
256 let start = std::time::Instant::now();
257
258 loop {
259 let actual = self.get_selected_values().await?;
260 let expected_set: std::collections::HashSet<&str> = expected.iter().copied().collect();
261 let actual_set: std::collections::HashSet<&str> = actual.iter().map(std::string::String::as_str).collect();
262 let matches = expected_set == actual_set;
263 let expected_match = !self.is_negated;
264
265 if matches == expected_match {
266 return Ok(());
267 }
268
269 if start.elapsed() >= self.timeout {
270 return Err(AssertionError::new(
271 if self.is_negated {
272 "Element should not have values"
273 } else {
274 "Element should have values"
275 },
276 if self.is_negated {
277 format!("not {expected:?}")
278 } else {
279 format!("{expected:?}")
280 },
281 format!("{actual:?}"),
282 ));
283 }
284
285 tokio::time::sleep(Duration::from_millis(100)).await;
286 }
287 }
288
289 pub async fn to_have_id(&self, expected: &str) -> Result<(), AssertionError> {
295 self.to_have_attribute("id", expected).await
296 }
297
298 pub async fn to_have_count(&self, expected: usize) -> Result<(), AssertionError> {
304 CountAssertions::new(self.locator, self.timeout, self.is_negated)
305 .to_have_count(expected)
306 .await
307 }
308
309 pub async fn to_have_count_greater_than(&self, n: usize) -> Result<(), AssertionError> {
315 CountAssertions::new(self.locator, self.timeout, self.is_negated)
316 .to_have_count_greater_than(n)
317 .await
318 }
319
320 pub async fn to_have_count_less_than(&self, n: usize) -> Result<(), AssertionError> {
326 CountAssertions::new(self.locator, self.timeout, self.is_negated)
327 .to_have_count_less_than(n)
328 .await
329 }
330
331 pub async fn to_have_count_at_least(&self, n: usize) -> Result<(), AssertionError> {
337 CountAssertions::new(self.locator, self.timeout, self.is_negated)
338 .to_have_count_at_least(n)
339 .await
340 }
341
342 pub async fn to_have_count_at_most(&self, n: usize) -> Result<(), AssertionError> {
348 CountAssertions::new(self.locator, self.timeout, self.is_negated)
349 .to_have_count_at_most(n)
350 .await
351 }
352
353 pub async fn to_have_texts(&self, expected: &[&str]) -> Result<(), AssertionError> {
359 TextAssertions::new(self.locator, self.timeout, self.is_negated)
360 .to_have_texts(expected)
361 .await
362 }
363
364 pub async fn to_match_aria_snapshot(
374 &self,
375 expected: &viewpoint_core::AriaSnapshot,
376 ) -> Result<(), AssertionError> {
377 let start = std::time::Instant::now();
378
379 loop {
380 let actual = self
381 .locator
382 .aria_snapshot()
383 .await
384 .map_err(|e| AssertionError::new("Failed to get ARIA snapshot", "snapshot", e.to_string()))?;
385
386 let matches = actual.matches(expected);
387 let expected_match = !self.is_negated;
388
389 if matches == expected_match {
390 return Ok(());
391 }
392
393 if start.elapsed() >= self.timeout {
394 let diff = actual.diff(expected);
395 return Err(AssertionError::new(
396 if self.is_negated {
397 "ARIA snapshot should not match"
398 } else {
399 "ARIA snapshot should match"
400 },
401 expected.to_yaml(),
402 format!("{}\n\nDiff:\n{}", actual.to_yaml(), diff),
403 ));
404 }
405
406 tokio::time::sleep(Duration::from_millis(100)).await;
407 }
408 }
409
410 pub async fn to_match_aria_snapshot_yaml(&self, expected_yaml: &str) -> Result<(), AssertionError> {
420 let expected = viewpoint_core::AriaSnapshot::from_yaml(expected_yaml)
421 .map_err(|e| AssertionError::new("Failed to parse expected ARIA snapshot", expected_yaml, e.to_string()))?;
422 self.to_match_aria_snapshot(&expected).await
423 }
424
425 pub async fn to_contain_texts(&self, expected: &[&str]) -> Result<(), AssertionError> {
431 TextAssertions::new(self.locator, self.timeout, self.is_negated)
432 .to_contain_texts(expected)
433 .await
434 }
435
436 pub async fn to_have_classes(&self, expected_classes: &[&str]) -> Result<(), AssertionError> {
445 let start = std::time::Instant::now();
446
447 loop {
448 let class_attr = self.get_attribute("class").await?;
449 let actual_classes: std::collections::HashSet<&str> = class_attr
450 .as_deref()
451 .unwrap_or("")
452 .split_whitespace()
453 .collect();
454
455 let has_all = expected_classes.iter().all(|c| actual_classes.contains(c));
456 let expected_match = !self.is_negated;
457
458 if has_all == expected_match {
459 return Ok(());
460 }
461
462 if start.elapsed() >= self.timeout {
463 return Err(AssertionError::new(
464 if self.is_negated {
465 format!("Element should not have classes {expected_classes:?}")
466 } else {
467 format!("Element should have classes {expected_classes:?}")
468 },
469 format!("{expected_classes:?}"),
470 format!("{:?}", actual_classes.into_iter().collect::<Vec<_>>()),
471 ));
472 }
473
474 tokio::time::sleep(Duration::from_millis(100)).await;
475 }
476 }
477
478 async fn get_input_value(&self) -> Result<String, AssertionError> {
483 super::locator_helpers::get_input_value(self.locator).await
484 }
485
486 async fn get_selected_values(&self) -> Result<Vec<String>, AssertionError> {
487 super::locator_helpers::get_selected_values(self.locator).await
488 }
489
490 async fn get_attribute(&self, name: &str) -> Result<Option<String>, AssertionError> {
491 super::locator_helpers::get_attribute(self.locator, name).await
492 }
493}