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::{
10    CreateTargetParams, CreateTargetResult, GetTargetsParams, GetTargetsResult,
11};
12
13use crate::error::ContextError;
14use crate::page::Page;
15
16use super::{BrowserContext, PageInfo};
17
18impl BrowserContext {
19    /// Create a new page in this context.
20    ///
21    /// This method creates a new page target and waits for the CDP event listener
22    /// to complete page initialization. All page creation goes through the unified
23    /// CDP event-driven path, ensuring consistent behavior.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if page creation fails or times out.
28    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
29    pub async fn new_page(&self) -> Result<Page, ContextError> {
30        if self.closed {
31            return Err(ContextError::Closed);
32        }
33
34        info!("Creating new page");
35
36        // Set up a oneshot channel to receive the page from the event listener
37        let (tx, rx) = oneshot::channel::<Page>();
38        let tx = Arc::new(tokio::sync::Mutex::new(Some(tx)));
39
40        // Register a temporary handler to capture the new page
41        let tx_clone = tx.clone();
42        let handler_id = self
43            .event_manager
44            .on_page(move |page| {
45                let tx = tx_clone.clone();
46                async move {
47                    let mut guard = tx.lock().await;
48                    if let Some(sender) = guard.take() {
49                        let _ = sender.send(page);
50                    }
51                }
52            })
53            .await;
54
55        // Create the target - the CDP event listener will handle attachment,
56        // domain enabling, and page creation
57        let create_result: Result<CreateTargetResult, _> = self
58            .connection
59            .send_command(
60                "Target.createTarget",
61                Some(CreateTargetParams {
62                    url: "about:blank".to_string(),
63                    width: None,
64                    height: None,
65                    browser_context_id: Some(self.context_id.clone()),
66                    background: None,
67                    new_window: None,
68                }),
69                None,
70            )
71            .await;
72
73        // Handle target creation error
74        if let Err(e) = create_result {
75            // Clean up handler before returning error
76            self.event_manager.off_page(handler_id).await;
77            return Err(e.into());
78        }
79
80        // Wait for the event listener to complete page setup
81        let timeout_duration = Duration::from_secs(30);
82        let page_result = tokio::time::timeout(timeout_duration, rx).await;
83
84        // Clean up the handler
85        self.event_manager.off_page(handler_id).await;
86
87        // Process the result
88        match page_result {
89            Ok(Ok(page)) => {
90                // Apply context-level init scripts to the new page
91                if let Err(e) = self.apply_init_scripts_to_session(page.session_id()).await {
92                    debug!("Failed to apply init scripts: {}", e);
93                }
94
95                info!(
96                    target_id = %page.target_id(),
97                    session_id = %page.session_id(),
98                    "Page created successfully"
99                );
100
101                Ok(page)
102            }
103            Ok(Err(_)) => Err(ContextError::Internal(
104                "Page channel closed unexpectedly".to_string(),
105            )),
106            Err(_) => Err(ContextError::Timeout {
107                operation: "new_page".to_string(),
108                duration: timeout_duration,
109            }),
110        }
111    }
112
113    /// Get all pages in this context.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if querying targets fails.
118    pub async fn pages(&self) -> Result<Vec<PageInfo>, ContextError> {
119        if self.closed {
120            return Err(ContextError::Closed);
121        }
122
123        let result: GetTargetsResult = self
124            .connection
125            .send_command("Target.getTargets", Some(GetTargetsParams::default()), None)
126            .await?;
127
128        let pages: Vec<PageInfo> = result
129            .target_infos
130            .into_iter()
131            .filter(|t| {
132                // For the default context (empty string ID), match targets with no context ID
133                // or with an empty context ID
134                let matches_context = if self.context_id.is_empty() {
135                    // Default context: match targets without a context ID or with empty context ID
136                    t.browser_context_id.as_deref().is_none()
137                        || t.browser_context_id.as_deref() == Some("")
138                } else {
139                    // Named context: exact match
140                    t.browser_context_id.as_deref() == Some(&self.context_id)
141                };
142                matches_context && t.target_type == "page"
143            })
144            .map(|t| PageInfo {
145                target_id: t.target_id,
146                session_id: String::new(), // Would need to track sessions
147            })
148            .collect();
149
150        Ok(pages)
151    }
152}