playwright_core/assertions.rs
1// Assertions - Auto-retry assertions for testing
2//
3// Provides expect() API with auto-retry logic matching Playwright's assertions.
4//
5// See: https://playwright.dev/docs/test-assertions
6
7use crate::error::Result;
8use crate::protocol::Locator;
9use std::time::Duration;
10
11/// Default timeout for assertions (5 seconds, matching Playwright)
12const DEFAULT_ASSERTION_TIMEOUT: Duration = Duration::from_secs(5);
13
14/// Default polling interval for assertions (100ms)
15const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(100);
16
17/// Creates an expectation for a locator with auto-retry behavior.
18///
19/// Assertions will retry until they pass or timeout (default: 5 seconds).
20///
21/// # Example
22///
23/// ```ignore
24/// use playwright_core::{expect, protocol::Playwright};
25/// use std::time::Duration;
26///
27/// #[tokio::main]
28/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
29/// let playwright = Playwright::launch().await?;
30/// let browser = playwright.chromium().launch().await?;
31/// let page = browser.new_page().await?;
32///
33/// // Test to_be_visible and to_be_hidden
34/// page.goto("data:text/html,<button id='btn'>Click me</button><div id='hidden' style='display:none'>Hidden</div>", None).await?;
35/// expect(page.locator("#btn").await).to_be_visible().await?;
36/// expect(page.locator("#hidden").await).to_be_hidden().await?;
37///
38/// // Test not() negation
39/// expect(page.locator("#btn").await).not().to_be_hidden().await?;
40/// expect(page.locator("#hidden").await).not().to_be_visible().await?;
41///
42/// // Test with_timeout()
43/// page.goto("data:text/html,<div id='element'>Visible</div>", None).await?;
44/// expect(page.locator("#element").await)
45/// .with_timeout(Duration::from_secs(10))
46/// .to_be_visible()
47/// .await?;
48///
49/// // Test to_be_enabled and to_be_disabled
50/// page.goto("data:text/html,<button id='enabled'>Enabled</button><button id='disabled' disabled>Disabled</button>", None).await?;
51/// expect(page.locator("#enabled").await).to_be_enabled().await?;
52/// expect(page.locator("#disabled").await).to_be_disabled().await?;
53///
54/// // Test to_be_checked and to_be_unchecked
55/// page.goto("data:text/html,<input type='checkbox' id='checked' checked><input type='checkbox' id='unchecked'>", None).await?;
56/// expect(page.locator("#checked").await).to_be_checked().await?;
57/// expect(page.locator("#unchecked").await).to_be_unchecked().await?;
58///
59/// // Test to_be_editable
60/// page.goto("data:text/html,<input type='text' id='editable'>", None).await?;
61/// expect(page.locator("#editable").await).to_be_editable().await?;
62///
63/// // Test to_be_focused
64/// page.goto("data:text/html,<input type='text' id='input'>", None).await?;
65/// page.evaluate("document.getElementById('input').focus()").await?;
66/// expect(page.locator("#input").await).to_be_focused().await?;
67///
68/// // Test to_contain_text
69/// page.goto("data:text/html,<div id='content'>Hello World</div>", None).await?;
70/// expect(page.locator("#content").await).to_contain_text("Hello").await?;
71/// expect(page.locator("#content").await).to_contain_text("World").await?;
72///
73/// // Test to_have_text
74/// expect(page.locator("#content").await).to_have_text("Hello World").await?;
75///
76/// // Test to_have_value
77/// page.goto("data:text/html,<input type='text' id='input' value='test value'>", None).await?;
78/// expect(page.locator("#input").await).to_have_value("test value").await?;
79///
80/// browser.close().await?;
81/// Ok(())
82/// }
83/// ```
84///
85/// See: <https://playwright.dev/docs/test-assertions>
86pub fn expect(locator: Locator) -> Expectation {
87 Expectation::new(locator)
88}
89
90/// Expectation wraps a locator and provides assertion methods with auto-retry.
91pub struct Expectation {
92 locator: Locator,
93 timeout: Duration,
94 poll_interval: Duration,
95 negate: bool,
96}
97
98impl Expectation {
99 /// Creates a new expectation for the given locator.
100 pub(crate) fn new(locator: Locator) -> Self {
101 Self {
102 locator,
103 timeout: DEFAULT_ASSERTION_TIMEOUT,
104 poll_interval: DEFAULT_POLL_INTERVAL,
105 negate: false,
106 }
107 }
108
109 /// Sets a custom timeout for this assertion.
110 ///
111 pub fn with_timeout(mut self, timeout: Duration) -> Self {
112 self.timeout = timeout;
113 self
114 }
115
116 /// Sets a custom poll interval for this assertion.
117 ///
118 /// Default is 100ms.
119 pub fn with_poll_interval(mut self, interval: Duration) -> Self {
120 self.poll_interval = interval;
121 self
122 }
123
124 /// Negates the assertion.
125 ///
126 /// Note: We intentionally use `.not()` method instead of implementing `std::ops::Not`
127 /// to match Playwright's API across all language bindings (JS/Python/Java/.NET).
128 #[allow(clippy::should_implement_trait)]
129 pub fn not(mut self) -> Self {
130 self.negate = true;
131 self
132 }
133
134 /// Asserts that the element is visible.
135 ///
136 /// This assertion will retry until the element becomes visible or timeout.
137 ///
138 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-visible>
139 pub async fn to_be_visible(self) -> Result<()> {
140 let start = std::time::Instant::now();
141 let selector = self.locator.selector().to_string();
142
143 loop {
144 let is_visible = self.locator.is_visible().await?;
145
146 // Check if condition matches (with negation support)
147 let matches = if self.negate { !is_visible } else { is_visible };
148
149 if matches {
150 return Ok(());
151 }
152
153 // Check timeout
154 if start.elapsed() >= self.timeout {
155 let message = if self.negate {
156 format!(
157 "Expected element '{}' NOT to be visible, but it was visible after {:?}",
158 selector, self.timeout
159 )
160 } else {
161 format!(
162 "Expected element '{}' to be visible, but it was not visible after {:?}",
163 selector, self.timeout
164 )
165 };
166 return Err(crate::error::Error::AssertionTimeout(message));
167 }
168
169 // Wait before next poll
170 tokio::time::sleep(self.poll_interval).await;
171 }
172 }
173
174 /// Asserts that the element is hidden (not visible).
175 ///
176 /// This assertion will retry until the element becomes hidden or timeout.
177 ///
178 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-hidden>
179 pub async fn to_be_hidden(self) -> Result<()> {
180 // to_be_hidden is the opposite of to_be_visible
181 // Use negation to reuse the visibility logic
182 let negated = Expectation {
183 negate: !self.negate, // Flip negation
184 ..self
185 };
186 negated.to_be_visible().await
187 }
188
189 /// Asserts that the element has the specified text content (exact match).
190 ///
191 /// This assertion will retry until the element has the exact text or timeout.
192 /// Text is trimmed before comparison.
193 ///
194 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-have-text>
195 pub async fn to_have_text(self, expected: &str) -> Result<()> {
196 let start = std::time::Instant::now();
197 let selector = self.locator.selector().to_string();
198 let expected = expected.trim();
199
200 loop {
201 // Get text content (using inner_text for consistency with Playwright)
202 let actual_text = self.locator.inner_text().await?;
203 let actual = actual_text.trim();
204
205 // Check if condition matches (with negation support)
206 let matches = if self.negate {
207 actual != expected
208 } else {
209 actual == expected
210 };
211
212 if matches {
213 return Ok(());
214 }
215
216 // Check timeout
217 if start.elapsed() >= self.timeout {
218 let message = if self.negate {
219 format!(
220 "Expected element '{}' NOT to have text '{}', but it did after {:?}",
221 selector, expected, self.timeout
222 )
223 } else {
224 format!(
225 "Expected element '{}' to have text '{}', but had '{}' after {:?}",
226 selector, expected, actual, self.timeout
227 )
228 };
229 return Err(crate::error::Error::AssertionTimeout(message));
230 }
231
232 // Wait before next poll
233 tokio::time::sleep(self.poll_interval).await;
234 }
235 }
236
237 /// Asserts that the element's text matches the specified regex pattern.
238 ///
239 /// This assertion will retry until the element's text matches the pattern or timeout.
240 pub async fn to_have_text_regex(self, pattern: &str) -> Result<()> {
241 let start = std::time::Instant::now();
242 let selector = self.locator.selector().to_string();
243 let re = regex::Regex::new(pattern)
244 .map_err(|e| crate::error::Error::InvalidArgument(format!("Invalid regex: {}", e)))?;
245
246 loop {
247 let actual_text = self.locator.inner_text().await?;
248 let actual = actual_text.trim();
249
250 // Check if condition matches (with negation support)
251 let matches = if self.negate {
252 !re.is_match(actual)
253 } else {
254 re.is_match(actual)
255 };
256
257 if matches {
258 return Ok(());
259 }
260
261 // Check timeout
262 if start.elapsed() >= self.timeout {
263 let message = if self.negate {
264 format!(
265 "Expected element '{}' NOT to match pattern '{}', but it did after {:?}",
266 selector, pattern, self.timeout
267 )
268 } else {
269 format!(
270 "Expected element '{}' to match pattern '{}', but had '{}' after {:?}",
271 selector, pattern, actual, self.timeout
272 )
273 };
274 return Err(crate::error::Error::AssertionTimeout(message));
275 }
276
277 // Wait before next poll
278 tokio::time::sleep(self.poll_interval).await;
279 }
280 }
281
282 /// Asserts that the element contains the specified text (substring match).
283 ///
284 /// This assertion will retry until the element contains the text or timeout.
285 ///
286 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-contain-text>
287 pub async fn to_contain_text(self, expected: &str) -> Result<()> {
288 let start = std::time::Instant::now();
289 let selector = self.locator.selector().to_string();
290
291 loop {
292 let actual_text = self.locator.inner_text().await?;
293 let actual = actual_text.trim();
294
295 // Check if condition matches (with negation support)
296 let matches = if self.negate {
297 !actual.contains(expected)
298 } else {
299 actual.contains(expected)
300 };
301
302 if matches {
303 return Ok(());
304 }
305
306 // Check timeout
307 if start.elapsed() >= self.timeout {
308 let message = if self.negate {
309 format!(
310 "Expected element '{}' NOT to contain text '{}', but it did after {:?}",
311 selector, expected, self.timeout
312 )
313 } else {
314 format!(
315 "Expected element '{}' to contain text '{}', but had '{}' after {:?}",
316 selector, expected, actual, self.timeout
317 )
318 };
319 return Err(crate::error::Error::AssertionTimeout(message));
320 }
321
322 // Wait before next poll
323 tokio::time::sleep(self.poll_interval).await;
324 }
325 }
326
327 /// Asserts that the element's text contains a substring matching the regex pattern.
328 ///
329 /// This assertion will retry until the element contains the pattern or timeout.
330 pub async fn to_contain_text_regex(self, pattern: &str) -> Result<()> {
331 let start = std::time::Instant::now();
332 let selector = self.locator.selector().to_string();
333 let re = regex::Regex::new(pattern)
334 .map_err(|e| crate::error::Error::InvalidArgument(format!("Invalid regex: {}", e)))?;
335
336 loop {
337 let actual_text = self.locator.inner_text().await?;
338 let actual = actual_text.trim();
339
340 // Check if condition matches (with negation support)
341 let matches = if self.negate {
342 !re.is_match(actual)
343 } else {
344 re.is_match(actual)
345 };
346
347 if matches {
348 return Ok(());
349 }
350
351 // Check timeout
352 if start.elapsed() >= self.timeout {
353 let message = if self.negate {
354 format!(
355 "Expected element '{}' NOT to contain pattern '{}', but it did after {:?}",
356 selector, pattern, self.timeout
357 )
358 } else {
359 format!(
360 "Expected element '{}' to contain pattern '{}', but had '{}' after {:?}",
361 selector, pattern, actual, self.timeout
362 )
363 };
364 return Err(crate::error::Error::AssertionTimeout(message));
365 }
366
367 // Wait before next poll
368 tokio::time::sleep(self.poll_interval).await;
369 }
370 }
371
372 /// Asserts that the input element has the specified value.
373 ///
374 /// This assertion will retry until the input has the exact value or timeout.
375 ///
376 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-have-value>
377 pub async fn to_have_value(self, expected: &str) -> Result<()> {
378 let start = std::time::Instant::now();
379 let selector = self.locator.selector().to_string();
380
381 loop {
382 let actual = self.locator.input_value(None).await?;
383
384 // Check if condition matches (with negation support)
385 let matches = if self.negate {
386 actual != expected
387 } else {
388 actual == expected
389 };
390
391 if matches {
392 return Ok(());
393 }
394
395 // Check timeout
396 if start.elapsed() >= self.timeout {
397 let message = if self.negate {
398 format!(
399 "Expected input '{}' NOT to have value '{}', but it did after {:?}",
400 selector, expected, self.timeout
401 )
402 } else {
403 format!(
404 "Expected input '{}' to have value '{}', but had '{}' after {:?}",
405 selector, expected, actual, self.timeout
406 )
407 };
408 return Err(crate::error::Error::AssertionTimeout(message));
409 }
410
411 // Wait before next poll
412 tokio::time::sleep(self.poll_interval).await;
413 }
414 }
415
416 /// Asserts that the input element's value matches the specified regex pattern.
417 ///
418 /// This assertion will retry until the input value matches the pattern or timeout.
419 pub async fn to_have_value_regex(self, pattern: &str) -> Result<()> {
420 let start = std::time::Instant::now();
421 let selector = self.locator.selector().to_string();
422 let re = regex::Regex::new(pattern)
423 .map_err(|e| crate::error::Error::InvalidArgument(format!("Invalid regex: {}", e)))?;
424
425 loop {
426 let actual = self.locator.input_value(None).await?;
427
428 // Check if condition matches (with negation support)
429 let matches = if self.negate {
430 !re.is_match(&actual)
431 } else {
432 re.is_match(&actual)
433 };
434
435 if matches {
436 return Ok(());
437 }
438
439 // Check timeout
440 if start.elapsed() >= self.timeout {
441 let message = if self.negate {
442 format!(
443 "Expected input '{}' NOT to match pattern '{}', but it did after {:?}",
444 selector, pattern, self.timeout
445 )
446 } else {
447 format!(
448 "Expected input '{}' to match pattern '{}', but had '{}' after {:?}",
449 selector, pattern, actual, self.timeout
450 )
451 };
452 return Err(crate::error::Error::AssertionTimeout(message));
453 }
454
455 // Wait before next poll
456 tokio::time::sleep(self.poll_interval).await;
457 }
458 }
459
460 /// Asserts that the element is enabled.
461 ///
462 /// This assertion will retry until the element is enabled or timeout.
463 /// An element is enabled if it does not have the "disabled" attribute.
464 ///
465 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-enabled>
466 pub async fn to_be_enabled(self) -> Result<()> {
467 let start = std::time::Instant::now();
468 let selector = self.locator.selector().to_string();
469
470 loop {
471 let is_enabled = self.locator.is_enabled().await?;
472
473 // Check if condition matches (with negation support)
474 let matches = if self.negate { !is_enabled } else { is_enabled };
475
476 if matches {
477 return Ok(());
478 }
479
480 // Check timeout
481 if start.elapsed() >= self.timeout {
482 let message = if self.negate {
483 format!(
484 "Expected element '{}' NOT to be enabled, but it was enabled after {:?}",
485 selector, self.timeout
486 )
487 } else {
488 format!(
489 "Expected element '{}' to be enabled, but it was not enabled after {:?}",
490 selector, self.timeout
491 )
492 };
493 return Err(crate::error::Error::AssertionTimeout(message));
494 }
495
496 // Wait before next poll
497 tokio::time::sleep(self.poll_interval).await;
498 }
499 }
500
501 /// Asserts that the element is disabled.
502 ///
503 /// This assertion will retry until the element is disabled or timeout.
504 /// An element is disabled if it has the "disabled" attribute.
505 ///
506 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-disabled>
507 pub async fn to_be_disabled(self) -> Result<()> {
508 // to_be_disabled is the opposite of to_be_enabled
509 // Use negation to reuse the enabled logic
510 let negated = Expectation {
511 negate: !self.negate, // Flip negation
512 ..self
513 };
514 negated.to_be_enabled().await
515 }
516
517 /// Asserts that the checkbox or radio button is checked.
518 ///
519 /// This assertion will retry until the element is checked or timeout.
520 ///
521 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-checked>
522 pub async fn to_be_checked(self) -> Result<()> {
523 let start = std::time::Instant::now();
524 let selector = self.locator.selector().to_string();
525
526 loop {
527 let is_checked = self.locator.is_checked().await?;
528
529 // Check if condition matches (with negation support)
530 let matches = if self.negate { !is_checked } else { is_checked };
531
532 if matches {
533 return Ok(());
534 }
535
536 // Check timeout
537 if start.elapsed() >= self.timeout {
538 let message = if self.negate {
539 format!(
540 "Expected element '{}' NOT to be checked, but it was checked after {:?}",
541 selector, self.timeout
542 )
543 } else {
544 format!(
545 "Expected element '{}' to be checked, but it was not checked after {:?}",
546 selector, self.timeout
547 )
548 };
549 return Err(crate::error::Error::AssertionTimeout(message));
550 }
551
552 // Wait before next poll
553 tokio::time::sleep(self.poll_interval).await;
554 }
555 }
556
557 /// Asserts that the checkbox or radio button is unchecked.
558 ///
559 /// This assertion will retry until the element is unchecked or timeout.
560 ///
561 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-checked>
562 pub async fn to_be_unchecked(self) -> Result<()> {
563 // to_be_unchecked is the opposite of to_be_checked
564 // Use negation to reuse the checked logic
565 let negated = Expectation {
566 negate: !self.negate, // Flip negation
567 ..self
568 };
569 negated.to_be_checked().await
570 }
571
572 /// Asserts that the element is editable.
573 ///
574 /// This assertion will retry until the element is editable or timeout.
575 /// An element is editable if it is enabled and does not have the "readonly" attribute.
576 ///
577 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-editable>
578 pub async fn to_be_editable(self) -> Result<()> {
579 let start = std::time::Instant::now();
580 let selector = self.locator.selector().to_string();
581
582 loop {
583 let is_editable = self.locator.is_editable().await?;
584
585 // Check if condition matches (with negation support)
586 let matches = if self.negate {
587 !is_editable
588 } else {
589 is_editable
590 };
591
592 if matches {
593 return Ok(());
594 }
595
596 // Check timeout
597 if start.elapsed() >= self.timeout {
598 let message = if self.negate {
599 format!(
600 "Expected element '{}' NOT to be editable, but it was editable after {:?}",
601 selector, self.timeout
602 )
603 } else {
604 format!(
605 "Expected element '{}' to be editable, but it was not editable after {:?}",
606 selector, self.timeout
607 )
608 };
609 return Err(crate::error::Error::AssertionTimeout(message));
610 }
611
612 // Wait before next poll
613 tokio::time::sleep(self.poll_interval).await;
614 }
615 }
616
617 /// Asserts that the element is focused (currently has focus).
618 ///
619 /// This assertion will retry until the element becomes focused or timeout.
620 ///
621 /// See: <https://playwright.dev/docs/test-assertions#locator-assertions-to-be-focused>
622 pub async fn to_be_focused(self) -> Result<()> {
623 let start = std::time::Instant::now();
624 let selector = self.locator.selector().to_string();
625
626 loop {
627 let is_focused = self.locator.is_focused().await?;
628
629 // Check if condition matches (with negation support)
630 let matches = if self.negate { !is_focused } else { is_focused };
631
632 if matches {
633 return Ok(());
634 }
635
636 // Check timeout
637 if start.elapsed() >= self.timeout {
638 let message = if self.negate {
639 format!(
640 "Expected element '{}' NOT to be focused, but it was focused after {:?}",
641 selector, self.timeout
642 )
643 } else {
644 format!(
645 "Expected element '{}' to be focused, but it was not focused after {:?}",
646 selector, self.timeout
647 )
648 };
649 return Err(crate::error::Error::AssertionTimeout(message));
650 }
651
652 // Wait before next poll
653 tokio::time::sleep(self.poll_interval).await;
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_expectation_defaults() {
664 // Verify default timeout and poll interval constants
665 assert_eq!(DEFAULT_ASSERTION_TIMEOUT, Duration::from_secs(5));
666 assert_eq!(DEFAULT_POLL_INTERVAL, Duration::from_millis(100));
667 }
668}