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}