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 = self
65                .page
66                .url()
67                .await
68                .map_err(|e| AssertionError::new("Failed to get URL", expected, e.to_string()))?;
69
70            let matches = url == expected;
71            let expected_match = !self.is_negated;
72
73            if matches == expected_match {
74                return Ok(());
75            }
76
77            if start.elapsed() >= self.timeout {
78                return Err(AssertionError::new(
79                    if self.is_negated {
80                        "Page should not have URL"
81                    } else {
82                        "Page should have URL"
83                    },
84                    if self.is_negated {
85                        format!("not \"{expected}\"")
86                    } else {
87                        expected.to_string()
88                    },
89                    url,
90                ));
91            }
92
93            tokio::time::sleep(Duration::from_millis(100)).await;
94        }
95    }
96
97    /// Assert that the page URL contains the specified substring.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if the assertion fails or the URL cannot be retrieved.
102    pub async fn to_have_url_containing(&self, expected: &str) -> Result<(), AssertionError> {
103        let start = std::time::Instant::now();
104
105        loop {
106            let url = self
107                .page
108                .url()
109                .await
110                .map_err(|e| AssertionError::new("Failed to get URL", expected, e.to_string()))?;
111
112            let contains = url.contains(expected);
113            let expected_match = !self.is_negated;
114
115            if contains == expected_match {
116                return Ok(());
117            }
118
119            if start.elapsed() >= self.timeout {
120                return Err(AssertionError::new(
121                    if self.is_negated {
122                        "Page URL should not contain"
123                    } else {
124                        "Page URL should contain"
125                    },
126                    if self.is_negated {
127                        format!("not containing \"{expected}\"")
128                    } else {
129                        format!("containing \"{expected}\"")
130                    },
131                    url,
132                ));
133            }
134
135            tokio::time::sleep(Duration::from_millis(100)).await;
136        }
137    }
138
139    /// Assert that the page has the specified title.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the assertion fails or the title cannot be retrieved.
144    pub async fn to_have_title(&self, expected: &str) -> Result<(), AssertionError> {
145        let start = std::time::Instant::now();
146
147        loop {
148            let title = self
149                .page
150                .title()
151                .await
152                .map_err(|e| AssertionError::new("Failed to get title", expected, e.to_string()))?;
153
154            let matches = title == expected;
155            let expected_match = !self.is_negated;
156
157            if matches == expected_match {
158                return Ok(());
159            }
160
161            if start.elapsed() >= self.timeout {
162                return Err(AssertionError::new(
163                    if self.is_negated {
164                        "Page should not have title"
165                    } else {
166                        "Page should have title"
167                    },
168                    if self.is_negated {
169                        format!("not \"{expected}\"")
170                    } else {
171                        expected.to_string()
172                    },
173                    title,
174                ));
175            }
176
177            tokio::time::sleep(Duration::from_millis(100)).await;
178        }
179    }
180
181    /// Assert that the page title contains the specified substring.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the assertion fails or the title cannot be retrieved.
186    pub async fn to_have_title_containing(&self, expected: &str) -> Result<(), AssertionError> {
187        let start = std::time::Instant::now();
188
189        loop {
190            let title = self
191                .page
192                .title()
193                .await
194                .map_err(|e| AssertionError::new("Failed to get title", expected, e.to_string()))?;
195
196            let contains = title.contains(expected);
197            let expected_match = !self.is_negated;
198
199            if contains == expected_match {
200                return Ok(());
201            }
202
203            if start.elapsed() >= self.timeout {
204                return Err(AssertionError::new(
205                    if self.is_negated {
206                        "Page title should not contain"
207                    } else {
208                        "Page title should contain"
209                    },
210                    if self.is_negated {
211                        format!("not containing \"{expected}\"")
212                    } else {
213                        format!("containing \"{expected}\"")
214                    },
215                    title,
216                ));
217            }
218
219            tokio::time::sleep(Duration::from_millis(100)).await;
220        }
221    }
222}