1use std::future::Future;
13use std::time::Duration;
14
15use crate::error::{Error, Result};
16use crate::locator::Locator;
17use crate::page::Page;
18
19const DEFAULT_TIMEOUT: Duration = Duration::from_millis(5000);
21const POLL_INTERVAL: Duration = Duration::from_millis(100);
23
24pub fn expect(locator: Locator) -> LocatorAssertions {
29 LocatorAssertions::new(locator)
30}
31
32pub fn expect_page(page: Page) -> PageAssertions {
36 PageAssertions::new(page)
37}
38
39async fn wait_for<F, Fut, Msg>(cond: F, timeout: Duration, negated: bool, msg: Msg) -> Result<()>
45where
46 F: Fn() -> Fut,
47 Fut: Future<Output = Result<bool>>,
48 Msg: FnOnce() -> String,
49{
50 let want = !negated;
51 let deadline = tokio::time::Instant::now() + timeout;
52 loop {
53 let got = cond().await?;
54 if got == want {
55 return Ok(());
56 }
57 if tokio::time::Instant::now() >= deadline {
58 return Err(Error::Timeout(msg()));
59 }
60 tokio::time::sleep(POLL_INTERVAL).await;
61 }
62}
63
64#[derive(Clone)]
70pub struct LocatorAssertions {
71 locator: Locator,
72 timeout: Duration,
73 negated: bool,
74}
75
76impl LocatorAssertions {
77 pub fn new(locator: Locator) -> Self {
79 Self {
80 locator,
81 timeout: DEFAULT_TIMEOUT,
82 negated: false,
83 }
84 }
85
86 pub fn with_timeout(mut self, timeout: Duration) -> Self {
88 self.timeout = timeout;
89 self
90 }
91
92 pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
94 self.timeout = timeout;
95 self
96 }
97
98 pub fn not(&self) -> Self {
100 let mut c = self.clone();
101 c.negated = true;
102 c
103 }
104
105 fn sel(&self) -> &str {
106 self.locator.selector()
107 }
108
109 pub async fn to_be_visible(&self) -> Result<()> {
112 let sel = self.sel().to_string();
113 let l = self.locator.clone();
114 let negated = self.negated;
115 let timeout = self.timeout;
116 wait_for(
117 move || {
118 let l = l.clone();
119 async move { l.is_visible().await }
120 },
121 timeout,
122 negated,
123 || format!("to_be_visible('{sel}') timed out after {}ms", timeout.as_millis()),
124 )
125 .await
126 }
127
128 pub async fn to_be_hidden(&self) -> Result<()> {
129 let sel = self.sel().to_string();
130 let l = self.locator.clone();
131 let negated = self.negated;
132 let timeout = self.timeout;
133 wait_for(
134 move || {
135 let l = l.clone();
136 async move { l.is_hidden().await }
137 },
138 timeout,
139 negated,
140 || format!("to_be_hidden('{sel}') timed out after {}ms", timeout.as_millis()),
141 )
142 .await
143 }
144
145 pub async fn to_be_enabled(&self) -> Result<()> {
146 let sel = self.sel().to_string();
147 let l = self.locator.clone();
148 let negated = self.negated;
149 let timeout = self.timeout;
150 wait_for(
151 move || {
152 let l = l.clone();
153 async move { l.is_enabled().await }
154 },
155 timeout,
156 negated,
157 || format!("to_be_enabled('{sel}') timed out after {}ms", timeout.as_millis()),
158 )
159 .await
160 }
161
162 pub async fn to_be_disabled(&self) -> Result<()> {
163 let sel = self.sel().to_string();
164 let l = self.locator.clone();
165 let negated = self.negated;
166 let timeout = self.timeout;
167 wait_for(
168 move || {
169 let l = l.clone();
170 async move { l.is_disabled().await }
171 },
172 timeout,
173 negated,
174 || format!("to_be_disabled('{sel}') timed out after {}ms", timeout.as_millis()),
175 )
176 .await
177 }
178
179 pub async fn to_be_editable(&self) -> Result<()> {
180 let sel = self.sel().to_string();
181 let l = self.locator.clone();
182 let negated = self.negated;
183 let timeout = self.timeout;
184 wait_for(
185 move || {
186 let l = l.clone();
187 async move { l.is_editable().await }
188 },
189 timeout,
190 negated,
191 || format!("to_be_editable('{sel}') timed out after {}ms", timeout.as_millis()),
192 )
193 .await
194 }
195
196 pub async fn to_be_checked(&self) -> Result<()> {
197 let sel = self.sel().to_string();
198 let l = self.locator.clone();
199 let negated = self.negated;
200 let timeout = self.timeout;
201 wait_for(
202 move || {
203 let l = l.clone();
204 async move { l.is_checked().await }
205 },
206 timeout,
207 negated,
208 || format!("to_be_checked('{sel}') timed out after {}ms", timeout.as_millis()),
209 )
210 .await
211 }
212
213 pub async fn to_be_unchecked(&self) -> Result<()> {
214 let sel = self.sel().to_string();
215 let l = self.locator.clone();
216 let negated = self.negated;
217 let timeout = self.timeout;
218 wait_for(
219 move || {
220 let l = l.clone();
221 async move {
222 Ok(!l.is_checked().await?)
224 }
225 },
226 timeout,
227 negated,
228 || format!("to_be_unchecked('{sel}') timed out after {}ms", timeout.as_millis()),
229 )
230 .await
231 }
232
233 pub async fn to_have_text(&self, expected: &str) -> Result<()> {
237 let sel = self.sel().to_string();
238 let l = self.locator.clone();
239 let negated = self.negated;
240 let timeout = self.timeout;
241 let expected_owned = expected.to_string();
242 let expected_for_msg = expected_owned.clone();
243 wait_for(
244 move || {
245 let l = l.clone();
246 let expected_owned = expected_owned.clone();
247 async move {
248 let actual = l.text_content().await?;
249 let actual = actual.unwrap_or_default();
250 Ok(actual.trim() == expected_owned.trim())
251 }
252 },
253 timeout,
254 negated,
255 move || {
256 format!(
257 "to_have_text('{sel}', '{expected_for_msg}') timed out after {}ms",
258 timeout.as_millis()
259 )
260 },
261 )
262 .await
263 }
264
265 pub async fn to_contain_text(&self, expected: &str) -> Result<()> {
267 let sel = self.sel().to_string();
268 let l = self.locator.clone();
269 let negated = self.negated;
270 let timeout = self.timeout;
271 let expected_owned = expected.to_string();
272 let expected_for_msg = expected_owned.clone();
273 wait_for(
274 move || {
275 let l = l.clone();
276 let expected_owned = expected_owned.clone();
277 async move {
278 let actual = l.text_content().await?;
279 Ok(actual
280 .unwrap_or_default()
281 .trim()
282 .contains(expected_owned.trim()))
283 }
284 },
285 timeout,
286 negated,
287 move || {
288 format!(
289 "to_contain_text('{sel}', '{expected_for_msg}') timed out after {}ms",
290 timeout.as_millis()
291 )
292 },
293 )
294 .await
295 }
296
297 pub async fn to_have_inner_text(&self, expected: &str) -> Result<()> {
299 let sel = self.sel().to_string();
300 let l = self.locator.clone();
301 let negated = self.negated;
302 let timeout = self.timeout;
303 let expected_owned = expected.to_string();
304 let expected_for_msg = expected_owned.clone();
305 wait_for(
306 move || {
307 let l = l.clone();
308 let expected_owned = expected_owned.clone();
309 async move {
310 let actual = l.inner_text().await?;
311 Ok(actual == expected_owned)
312 }
313 },
314 timeout,
315 negated,
316 move || {
317 format!(
318 "to_have_inner_text('{sel}', '{expected_for_msg}') timed out after {}ms",
319 timeout.as_millis()
320 )
321 },
322 )
323 .await
324 }
325
326 pub async fn to_have_count(&self, expected: usize) -> Result<()> {
330 let sel = self.sel().to_string();
331 let l = self.locator.clone();
332 let negated = self.negated;
333 let timeout = self.timeout;
334 wait_for(
335 move || {
336 let l = l.clone();
337 async move { Ok(l.count().await? == expected) }
338 },
339 timeout,
340 negated,
341 move || {
342 format!(
343 "to_have_count('{sel}', {expected}) timed out after {}ms",
344 timeout.as_millis()
345 )
346 },
347 )
348 .await
349 }
350
351 pub async fn to_have_attribute(&self, name: &str, value: &str) -> Result<()> {
355 let sel = self.sel().to_string();
356 let l = self.locator.clone();
357 let negated = self.negated;
358 let timeout = self.timeout;
359 let name_owned = name.to_string();
360 let value_owned = value.to_string();
361 let name_for_msg = name.to_string();
362 let value_for_msg = value.to_string();
363 wait_for(
364 move || {
365 let l = l.clone();
366 let name_owned = name_owned.clone();
367 let value_owned = value_owned.clone();
368 async move {
369 let attr = l.get_attribute(&name_owned).await?;
370 Ok(attr.as_deref() == Some(value_owned.as_str()))
371 }
372 },
373 timeout,
374 negated,
375 move || {
376 format!(
377 "to_have_attribute('{sel}', '{name_for_msg}', '{value_for_msg}') timed out after {}ms",
378 timeout.as_millis()
379 )
380 },
381 )
382 .await
383 }
384
385 pub async fn to_have_value(&self, expected: &str) -> Result<()> {
387 let sel = self.sel().to_string();
388 let l = self.locator.clone();
389 let negated = self.negated;
390 let timeout = self.timeout;
391 let expected_owned = expected.to_string();
392 let expected_for_msg = expected.to_string();
393 wait_for(
394 move || {
395 let l = l.clone();
396 let expected_owned = expected_owned.clone();
397 async move { Ok(l.input_value(None).await? == expected_owned) }
398 },
399 timeout,
400 negated,
401 move || {
402 format!(
403 "to_have_value('{sel}', '{expected_for_msg}') timed out after {}ms",
404 timeout.as_millis()
405 )
406 },
407 )
408 .await
409 }
410
411 pub async fn to_be_empty(&self) -> Result<()> {
414 let sel = self.sel().to_string();
415 let l = self.locator.clone();
416 let negated = self.negated;
417 let timeout = self.timeout;
418 wait_for(
419 move || {
420 let l = l.clone();
421 async move {
422 let count = l.count().await?;
423 if count == 0 {
424 return Ok(true);
425 }
426 let value = l.input_value(None).await.unwrap_or_default();
428 Ok(value.is_empty())
429 }
430 },
431 timeout,
432 negated,
433 move || format!("to_be_empty('{sel}') timed out after {}ms", timeout.as_millis()),
434 )
435 .await
436 }
437}
438
439#[derive(Clone)]
445pub struct PageAssertions {
446 page: Page,
447 timeout: Duration,
448 negated: bool,
449}
450
451impl PageAssertions {
452 pub fn new(page: Page) -> Self {
454 Self {
455 page,
456 timeout: DEFAULT_TIMEOUT,
457 negated: false,
458 }
459 }
460
461 pub fn with_timeout(mut self, timeout: Duration) -> Self {
463 self.timeout = timeout;
464 self
465 }
466
467 pub fn set_timeout(&mut self, timeout: Duration) -> &mut Self {
469 self.timeout = timeout;
470 self
471 }
472
473 pub fn not(&self) -> Self {
475 let mut c = self.clone();
476 c.negated = true;
477 c
478 }
479
480 pub async fn to_have_title(&self, expected: &str) -> Result<()> {
482 let p = self.page.clone();
483 let negated = self.negated;
484 let timeout = self.timeout;
485 let expected_owned = expected.to_string();
486 let expected_for_msg = expected.to_string();
487 wait_for(
488 move || {
489 let p = p.clone();
490 let expected_owned = expected_owned.clone();
491 async move { Ok(p.title().await? == expected_owned) }
492 },
493 timeout,
494 negated,
495 move || {
496 format!(
497 "to_have_title('{expected_for_msg}') timed out after {}ms",
498 timeout.as_millis()
499 )
500 },
501 )
502 .await
503 }
504
505 pub async fn to_have_url(&self, expected: &str) -> Result<()> {
507 let p = self.page.clone();
508 let negated = self.negated;
509 let timeout = self.timeout;
510 let expected_owned = expected.to_string();
511 let expected_for_msg = expected.to_string();
512 wait_for(
513 move || {
514 let p = p.clone();
515 let expected_owned = expected_owned.clone();
516 async move { Ok(p.url().await? == expected_owned) }
517 },
518 timeout,
519 negated,
520 move || {
521 format!(
522 "to_have_url('{expected_for_msg}') timed out after {}ms",
523 timeout.as_millis()
524 )
525 },
526 )
527 .await
528 }
529}