viewpoint_test/expect/
page.rs

1//! Page assertions for testing page state.
2
3use std::time::Duration;
4
5use viewpoint_core::Page;
6
7use crate::error::AssertionError;
8
9/// Default timeout for assertions.
10const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
11
12/// Assertions for pages.
13pub struct PageAssertions<'a> {
14    page: &'a Page,
15    timeout: Duration,
16    is_negated: bool,
17}
18
19impl<'a> PageAssertions<'a> {
20    /// Create a new `PageAssertions` for the given page.
21    pub fn new(page: &'a Page) -> Self {
22        Self {
23            page,
24            timeout: DEFAULT_TIMEOUT,
25            is_negated: false,
26        }
27    }
28
29    /// Set the timeout for this assertion.
30    #[must_use]
31    pub fn timeout(mut self, timeout: Duration) -> Self {
32        self.timeout = timeout;
33        self
34    }
35
36    /// Negate the assertion.
37    ///
38    /// This is an alias for the `not` method to avoid conflict with `std::ops::Not`.
39    #[must_use]
40    pub fn negated(mut self) -> Self {
41        self.is_negated = !self.is_negated;
42        self
43    }
44
45    /// Negate the assertion.
46    ///
47    /// Note: This method name shadows the `Not` trait's method. Use `negated()` if
48    /// you need to avoid this conflict.
49    #[must_use]
50    #[allow(clippy::should_implement_trait)]
51    pub fn not(self) -> Self {
52        self.negated()
53    }
54
55    /// Assert that the page has the specified URL.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the assertion fails or the URL cannot be retrieved.
60    pub async fn to_have_url(&self, expected: &str) -> Result<(), AssertionError> {
61        let start = std::time::Instant::now();
62
63        loop {
64            let url =
65                self.page.url().await.map_err(|e| {
66                    AssertionError::new("Failed to get URL", expected, e.to_string())
67                })?;
68
69            let matches = url == expected;
70            let expected_match = !self.is_negated;
71
72            if matches == expected_match {
73                return Ok(());
74            }
75
76            if start.elapsed() >= self.timeout {
77                return Err(AssertionError::new(
78                    if self.is_negated {
79                        "Page should not have URL"
80                    } else {
81                        "Page should have URL"
82                    },
83                    if self.is_negated {
84                        format!("not \"{expected}\"")
85                    } else {
86                        expected.to_string()
87                    },
88                    url,
89                ));
90            }
91
92            tokio::time::sleep(Duration::from_millis(100)).await;
93        }
94    }
95
96    /// Assert that the page URL contains the specified substring.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the assertion fails or the URL cannot be retrieved.
101    pub async fn to_have_url_containing(&self, expected: &str) -> Result<(), AssertionError> {
102        let start = std::time::Instant::now();
103
104        loop {
105            let url =
106                self.page.url().await.map_err(|e| {
107                    AssertionError::new("Failed to get URL", expected, e.to_string())
108                })?;
109
110            let contains = url.contains(expected);
111            let expected_match = !self.is_negated;
112
113            if contains == expected_match {
114                return Ok(());
115            }
116
117            if start.elapsed() >= self.timeout {
118                return Err(AssertionError::new(
119                    if self.is_negated {
120                        "Page URL should not contain"
121                    } else {
122                        "Page URL should contain"
123                    },
124                    if self.is_negated {
125                        format!("not containing \"{expected}\"")
126                    } else {
127                        format!("containing \"{expected}\"")
128                    },
129                    url,
130                ));
131            }
132
133            tokio::time::sleep(Duration::from_millis(100)).await;
134        }
135    }
136
137    /// Assert that the page has the specified title.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the assertion fails or the title cannot be retrieved.
142    pub async fn to_have_title(&self, expected: &str) -> Result<(), AssertionError> {
143        let start = std::time::Instant::now();
144
145        loop {
146            let title =
147                self.page.title().await.map_err(|e| {
148                    AssertionError::new("Failed to get title", expected, e.to_string())
149                })?;
150
151            let matches = title == expected;
152            let expected_match = !self.is_negated;
153
154            if matches == expected_match {
155                return Ok(());
156            }
157
158            if start.elapsed() >= self.timeout {
159                return Err(AssertionError::new(
160                    if self.is_negated {
161                        "Page should not have title"
162                    } else {
163                        "Page should have title"
164                    },
165                    if self.is_negated {
166                        format!("not \"{expected}\"")
167                    } else {
168                        expected.to_string()
169                    },
170                    title,
171                ));
172            }
173
174            tokio::time::sleep(Duration::from_millis(100)).await;
175        }
176    }
177
178    /// Assert that the page title contains the specified substring.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if the assertion fails or the title cannot be retrieved.
183    pub async fn to_have_title_containing(&self, expected: &str) -> Result<(), AssertionError> {
184        let start = std::time::Instant::now();
185
186        loop {
187            let title =
188                self.page.title().await.map_err(|e| {
189                    AssertionError::new("Failed to get title", expected, e.to_string())
190                })?;
191
192            let contains = title.contains(expected);
193            let expected_match = !self.is_negated;
194
195            if contains == expected_match {
196                return Ok(());
197            }
198
199            if start.elapsed() >= self.timeout {
200                return Err(AssertionError::new(
201                    if self.is_negated {
202                        "Page title should not contain"
203                    } else {
204                        "Page title should contain"
205                    },
206                    if self.is_negated {
207                        format!("not containing \"{expected}\"")
208                    } else {
209                        format!("containing \"{expected}\"")
210                    },
211                    title,
212                ));
213            }
214
215            tokio::time::sleep(Duration::from_millis(100)).await;
216        }
217    }
218}