viewpoint_core/context/page_management/
mod.rs

1//! Page creation and management within a browser context.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use tokio::sync::oneshot;
7use tracing::{debug, info, instrument};
8
9use viewpoint_cdp::protocol::target_domain::{CreateTargetParams, CreateTargetResult};
10
11use crate::error::ContextError;
12use crate::page::Page;
13
14use super::BrowserContext;
15
16impl BrowserContext {
17    /// Create a new page in this context.
18    ///
19    /// This method creates a new page target and waits for the CDP event listener
20    /// to complete page initialization. All page creation goes through the unified
21    /// CDP event-driven path, ensuring consistent behavior.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if page creation fails or times out.
26    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
27    pub async fn new_page(&self) -> Result<Page, ContextError> {
28        if self.closed {
29            return Err(ContextError::Closed);
30        }
31
32        info!("Creating new page");
33
34        // Set up a oneshot channel to receive the page from the event listener
35        let (tx, rx) = oneshot::channel::<Page>();
36        let tx = Arc::new(tokio::sync::Mutex::new(Some(tx)));
37
38        // Register a temporary handler to capture the new page
39        let tx_clone = tx.clone();
40        let handler_id = self
41            .event_manager
42            .on_page(move |page| {
43                let tx = tx_clone.clone();
44                async move {
45                    let mut guard = tx.lock().await;
46                    if let Some(sender) = guard.take() {
47                        let _ = sender.send(page);
48                    }
49                }
50            })
51            .await;
52
53        // Create the target - the CDP event listener will handle attachment,
54        // domain enabling, and page creation
55        let create_result: Result<CreateTargetResult, _> = self
56            .connection
57            .send_command(
58                "Target.createTarget",
59                Some(CreateTargetParams {
60                    url: "about:blank".to_string(),
61                    width: None,
62                    height: None,
63                    browser_context_id: Some(self.context_id.clone()),
64                    background: None,
65                    new_window: None,
66                }),
67                None,
68            )
69            .await;
70
71        // Handle target creation error
72        if let Err(e) = create_result {
73            // Clean up handler before returning error
74            self.event_manager.off_page(handler_id).await;
75            return Err(e.into());
76        }
77
78        // Wait for the event listener to complete page setup
79        let timeout_duration = Duration::from_secs(30);
80        let page_result = tokio::time::timeout(timeout_duration, rx).await;
81
82        // Clean up the handler
83        self.event_manager.off_page(handler_id).await;
84
85        // Process the result
86        match page_result {
87            Ok(Ok(page)) => {
88                // Apply context-level init scripts to the new page
89                if let Err(e) = self.apply_init_scripts_to_session(page.session_id()).await {
90                    debug!("Failed to apply init scripts: {}", e);
91                }
92
93                info!(
94                    target_id = %page.target_id(),
95                    session_id = %page.session_id(),
96                    "Page created successfully"
97                );
98
99                Ok(page)
100            }
101            Ok(Err(_)) => Err(ContextError::Internal(
102                "Page channel closed unexpectedly".to_string(),
103            )),
104            Err(_) => Err(ContextError::Timeout {
105                operation: "new_page".to_string(),
106                duration: timeout_duration,
107            }),
108        }
109    }
110
111    /// Get all pages in this context.
112    ///
113    /// Returns fully functional `Page` objects that can be used to interact
114    /// with the pages (navigate, click, type, etc.).
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the context is closed.
119    ///
120    /// # Example
121    ///
122    /// ```no_run
123    /// use viewpoint_core::BrowserContext;
124    ///
125    /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
126    /// // Get all pages
127    /// let pages = context.pages().await?;
128    /// for page in &pages {
129    ///     println!("Page URL: {}", page.url().await?);
130    /// }
131    /// # Ok(())
132    /// # }
133    /// ```
134    pub async fn pages(&self) -> Result<Vec<Page>, ContextError> {
135        if self.closed {
136            return Err(ContextError::Closed);
137        }
138
139        let pages_guard = self.pages.read().await;
140        Ok(pages_guard.iter().map(Page::clone_internal).collect())
141    }
142
143    /// Get the number of pages in this context.
144    ///
145    /// This is a convenience method that avoids cloning all pages when
146    /// you only need the count.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the context is closed.
151    pub async fn page_count(&self) -> Result<usize, ContextError> {
152        if self.closed {
153            return Err(ContextError::Closed);
154        }
155
156        let pages_guard = self.pages.read().await;
157        Ok(pages_guard.len())
158    }
159}