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::{Mutex, RwLock, oneshot};
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, FrameAttachedHandler,
32    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| Box::pin(handler(dialog))));
147    }
148
149    /// Remove the dialog handler.
150    pub async fn remove_dialog_handler(&self) {
151        let mut dialog_handler = self.dialog_handler.write().await;
152        *dialog_handler = None;
153    }
154
155    /// Set the download handler.
156    pub async fn set_download_handler<F, Fut>(&self, handler: F)
157    where
158        F: Fn(Download) -> Fut + Send + Sync + 'static,
159        Fut: Future<Output = ()> + Send + 'static,
160    {
161        let mut download_handler = self.download_handler.write().await;
162        *download_handler = Some(Box::new(move |download| Box::pin(handler(download))));
163    }
164
165    /// Set the file chooser handler.
166    pub async fn set_file_chooser_handler<F, Fut>(&self, handler: F)
167    where
168        F: Fn(FileChooser) -> Fut + Send + Sync + 'static,
169        Fut: Future<Output = ()> + Send + 'static,
170    {
171        let mut file_chooser_handler = self.file_chooser_handler.write().await;
172        *file_chooser_handler = Some(Box::new(move |chooser| Box::pin(handler(chooser))));
173    }
174
175    /// Set the console handler.
176    pub async fn set_console_handler<F, Fut>(&self, handler: F)
177    where
178        F: Fn(ConsoleMessage) -> Fut + Send + Sync + 'static,
179        Fut: Future<Output = ()> + Send + 'static,
180    {
181        let mut console_handler = self.console_handler.write().await;
182        *console_handler = Some(Box::new(move |message| Box::pin(handler(message))));
183    }
184
185    /// Remove the console handler.
186    pub async fn remove_console_handler(&self) {
187        let mut console_handler = self.console_handler.write().await;
188        *console_handler = None;
189    }
190
191    /// Set the page error handler.
192    pub async fn set_pageerror_handler<F, Fut>(&self, handler: F)
193    where
194        F: Fn(PageErrorInfo) -> Fut + Send + Sync + 'static,
195        Fut: Future<Output = ()> + Send + 'static,
196    {
197        let mut pageerror_handler = self.pageerror_handler.write().await;
198        *pageerror_handler = Some(Box::new(move |error| Box::pin(handler(error))));
199    }
200
201    /// Remove the page error handler.
202    pub async fn remove_pageerror_handler(&self) {
203        let mut pageerror_handler = self.pageerror_handler.write().await;
204        *pageerror_handler = None;
205    }
206
207    /// Set the frame attached handler.
208    pub async fn set_frameattached_handler<F, Fut>(&self, handler: F)
209    where
210        F: Fn(Frame) -> Fut + Send + Sync + 'static,
211        Fut: Future<Output = ()> + Send + 'static,
212    {
213        let mut frameattached_handler = self.frameattached_handler.write().await;
214        *frameattached_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
215    }
216
217    /// Remove the frame attached handler.
218    pub async fn remove_frameattached_handler(&self) {
219        let mut frameattached_handler = self.frameattached_handler.write().await;
220        *frameattached_handler = None;
221    }
222
223    /// Set the frame navigated handler.
224    pub async fn set_framenavigated_handler<F, Fut>(&self, handler: F)
225    where
226        F: Fn(Frame) -> Fut + Send + Sync + 'static,
227        Fut: Future<Output = ()> + Send + 'static,
228    {
229        let mut framenavigated_handler = self.framenavigated_handler.write().await;
230        *framenavigated_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
231    }
232
233    /// Remove the frame navigated handler.
234    pub async fn remove_framenavigated_handler(&self) {
235        let mut framenavigated_handler = self.framenavigated_handler.write().await;
236        *framenavigated_handler = None;
237    }
238
239    /// Set the frame detached handler.
240    pub async fn set_framedetached_handler<F, Fut>(&self, handler: F)
241    where
242        F: Fn(Frame) -> Fut + Send + Sync + 'static,
243        Fut: Future<Output = ()> + Send + 'static,
244    {
245        let mut framedetached_handler = self.framedetached_handler.write().await;
246        *framedetached_handler = Some(Box::new(move |frame| Box::pin(handler(frame))));
247    }
248
249    /// Remove the frame detached handler.
250    pub async fn remove_framedetached_handler(&self) {
251        let mut framedetached_handler = self.framedetached_handler.write().await;
252        *framedetached_handler = None;
253    }
254
255    /// Handle frame attached event externally.
256    pub async fn handle_frame_attached(&self, frame: Frame) {
257        let handler = self.frameattached_handler.read().await;
258        if let Some(ref h) = *handler {
259            h(frame).await;
260        }
261    }
262
263    /// Handle frame navigated event externally.
264    pub async fn handle_frame_navigated(&self, frame: Frame) {
265        let handler = self.framenavigated_handler.read().await;
266        if let Some(ref h) = *handler {
267            h(frame).await;
268        }
269    }
270
271    /// Handle frame detached event externally.
272    pub async fn handle_frame_detached(&self, frame: Frame) {
273        let handler = self.framedetached_handler.read().await;
274        if let Some(ref h) = *handler {
275            h(frame).await;
276        }
277    }
278
279    /// Set whether to intercept file chooser dialogs.
280    pub async fn set_intercept_file_chooser(&self, enabled: bool) -> Result<(), PageError> {
281        *self.file_chooser_intercepted.write().await = enabled;
282
283        self.connection
284            .send_command::<_, serde_json::Value>(
285                "Page.setInterceptFileChooserDialog",
286                Some(serde_json::json!({ "enabled": enabled })),
287                Some(&self.session_id),
288            )
289            .await?;
290
291        Ok(())
292    }
293
294    /// Set the download behavior.
295    pub async fn set_download_behavior(&self, allow: bool) -> Result<(), PageError> {
296        // Ensure download directory exists
297        if allow {
298            tokio::fs::create_dir_all(&self.download_dir)
299                .await
300                .map_err(|e| {
301                    PageError::EvaluationFailed(format!("Failed to create download directory: {e}"))
302                })?;
303        }
304
305        let behavior = if allow { "allow" } else { "deny" };
306
307        self.connection
308            .send_command::<_, serde_json::Value>(
309                "Browser.setDownloadBehavior",
310                Some(serde_json::json!({
311                    "behavior": behavior,
312                    "downloadPath": self.download_dir.to_string_lossy(),
313                    "eventsEnabled": true,
314                })),
315                Some(&self.session_id),
316            )
317            .await?;
318
319        Ok(())
320    }
321
322    /// Handle a dialog event from CDP.
323    pub async fn handle_dialog_event(
324        &self,
325        dialog_type: viewpoint_cdp::protocol::DialogType,
326        message: String,
327        default_prompt: Option<String>,
328    ) {
329        let dialog = Dialog::new(
330            self.connection.clone(),
331            self.session_id.clone(),
332            dialog_type,
333            message,
334            default_prompt,
335        );
336
337        // Check if there's a waiter
338        {
339            let mut waiter = self.wait_for_dialog_tx.lock().await;
340            if let Some(tx) = waiter.take() {
341                let _ = tx.send(dialog);
342                return;
343            }
344        }
345
346        // Check if there's a handler
347        let handler = self.dialog_handler.read().await;
348        if let Some(ref h) = *handler {
349            if let Err(e) = h(dialog).await {
350                warn!("Dialog handler failed: {}", e);
351            }
352        } else {
353            // Auto-dismiss if no handler
354            debug!("Auto-dismissing dialog (no handler registered)");
355            if let Err(e) = dialog.dismiss().await {
356                warn!("Failed to auto-dismiss dialog: {}", e);
357            }
358        }
359    }
360
361    /// Handle download begin event.
362    pub async fn handle_download_begin(
363        &self,
364        guid: String,
365        suggested_filename: String,
366        url: String,
367    ) {
368        download_handling::handle_download_begin(
369            &self.connection,
370            &self.session_id,
371            &self.downloads,
372            &self.download_handler,
373            &self.wait_for_download_tx,
374            guid,
375            suggested_filename,
376            url,
377        )
378        .await;
379    }
380
381    /// Handle download progress event.
382    pub async fn handle_download_progress(&self, guid: String, state: &str) {
383        download_handling::handle_download_progress(&self.downloads, guid, state).await;
384    }
385
386    /// Handle file chooser event.
387    pub async fn handle_file_chooser_event(
388        &self,
389        frame_id: String,
390        mode: viewpoint_cdp::protocol::page::FileChooserMode,
391        backend_node_id: Option<i32>,
392    ) {
393        download_handling::handle_file_chooser_event(
394            &self.connection,
395            &self.session_id,
396            &self.file_chooser_handler,
397            &self.wait_for_file_chooser_tx,
398            frame_id,
399            mode,
400            backend_node_id,
401        )
402        .await;
403    }
404
405    /// Wait for a dialog to appear.
406    pub async fn wait_for_dialog(&self, timeout: Duration) -> Result<Dialog, PageError> {
407        let (tx, rx) = oneshot::channel();
408        {
409            let mut waiter = self.wait_for_dialog_tx.lock().await;
410            *waiter = Some(tx);
411        }
412
413        tokio::time::timeout(timeout, rx)
414            .await
415            .map_err(|_| PageError::EvaluationFailed("Timeout waiting for dialog".to_string()))?
416            .map_err(|_| PageError::EvaluationFailed("Dialog wait cancelled".to_string()))
417    }
418
419    /// Wait for a download to start.
420    pub async fn wait_for_download(&self, timeout: Duration) -> Result<Download, PageError> {
421        download_handling::wait_for_download(&self.wait_for_download_tx, timeout).await
422    }
423
424    /// Wait for a file chooser to open.
425    pub async fn wait_for_file_chooser(&self, timeout: Duration) -> Result<FileChooser, PageError> {
426        download_handling::wait_for_file_chooser(&self.wait_for_file_chooser_tx, timeout).await
427    }
428
429    /// Wait for a console message.
430    pub async fn wait_for_console(&self, timeout: Duration) -> Result<ConsoleMessage, PageError> {
431        let (tx, rx) = oneshot::channel();
432        {
433            let mut waiter = self.wait_for_console_tx.lock().await;
434            *waiter = Some(tx);
435        }
436
437        tokio::time::timeout(timeout, rx)
438            .await
439            .map_err(|_| {
440                PageError::EvaluationFailed("Timeout waiting for console message".to_string())
441            })?
442            .map_err(|_| PageError::EvaluationFailed("Console wait cancelled".to_string()))
443    }
444
445    /// Wait for a page error.
446    pub async fn wait_for_pageerror(&self, timeout: Duration) -> Result<PageErrorInfo, PageError> {
447        let (tx, rx) = oneshot::channel();
448        {
449            let mut waiter = self.wait_for_pageerror_tx.lock().await;
450            *waiter = Some(tx);
451        }
452
453        tokio::time::timeout(timeout, rx)
454            .await
455            .map_err(|_| PageError::EvaluationFailed("Timeout waiting for page error".to_string()))?
456            .map_err(|_| PageError::EvaluationFailed("Page error wait cancelled".to_string()))
457    }
458}