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}