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> =
262 actual.iter().map(std::string::String::as_str).collect();
263 let matches = expected_set == actual_set;
264 let expected_match = !self.is_negated;
265
266 if matches == expected_match {
267 return Ok(());
268 }
269
270 if start.elapsed() >= self.timeout {
271 return Err(AssertionError::new(
272 if self.is_negated {
273 "Element should not have values"
274 } else {
275 "Element should have values"
276 },
277 if self.is_negated {
278 format!("not {expected:?}")
279 } else {
280 format!("{expected:?}")
281 },
282 format!("{actual:?}"),
283 ));
284 }
285
286 tokio::time::sleep(Duration::from_millis(100)).await;
287 }
288 }
289
290 pub async fn to_have_id(&self, expected: &str) -> Result<(), AssertionError> {
296 self.to_have_attribute("id", expected).await
297 }
298
299 pub async fn to_have_count(&self, expected: usize) -> Result<(), AssertionError> {
305 CountAssertions::new(self.locator, self.timeout, self.is_negated)
306 .to_have_count(expected)
307 .await
308 }
309
310 pub async fn to_have_count_greater_than(&self, n: usize) -> Result<(), AssertionError> {
316 CountAssertions::new(self.locator, self.timeout, self.is_negated)
317 .to_have_count_greater_than(n)
318 .await
319 }
320
321 pub async fn to_have_count_less_than(&self, n: usize) -> Result<(), AssertionError> {
327 CountAssertions::new(self.locator, self.timeout, self.is_negated)
328 .to_have_count_less_than(n)
329 .await
330 }
331
332 pub async fn to_have_count_at_least(&self, n: usize) -> Result<(), AssertionError> {
338 CountAssertions::new(self.locator, self.timeout, self.is_negated)
339 .to_have_count_at_least(n)
340 .await
341 }
342
343 pub async fn to_have_count_at_most(&self, n: usize) -> Result<(), AssertionError> {
349 CountAssertions::new(self.locator, self.timeout, self.is_negated)
350 .to_have_count_at_most(n)
351 .await
352 }
353
354 pub async fn to_have_texts(&self, expected: &[&str]) -> Result<(), AssertionError> {
360 TextAssertions::new(self.locator, self.timeout, self.is_negated)
361 .to_have_texts(expected)
362 .await
363 }
364
365 pub async fn to_match_aria_snapshot(
375 &self,
376 expected: &viewpoint_core::AriaSnapshot,
377 ) -> Result<(), AssertionError> {
378 let start = std::time::Instant::now();
379
380 loop {
381 let actual = self.locator.aria_snapshot().await.map_err(|e| {
382 AssertionError::new("Failed to get ARIA snapshot", "snapshot", e.to_string())
383 })?;
384
385 let matches = actual.matches(expected);
386 let expected_match = !self.is_negated;
387
388 if matches == expected_match {
389 return Ok(());
390 }
391
392 if start.elapsed() >= self.timeout {
393 let diff = actual.diff(expected);
394 return Err(AssertionError::new(
395 if self.is_negated {
396 "ARIA snapshot should not match"
397 } else {
398 "ARIA snapshot should match"
399 },
400 expected.to_yaml(),
401 format!("{}\n\nDiff:\n{}", actual.to_yaml(), diff),
402 ));
403 }
404
405 tokio::time::sleep(Duration::from_millis(100)).await;
406 }
407 }
408
409 pub async fn to_match_aria_snapshot_yaml(
419 &self,
420 expected_yaml: &str,
421 ) -> Result<(), AssertionError> {
422 let expected = viewpoint_core::AriaSnapshot::from_yaml(expected_yaml).map_err(|e| {
423 AssertionError::new(
424 "Failed to parse expected ARIA snapshot",
425 expected_yaml,
426 e.to_string(),
427 )
428 })?;
429 self.to_match_aria_snapshot(&expected).await
430 }
431
432 pub async fn to_contain_texts(&self, expected: &[&str]) -> Result<(), AssertionError> {
438 TextAssertions::new(self.locator, self.timeout, self.is_negated)
439 .to_contain_texts(expected)
440 .await
441 }
442
443 pub async fn to_have_classes(&self, expected_classes: &[&str]) -> Result<(), AssertionError> {
452 let start = std::time::Instant::now();
453
454 loop {
455 let class_attr = self.get_attribute("class").await?;
456 let actual_classes: std::collections::HashSet<&str> = class_attr
457 .as_deref()
458 .unwrap_or("")
459 .split_whitespace()
460 .collect();
461
462 let has_all = expected_classes.iter().all(|c| actual_classes.contains(c));
463 let expected_match = !self.is_negated;
464
465 if has_all == expected_match {
466 return Ok(());
467 }
468
469 if start.elapsed() >= self.timeout {
470 return Err(AssertionError::new(
471 if self.is_negated {
472 format!("Element should not have classes {expected_classes:?}")
473 } else {
474 format!("Element should have classes {expected_classes:?}")
475 },
476 format!("{expected_classes:?}"),
477 format!("{:?}", actual_classes.into_iter().collect::<Vec<_>>()),
478 ));
479 }
480
481 tokio::time::sleep(Duration::from_millis(100)).await;
482 }
483 }
484
485 async fn get_input_value(&self) -> Result<String, AssertionError> {
490 super::locator_helpers::get_input_value(self.locator).await
491 }
492
493 async fn get_selected_values(&self) -> Result<Vec<String>, AssertionError> {
494 super::locator_helpers::get_selected_values(self.locator).await
495 }
496
497 async fn get_attribute(&self, name: &str) -> Result<Option<String>, AssertionError> {
498 super::locator_helpers::get_attribute(self.locator, name).await
499 }
500}