viewpoint_core/context/
mod.rs

1//! Browser context management.
2
3use std::sync::Arc;
4
5use viewpoint_cdp::protocol::target::{
6    AttachToTargetParams, AttachToTargetResult, CreateTargetParams, CreateTargetResult,
7    DisposeBrowserContextParams,
8};
9use viewpoint_cdp::CdpConnection;
10use tracing::{debug, info, instrument, trace};
11
12use crate::error::ContextError;
13use crate::page::Page;
14
15/// An isolated browser context.
16///
17/// Browser contexts are similar to incognito windows - they have their own
18/// cookies, cache, and storage that are isolated from other contexts.
19#[derive(Debug)]
20pub struct BrowserContext {
21    /// CDP connection.
22    connection: Arc<CdpConnection>,
23    /// Browser context ID.
24    context_id: String,
25    /// Whether the context has been closed.
26    closed: bool,
27}
28
29impl BrowserContext {
30    /// Create a new browser context.
31    pub(crate) fn new(connection: Arc<CdpConnection>, context_id: String) -> Self {
32        debug!(context_id = %context_id, "Created BrowserContext");
33        Self {
34            connection,
35            context_id,
36            closed: false,
37        }
38    }
39
40    /// Create a new page in this context.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if page creation fails.
45    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
46    pub async fn new_page(&self) -> Result<Page, ContextError> {
47        if self.closed {
48            return Err(ContextError::Closed);
49        }
50
51        info!("Creating new page");
52
53        // Create a new target (page)
54        debug!("Creating target via Target.createTarget");
55        let create_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        let target_id = &create_result.target_id;
72        debug!(target_id = %target_id, "Target created");
73
74        // Attach to the target to get a session
75        debug!(target_id = %target_id, "Attaching to target");
76        let attach_result: AttachToTargetResult = self
77            .connection
78            .send_command(
79                "Target.attachToTarget",
80                Some(AttachToTargetParams {
81                    target_id: target_id.clone(),
82                    flatten: Some(true),
83                }),
84                None,
85            )
86            .await?;
87
88        let session_id = &attach_result.session_id;
89        debug!(session_id = %session_id, "Attached to target");
90
91        // Enable required domains on the page
92        trace!("Enabling Page domain");
93        self.connection
94            .send_command::<(), serde_json::Value>("Page.enable", None, Some(session_id))
95            .await?;
96
97        trace!("Enabling Network domain");
98        self.connection
99            .send_command::<(), serde_json::Value>("Network.enable", None, Some(session_id))
100            .await?;
101
102        trace!("Enabling Runtime domain");
103        self.connection
104            .send_command::<(), serde_json::Value>("Runtime.enable", None, Some(session_id))
105            .await?;
106
107        trace!("Enabling lifecycle events");
108        self.connection
109            .send_command::<_, serde_json::Value>(
110                "Page.setLifecycleEventsEnabled",
111                Some(viewpoint_cdp::protocol::page::SetLifecycleEventsEnabledParams {
112                    enabled: true,
113                }),
114                Some(session_id),
115            )
116            .await?;
117
118        // Get the main frame ID
119        trace!("Getting frame tree");
120        let frame_tree: viewpoint_cdp::protocol::page::GetFrameTreeResult = self
121            .connection
122            .send_command("Page.getFrameTree", None::<()>, Some(session_id))
123            .await?;
124
125        let frame_id = frame_tree.frame_tree.frame.id.clone();
126        debug!(frame_id = %frame_id, "Got main frame ID");
127
128        info!(target_id = %target_id, session_id = %session_id, frame_id = %frame_id, "Page created successfully");
129
130        Ok(Page::new(
131            self.connection.clone(),
132            create_result.target_id,
133            attach_result.session_id,
134            frame_id,
135        ))
136    }
137
138    /// Close this browser context and all its pages.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if closing fails.
143    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
144    pub async fn close(&mut self) -> Result<(), ContextError> {
145        if self.closed {
146            debug!("Context already closed");
147            return Ok(());
148        }
149
150        info!("Closing browser context");
151
152        self.connection
153            .send_command::<_, serde_json::Value>(
154                "Target.disposeBrowserContext",
155                Some(DisposeBrowserContextParams {
156                    browser_context_id: self.context_id.clone(),
157                }),
158                None,
159            )
160            .await?;
161
162        self.closed = true;
163        info!("Browser context closed");
164        Ok(())
165    }
166
167    /// Get the context ID.
168    pub fn id(&self) -> &str {
169        &self.context_id
170    }
171
172    /// Check if this context has been closed.
173    pub fn is_closed(&self) -> bool {
174        self.closed
175    }
176
177    /// Get a reference to the CDP connection.
178    pub fn connection(&self) -> &Arc<CdpConnection> {
179        &self.connection
180    }
181}