viewpoint_core/page/events/page_handlers/
mod.rs

1//! Page event handler methods.
2//!
3//! This module contains the `impl Page` block with methods for setting up
4//! event handlers (on_dialog, on_console, etc.) and waiting for events.
5
6use std::time::Duration;
7
8use super::super::Page;
9use super::super::console::ConsoleMessage;
10use super::super::dialog::Dialog;
11use super::super::download::Download;
12use super::super::file_chooser::FileChooser;
13use super::super::frame::Frame;
14use super::super::page_error::PageError as PageErrorInfo;
15use crate::error::{LocatorError, PageError};
16
17/// Default timeout for navigation and event waiting.
18const DEFAULT_NAVIGATION_TIMEOUT: Duration = Duration::from_secs(30);
19
20impl Page {
21    // =========================================================================
22    // Dialog Handling Methods
23    // =========================================================================
24
25    /// Set a handler for browser dialogs (alert, confirm, prompt, beforeunload).
26    ///
27    /// The handler will be called whenever a dialog appears. If no handler is
28    /// set, dialogs are automatically dismissed.
29    ///
30    /// # Example
31    ///
32    /// ```no_run
33    /// use viewpoint_core::Page;
34    /// use viewpoint_core::DialogType;
35    ///
36    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
37    /// // Accept all dialogs
38    /// page.on_dialog(|dialog| async move {
39    ///     println!("Dialog: {:?} - {}", dialog.type_(), dialog.message());
40    ///     dialog.accept().await
41    /// }).await;
42    ///
43    /// // Handle prompt with custom text
44    /// page.on_dialog(|dialog| async move {
45    ///     if matches!(dialog.type_(), DialogType::Prompt) {
46    ///         dialog.accept_with_text("my answer").await
47    ///     } else {
48    ///         dialog.accept().await
49    ///     }
50    /// }).await;
51    /// # Ok(())
52    /// # }
53    /// ```
54    pub async fn on_dialog<F, Fut>(&self, handler: F)
55    where
56        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
57        Fut: std::future::Future<Output = Result<(), PageError>> + Send + 'static,
58    {
59        self.event_manager.set_dialog_handler(handler).await;
60    }
61
62    /// Remove the dialog handler.
63    ///
64    /// After calling this, dialogs will be automatically dismissed.
65    pub async fn off_dialog(&self) {
66        self.event_manager.remove_dialog_handler().await;
67    }
68
69    // =========================================================================
70    // Console Event Methods
71    // =========================================================================
72
73    /// Set a handler for console messages (console.log, console.error, etc.).
74    ///
75    /// The handler will be called whenever JavaScript code logs to the console.
76    ///
77    /// # Example
78    ///
79    /// ```no_run
80    /// use viewpoint_core::Page;
81    /// use viewpoint_core::page::console::ConsoleMessageType;
82    ///
83    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
84    /// page.on_console(|message| async move {
85    ///     println!("[{:?}] {}", message.type_(), message.text());
86    /// }).await;
87    ///
88    /// // Filter by message type
89    /// page.on_console(|message| async move {
90    ///     if matches!(message.type_(), ConsoleMessageType::Error) {
91    ///         eprintln!("Console error: {}", message.text());
92    ///     }
93    /// }).await;
94    /// # Ok(())
95    /// # }
96    /// ```
97    pub async fn on_console<F, Fut>(&self, handler: F)
98    where
99        F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
100        Fut: std::future::Future<Output = ()> + Send + 'static,
101    {
102        self.event_manager.set_console_handler(handler).await;
103    }
104
105    /// Remove the console message handler.
106    pub async fn off_console(&self) {
107        self.event_manager.remove_console_handler().await;
108    }
109
110    // =========================================================================
111    // Page Error Event Methods
112    // =========================================================================
113
114    /// Set a handler for page errors (uncaught exceptions).
115    ///
116    /// The handler will be called whenever an uncaught JavaScript exception occurs.
117    ///
118    /// # Example
119    ///
120    /// ```no_run
121    /// use viewpoint_core::Page;
122    ///
123    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
124    /// page.on_pageerror(|error| async move {
125    ///     eprintln!("Page error: {}", error.message());
126    ///     if let Some(stack) = error.stack() {
127    ///         eprintln!("Stack trace:\n{}", stack);
128    ///     }
129    /// }).await;
130    /// # Ok(())
131    /// # }
132    /// ```
133    pub async fn on_pageerror<F, Fut>(&self, handler: F)
134    where
135        F: Fn(PageErrorInfo) -> Fut + Send + Sync + 'static,
136        Fut: std::future::Future<Output = ()> + Send + 'static,
137    {
138        self.event_manager.set_pageerror_handler(handler).await;
139    }
140
141    /// Remove the page error handler.
142    pub async fn off_pageerror(&self) {
143        self.event_manager.remove_pageerror_handler().await;
144    }
145
146    // =========================================================================
147    // Frame Event Methods
148    // =========================================================================
149
150    /// Set a handler for frame attached events.
151    ///
152    /// The handler will be called whenever a new frame is attached to the page,
153    /// typically when an `<iframe>` is added to the DOM.
154    ///
155    /// # Example
156    ///
157    /// ```no_run
158    /// use viewpoint_core::Page;
159    ///
160    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
161    /// page.on_frameattached(|frame| async move {
162    ///     println!("Frame attached: {}", frame.url());
163    /// }).await;
164    /// # Ok(())
165    /// # }
166    /// ```
167    pub async fn on_frameattached<F, Fut>(&self, handler: F)
168    where
169        F: Fn(Frame) -> Fut + Send + Sync + 'static,
170        Fut: std::future::Future<Output = ()> + Send + 'static,
171    {
172        self.event_manager.set_frameattached_handler(handler).await;
173    }
174
175    /// Remove the frame attached handler.
176    pub async fn off_frameattached(&self) {
177        self.event_manager.remove_frameattached_handler().await;
178    }
179
180    /// Set a handler for frame navigated events.
181    ///
182    /// The handler will be called whenever a frame navigates to a new URL.
183    ///
184    /// # Example
185    ///
186    /// ```no_run
187    /// use viewpoint_core::Page;
188    ///
189    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
190    /// page.on_framenavigated(|frame| async move {
191    ///     println!("Frame navigated to: {}", frame.url());
192    /// }).await;
193    /// # Ok(())
194    /// # }
195    /// ```
196    pub async fn on_framenavigated<F, Fut>(&self, handler: F)
197    where
198        F: Fn(Frame) -> Fut + Send + Sync + 'static,
199        Fut: std::future::Future<Output = ()> + Send + 'static,
200    {
201        self.event_manager.set_framenavigated_handler(handler).await;
202    }
203
204    /// Remove the frame navigated handler.
205    pub async fn off_framenavigated(&self) {
206        self.event_manager.remove_framenavigated_handler().await;
207    }
208
209    /// Set a handler for frame detached events.
210    ///
211    /// The handler will be called whenever a frame is detached from the page,
212    /// typically when an `<iframe>` is removed from the DOM.
213    ///
214    /// # Example
215    ///
216    /// ```no_run
217    /// use viewpoint_core::Page;
218    ///
219    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
220    /// page.on_framedetached(|frame| async move {
221    ///     println!("Frame detached: {}", frame.id());
222    /// }).await;
223    /// # Ok(())
224    /// # }
225    /// ```
226    pub async fn on_framedetached<F, Fut>(&self, handler: F)
227    where
228        F: Fn(Frame) -> Fut + Send + Sync + 'static,
229        Fut: std::future::Future<Output = ()> + Send + 'static,
230    {
231        self.event_manager.set_framedetached_handler(handler).await;
232    }
233
234    /// Remove the frame detached handler.
235    pub async fn off_framedetached(&self) {
236        self.event_manager.remove_framedetached_handler().await;
237    }
238
239    // =========================================================================
240    // Expect Methods (Wait for events triggered by actions)
241    // =========================================================================
242
243    /// Wait for a console message triggered by an action.
244    ///
245    /// # Example
246    ///
247    /// ```no_run
248    /// use viewpoint_core::Page;
249    ///
250    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
251    /// let message = page.expect_console(|| async {
252    ///     page.locator("#log-button").click().await?;
253    ///     Ok(())
254    /// }).await?;
255    ///
256    /// println!("Console message: {}", message.text());
257    /// # Ok(())
258    /// # }
259    /// ```
260    pub async fn expect_console<F, Fut>(&self, action: F) -> Result<ConsoleMessage, PageError>
261    where
262        F: FnOnce() -> Fut,
263        Fut: std::future::Future<Output = Result<(), LocatorError>>,
264    {
265        let timeout = DEFAULT_NAVIGATION_TIMEOUT;
266        let console_future = self.event_manager.wait_for_console(timeout);
267
268        // Perform the action
269        action()
270            .await
271            .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
272
273        // Wait for the console message
274        console_future.await
275    }
276
277    /// Wait for a page error triggered by an action.
278    ///
279    /// # Example
280    ///
281    /// ```no_run
282    /// use viewpoint_core::Page;
283    ///
284    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
285    /// let error = page.expect_pageerror(|| async {
286    ///     page.locator("#trigger-error").click().await?;
287    ///     Ok(())
288    /// }).await?;
289    ///
290    /// println!("Page error: {}", error.message());
291    /// # Ok(())
292    /// # }
293    /// ```
294    pub async fn expect_pageerror<F, Fut>(&self, action: F) -> Result<PageErrorInfo, PageError>
295    where
296        F: FnOnce() -> Fut,
297        Fut: std::future::Future<Output = Result<(), LocatorError>>,
298    {
299        let timeout = DEFAULT_NAVIGATION_TIMEOUT;
300        let pageerror_future = self.event_manager.wait_for_pageerror(timeout);
301
302        // Perform the action
303        action()
304            .await
305            .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
306
307        // Wait for the page error
308        pageerror_future.await
309    }
310
311    // =========================================================================
312    // Download Handling Methods
313    // =========================================================================
314
315    /// Set a handler for file downloads.
316    ///
317    /// The handler will be called whenever a download starts.
318    ///
319    /// # Example
320    ///
321    /// ```no_run
322    /// use viewpoint_core::Page;
323    ///
324    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
325    /// page.on_download(|mut download| async move {
326    ///     let path = download.path().await.unwrap();
327    ///     println!("Downloaded: {}", path.display());
328    /// }).await;
329    /// # Ok(())
330    /// # }
331    /// ```
332    pub async fn on_download<F, Fut>(&self, handler: F)
333    where
334        F: Fn(Download) -> Fut + Send + Sync + 'static,
335        Fut: std::future::Future<Output = ()> + Send + 'static,
336    {
337        // Enable downloads first
338        let _ = self.event_manager.set_download_behavior(true).await;
339        self.event_manager.set_download_handler(handler).await;
340    }
341
342    /// Wait for a download triggered by an action.
343    ///
344    /// # Example
345    ///
346    /// ```no_run
347    /// use viewpoint_core::Page;
348    ///
349    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
350    /// let mut download = page.expect_download(|| async {
351    ///     page.locator("a.download").click().await?;
352    ///     Ok(())
353    /// }).await?;
354    ///
355    /// download.save_as("./my-file.pdf").await?;
356    /// # Ok(())
357    /// # }
358    /// ```
359    pub async fn expect_download<F, Fut>(&self, action: F) -> Result<Download, PageError>
360    where
361        F: FnOnce() -> Fut,
362        Fut: std::future::Future<Output = Result<(), LocatorError>>,
363    {
364        // Enable downloads first
365        self.event_manager.set_download_behavior(true).await?;
366
367        // Register the download waiter BEFORE performing the action
368        // This ensures we don't miss the download event due to race conditions
369        let timeout = DEFAULT_NAVIGATION_TIMEOUT;
370        let download_rx = self.event_manager.register_download_waiter().await;
371
372        // Perform the action that triggers the download
373        action()
374            .await
375            .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
376
377        // Now await the download with timeout
378        self.event_manager
379            .await_download_waiter(download_rx, timeout)
380            .await
381    }
382
383    // =========================================================================
384    // File Chooser Handling Methods
385    // =========================================================================
386
387    /// Set whether to intercept file chooser dialogs.
388    ///
389    /// When enabled, file chooser dialogs will be intercepted and the
390    /// `on_filechooser` handler will be called instead of showing the
391    /// native file picker.
392    pub async fn set_intercept_file_chooser(&self, enabled: bool) -> Result<(), PageError> {
393        self.event_manager.set_intercept_file_chooser(enabled).await
394    }
395
396    /// Set a handler for file chooser dialogs.
397    ///
398    /// You must call `set_intercept_file_chooser(true)` before using this.
399    ///
400    /// # Example
401    ///
402    /// ```no_run
403    /// use viewpoint_core::Page;
404    ///
405    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
406    /// page.set_intercept_file_chooser(true).await?;
407    /// page.on_filechooser(|chooser| async move {
408    ///     chooser.set_files(&["./upload.txt"]).await.unwrap();
409    /// }).await;
410    /// # Ok(())
411    /// # }
412    /// ```
413    pub async fn on_filechooser<F, Fut>(&self, handler: F)
414    where
415        F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
416        Fut: std::future::Future<Output = ()> + Send + 'static,
417    {
418        self.event_manager.set_file_chooser_handler(handler).await;
419    }
420
421    /// Wait for a file chooser triggered by an action.
422    ///
423    /// You must call `set_intercept_file_chooser(true)` before using this.
424    ///
425    /// # Example
426    ///
427    /// ```no_run
428    /// use viewpoint_core::Page;
429    ///
430    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
431    /// page.set_intercept_file_chooser(true).await?;
432    /// let chooser = page.expect_file_chooser(|| async {
433    ///     page.locator("input[type=file]").click().await?;
434    ///     Ok(())
435    /// }).await?;
436    ///
437    /// chooser.set_files(&["./upload.txt"]).await?;
438    /// # Ok(())
439    /// # }
440    /// ```
441    pub async fn expect_file_chooser<F, Fut>(&self, action: F) -> Result<FileChooser, PageError>
442    where
443        F: FnOnce() -> Fut,
444        Fut: std::future::Future<Output = Result<(), LocatorError>>,
445    {
446        let timeout = DEFAULT_NAVIGATION_TIMEOUT;
447        let chooser_future = self.event_manager.wait_for_file_chooser(timeout);
448
449        // Perform the action
450        action()
451            .await
452            .map_err(|e| PageError::EvaluationFailed(e.to_string()))?;
453
454        // Wait for the file chooser
455        chooser_future.await
456    }
457}