viewpoint_core/page/events/
mod.rs

1//! Page event handling for dialogs, downloads, file choosers, console, and errors.
2//!
3//! This module provides the event management infrastructure for page-level events.
4
5// Allow dead code for event scaffolding (various specs)
6
7mod download_handling;
8mod event_listener;
9mod page_handlers;
10mod types;
11
12use std::collections::HashMap;
13use std::future::Future;
14use std::path::PathBuf;
15use std::sync::Arc;
16use std::time::Duration;
17
18use tokio::sync::{oneshot, Mutex, RwLock};
19use tracing::{debug, warn};
20use viewpoint_cdp::CdpConnection;
21
22use super::console::ConsoleMessage;
23use super::dialog::Dialog;
24use super::download::Download;
25use super::file_chooser::FileChooser;
26use super::frame::Frame;
27use super::page_error::PageError as PageErrorInfo;
28use crate::error::PageError;
29
30pub use types::{
31    ConsoleHandler, DialogHandler, DownloadHandler, FileChooserHandler,
32    FrameAttachedHandler, FrameDetachedHandler, FrameNavigatedHandler, PageErrorHandler,
33};
34
35use download_handling::DownloadTracker;
36
37/// Page event manager for handling CDP events.
38pub struct PageEventManager {
39    /// CDP connection.
40    connection: Arc<CdpConnection>,
41    /// Session ID.
42    session_id: String,
43    /// Dialog handler.
44    dialog_handler: Arc<RwLock<Option<DialogHandler>>>,
45    /// Download handler.
46    download_handler: Arc<RwLock<Option<DownloadHandler>>>,
47    /// File chooser handler.
48    file_chooser_handler: Arc<RwLock<Option<FileChooserHandler>>>,
49    /// Console message handler.
50    console_handler: Arc<RwLock<Option<ConsoleHandler>>>,
51    /// Page error handler.
52    pageerror_handler: Arc<RwLock<Option<PageErrorHandler>>>,
53    /// Frame attached handler.
54    frameattached_handler: Arc<RwLock<Option<FrameAttachedHandler>>>,
55    /// Frame navigated handler.
56    framenavigated_handler: Arc<RwLock<Option<FrameNavigatedHandler>>>,
57    /// Frame detached handler.
58    framedetached_handler: Arc<RwLock<Option<FrameDetachedHandler>>>,
59    /// Active downloads.
60    downloads: Arc<Mutex<HashMap<String, DownloadTracker>>>,
61    /// Download directory.
62    download_dir: PathBuf,
63    /// Whether file chooser interception is enabled.
64    file_chooser_intercepted: Arc<RwLock<bool>>,
65    /// One-shot sender for wait_for_dialog.
66    wait_for_dialog_tx: Arc<Mutex<Option<oneshot::Sender<Dialog>>>>,
67    /// One-shot sender for wait_for_download.
68    wait_for_download_tx: Arc<Mutex<Option<oneshot::Sender<Download>>>>,
69    /// One-shot sender for wait_for_file_chooser.
70    wait_for_file_chooser_tx: Arc<Mutex<Option<oneshot::Sender<FileChooser>>>>,
71    /// One-shot sender for wait_for_console.
72    wait_for_console_tx: Arc<Mutex<Option<oneshot::Sender<ConsoleMessage>>>>,
73    /// One-shot sender for wait_for_pageerror.
74    wait_for_pageerror_tx: Arc<Mutex<Option<oneshot::Sender<PageErrorInfo>>>>,
75}
76
77// Manual Debug implementation since handlers don't implement Debug
78impl std::fmt::Debug for PageEventManager {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.debug_struct("PageEventManager")
81            .field("session_id", &self.session_id)
82            .field("download_dir", &self.download_dir)
83            .finish_non_exhaustive()
84    }
85}
86
87impl PageEventManager {
88    /// Create a new page event manager.
89    pub fn new(connection: Arc<CdpConnection>, session_id: String) -> Self {
90        let download_dir = std::env::temp_dir().join("viewpoint-downloads");
91        let manager = Self {
92            connection: connection.clone(),
93            session_id: session_id.clone(),
94            dialog_handler: Arc::new(RwLock::new(None)),
95            download_handler: Arc::new(RwLock::new(None)),
96            file_chooser_handler: Arc::new(RwLock::new(None)),
97            console_handler: Arc::new(RwLock::new(None)),
98            pageerror_handler: Arc::new(RwLock::new(None)),
99            frameattached_handler: Arc::new(RwLock::new(None)),
100            framenavigated_handler: Arc::new(RwLock::new(None)),
101            framedetached_handler: Arc::new(RwLock::new(None)),
102            downloads: Arc::new(Mutex::new(HashMap::new())),
103            download_dir,
104            file_chooser_intercepted: Arc::new(RwLock::new(false)),
105            wait_for_dialog_tx: Arc::new(Mutex::new(None)),
106            wait_for_download_tx: Arc::new(Mutex::new(None)),
107            wait_for_file_chooser_tx: Arc::new(Mutex::new(None)),
108            wait_for_console_tx: Arc::new(Mutex::new(None)),
109            wait_for_pageerror_tx: Arc::new(Mutex::new(None)),
110        };
111        
112        // Start the event listener
113        manager.start_event_listener();
114        
115        manager
116    }
117    
118    /// Start the background event listener for console, pageerror, dialog, frame, and download events.
119    fn start_event_listener(&self) {
120        event_listener::start_event_listener(
121            self.connection.clone(),
122            self.session_id.clone(),
123            self.console_handler.clone(),
124            self.pageerror_handler.clone(),
125            self.dialog_handler.clone(),
126            self.frameattached_handler.clone(),
127            self.framenavigated_handler.clone(),
128            self.framedetached_handler.clone(),
129            self.wait_for_console_tx.clone(),
130            self.wait_for_pageerror_tx.clone(),
131            self.wait_for_dialog_tx.clone(),
132            // Download handling
133            self.download_handler.clone(),
134            self.downloads.clone(),
135            self.wait_for_download_tx.clone(),
136        );
137    }
138
139    /// Set the dialog handler.
140    pub async fn set_dialog_handler<F, Fut>(&self, handler: F)
141    where
142        F: Fn(Dialog) -> Fut + Send + Sync + 'static,
143        Fut: Future<Output = Result<(), PageError>> + Send + 'static,
144    {
145        let mut dialog_handler = self.dialog_handler.write().await;
146        *dialog_handler = Some(Box::new(move |dialog| {
147            Box::pin(handler(dialog))
148        }));
149    }
150
151    /// Remove the dialog handler.
152    pub async fn remove_dialog_handler(&self) {
153        let mut dialog_handler = self.dialog_handler.write().await;
154        *dialog_handler = None;
155    }
156
157    /// Set the download handler.
158    pub async fn set_download_handler<F, Fut>(&self, handler: F)
159    where
160        F: Fn(Download) -> Fut + Send + Sync + 'static,
161        Fut: Future<Output = ()> + Send + 'static,
162    {
163        let mut download_handler = self.download_handler.write().await;
164        *download_handler = Some(Box::new(move |download| {
165            Box::pin(handler(download))
166        }));
167    }
168
169    /// Set the file chooser handler.
170    pub async fn set_file_chooser_handler<F, Fut>(&self, handler: F)
171    where
172        F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
173        Fut: Future<Output = ()> + Send + 'static,
174    {
175        let mut file_chooser_handler = self.file_chooser_handler.write().await;
176        *file_chooser_handler = Some(Box::new(move |chooser| {
177            Box::pin(handler(chooser))
178        }));
179    }
180
181    /// Set the console handler.
182    pub async fn set_console_handler<F, Fut>(&self, handler: F)
183    where
184        F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
185        Fut: Future<Output = ()> + Send + 'static,
186    {
187        let mut console_handler = self.console_handler.write().await;
188        *console_handler = Some(Box::new(move |message| {
189            Box::pin(handler(message))
190        }));
191    }
192
193    /// Remove the console handler.
194    pub async fn remove_console_handler(&self) {
195        let mut console_handler = self.console_handler.write().await;
196        *console_handler = None;
197    }
198
199    /// Set the page error handler.
200    pub async fn set_pageerror_handler<F, Fut>(&self, handler: F)
201    where
202        F: Fn(PageErrorInfo) -> Fut + Send + Sync + 'static,
203        Fut: Future<Output = ()> + Send + 'static,
204    {
205        let mut pageerror_handler = self.pageerror_handler.write().await;
206        *pageerror_handler = Some(Box::new(move |error| {
207            Box::pin(handler(error))
208        }));
209    }
210
211    /// Remove the page error handler.
212    pub async fn remove_pageerror_handler(&self) {
213        let mut pageerror_handler = self.pageerror_handler.write().await;
214        *pageerror_handler = None;
215    }
216
217    /// Set the frame attached handler.
218    pub async fn set_frameattached_handler<F, Fut>(&self, handler: F)
219    where
220        F: Fn(Frame) -> Fut + Send + Sync + 'static,
221        Fut: Future<Output = ()> + Send + 'static,
222    {
223        let mut frameattached_handler = self.frameattached_handler.write().await;
224        *frameattached_handler = Some(Box::new(move |frame| {
225            Box::pin(handler(frame))
226        }));
227    }
228
229    /// Remove the frame attached handler.
230    pub async fn remove_frameattached_handler(&self) {
231        let mut frameattached_handler = self.frameattached_handler.write().await;
232        *frameattached_handler = None;
233    }
234
235    /// Set the frame navigated handler.
236    pub async fn set_framenavigated_handler<F, Fut>(&self, handler: F)
237    where
238        F: Fn(Frame) -> Fut + Send + Sync + 'static,
239        Fut: Future<Output = ()> + Send + 'static,
240    {
241        let mut framenavigated_handler = self.framenavigated_handler.write().await;
242        *framenavigated_handler = Some(Box::new(move |frame| {
243            Box::pin(handler(frame))
244        }));
245    }
246
247    /// Remove the frame navigated handler.
248    pub async fn remove_framenavigated_handler(&self) {
249        let mut framenavigated_handler = self.framenavigated_handler.write().await;
250        *framenavigated_handler = None;
251    }
252
253    /// Set the frame detached handler.
254    pub async fn set_framedetached_handler<F, Fut>(&self, handler: F)
255    where
256        F: Fn(Frame) -> Fut + Send + Sync + 'static,
257        Fut: Future<Output = ()> + Send + 'static,
258    {
259        let mut framedetached_handler = self.framedetached_handler.write().await;
260        *framedetached_handler = Some(Box::new(move |frame| {
261            Box::pin(handler(frame))
262        }));
263    }
264
265    /// Remove the frame detached handler.
266    pub async fn remove_framedetached_handler(&self) {
267        let mut framedetached_handler = self.framedetached_handler.write().await;
268        *framedetached_handler = None;
269    }
270
271    /// Handle frame attached event externally.
272    pub async fn handle_frame_attached(&self, frame: Frame) {
273        let handler = self.frameattached_handler.read().await;
274        if let Some(ref h) = *handler {
275            h(frame).await;
276        }
277    }
278
279    /// Handle frame navigated event externally.
280    pub async fn handle_frame_navigated(&self, frame: Frame) {
281        let handler = self.framenavigated_handler.read().await;
282        if let Some(ref h) = *handler {
283            h(frame).await;
284        }
285    }
286
287    /// Handle frame detached event externally.
288    pub async fn handle_frame_detached(&self, frame: Frame) {
289        let handler = self.framedetached_handler.read().await;
290        if let Some(ref h) = *handler {
291            h(frame).await;
292        }
293    }
294
295    /// Set whether to intercept file chooser dialogs.
296    pub async fn set_intercept_file_chooser(&self, enabled: bool) -> Result<(), PageError> {
297        *self.file_chooser_intercepted.write().await = enabled;
298        
299        self.connection
300            .send_command::<_, serde_json::Value>(
301                "Page.setInterceptFileChooserDialog",
302                Some(serde_json::json!({ "enabled": enabled })),
303                Some(&self.session_id),
304            )
305            .await?;
306        
307        Ok(())
308    }
309
310    /// Set the download behavior.
311    pub async fn set_download_behavior(&self, allow: bool) -> Result<(), PageError> {
312        // Ensure download directory exists
313        if allow {
314            tokio::fs::create_dir_all(&self.download_dir).await.map_err(|e| {
315                PageError::EvaluationFailed(format!("Failed to create download directory: {e}"))
316            })?;
317        }
318
319        let behavior = if allow { "allow" } else { "deny" };
320        
321        self.connection
322            .send_command::<_, serde_json::Value>(
323                "Browser.setDownloadBehavior",
324                Some(serde_json::json!({
325                    "behavior": behavior,
326                    "downloadPath": self.download_dir.to_string_lossy(),
327                    "eventsEnabled": true,
328                })),
329                Some(&self.session_id),
330            )
331            .await?;
332
333        Ok(())
334    }
335
336    /// Handle a dialog event from CDP.
337    pub async fn handle_dialog_event(
338        &self,
339        dialog_type: viewpoint_cdp::protocol::DialogType,
340        message: String,
341        default_prompt: Option<String>,
342    ) {
343        let dialog = Dialog::new(
344            self.connection.clone(),
345            self.session_id.clone(),
346            dialog_type,
347            message,
348            default_prompt,
349        );
350
351        // Check if there's a waiter
352        {
353            let mut waiter = self.wait_for_dialog_tx.lock().await;
354            if let Some(tx) = waiter.take() {
355                let _ = tx.send(dialog);
356                return;
357            }
358        }
359
360        // Check if there's a handler
361        let handler = self.dialog_handler.read().await;
362        if let Some(ref h) = *handler {
363            if let Err(e) = h(dialog).await {
364                warn!("Dialog handler failed: {}", e);
365            }
366        } else {
367            // Auto-dismiss if no handler
368            debug!("Auto-dismissing dialog (no handler registered)");
369            if let Err(e) = dialog.dismiss().await {
370                warn!("Failed to auto-dismiss dialog: {}", e);
371            }
372        }
373    }
374
375    /// Handle download begin event.
376    pub async fn handle_download_begin(
377        &self,
378        guid: String,
379        suggested_filename: String,
380        url: String,
381    ) {
382        download_handling::handle_download_begin(
383            &self.connection,
384            &self.session_id,
385            &self.downloads,
386            &self.download_handler,
387            &self.wait_for_download_tx,
388            guid,
389            suggested_filename,
390            url,
391        )
392        .await;
393    }
394
395    /// Handle download progress event.
396    pub async fn handle_download_progress(&self, guid: String, state: &str) {
397        download_handling::handle_download_progress(&self.downloads, guid, state).await;
398    }
399
400    /// Handle file chooser event.
401    pub async fn handle_file_chooser_event(
402        &self,
403        frame_id: String,
404        mode: viewpoint_cdp::protocol::page::FileChooserMode,
405        backend_node_id: Option<i32>,
406    ) {
407        download_handling::handle_file_chooser_event(
408            &self.connection,
409            &self.session_id,
410            &self.file_chooser_handler,
411            &self.wait_for_file_chooser_tx,
412            frame_id,
413            mode,
414            backend_node_id,
415        )
416        .await;
417    }
418
419    /// Wait for a dialog to appear.
420    pub async fn wait_for_dialog(&self, timeout: Duration) -> Result<Dialog, PageError> {
421        let (tx, rx) = oneshot::channel();
422        {
423            let mut waiter = self.wait_for_dialog_tx.lock().await;
424            *waiter = Some(tx);
425        }
426        
427        tokio::time::timeout(timeout, rx)
428            .await
429            .map_err(|_| PageError::EvaluationFailed("Timeout waiting for dialog".to_string()))?
430            .map_err(|_| PageError::EvaluationFailed("Dialog wait cancelled".to_string()))
431    }
432
433    /// Wait for a download to start.
434    pub async fn wait_for_download(&self, timeout: Duration) -> Result<Download, PageError> {
435        download_handling::wait_for_download(&self.wait_for_download_tx, timeout).await
436    }
437
438    /// Wait for a file chooser to open.
439    pub async fn wait_for_file_chooser(&self, timeout: Duration) -> Result<FileChooser, PageError> {
440        download_handling::wait_for_file_chooser(&self.wait_for_file_chooser_tx, timeout).await
441    }
442
443    /// Wait for a console message.
444    pub async fn wait_for_console(&self, timeout: Duration) -> Result<ConsoleMessage, PageError> {
445        let (tx, rx) = oneshot::channel();
446        {
447            let mut waiter = self.wait_for_console_tx.lock().await;
448            *waiter = Some(tx);
449        }
450        
451        tokio::time::timeout(timeout, rx)
452            .await
453            .map_err(|_| PageError::EvaluationFailed("Timeout waiting for console message".to_string()))?
454            .map_err(|_| PageError::EvaluationFailed("Console wait cancelled".to_string()))
455    }
456
457    /// Wait for a page error.
458    pub async fn wait_for_pageerror(&self, timeout: Duration) -> Result<PageErrorInfo, PageError> {
459        let (tx, rx) = oneshot::channel();
460        {
461            let mut waiter = self.wait_for_pageerror_tx.lock().await;
462            *waiter = Some(tx);
463        }
464        
465        tokio::time::timeout(timeout, rx)
466            .await
467            .map_err(|_| PageError::EvaluationFailed("Timeout waiting for page error".to_string()))?
468            .map_err(|_| PageError::EvaluationFailed("Page error wait cancelled".to_string()))
469    }
470}