viewpoint_core/context/
mod.rs

1//! Browser context management.
2
3mod api;
4pub mod binding;
5mod construction;
6mod cookies;
7mod emulation;
8pub mod events;
9mod har;
10mod page_events;
11mod page_factory;
12mod page_management;
13mod permissions;
14pub mod routing;
15mod routing_impl;
16mod scripts;
17pub mod storage;
18mod storage_restore;
19mod test_id;
20pub mod trace;
21mod tracing_access;
22pub mod types;
23mod weberror;
24
25pub use cookies::ClearCookiesBuilder;
26pub use emulation::SetGeolocationBuilder;
27
28// HashMap is used in emulation.rs
29use std::sync::Arc;
30use std::time::Duration;
31
32use tokio::sync::RwLock;
33use tracing::{debug, info, instrument};
34
35use viewpoint_cdp::CdpConnection;
36use viewpoint_cdp::protocol::target_domain::DisposeBrowserContextParams;
37
38use crate::error::ContextError;
39
40pub use events::{ContextEventManager, HandlerId};
41pub use storage::{StorageStateBuilder, StorageStateOptions};
42use trace::TracingState;
43pub use trace::{Tracing, TracingOptions};
44pub use types::{
45    ColorScheme, ContextOptions, ContextOptionsBuilder, Cookie, ForcedColors, Geolocation,
46    HttpCredentials, IndexedDbDatabase, IndexedDbEntry, IndexedDbIndex, IndexedDbObjectStore,
47    LocalStorageEntry, Permission, ReducedMotion, SameSite, StorageOrigin, StorageState,
48    StorageStateSource, ViewportSize,
49};
50pub use weberror::WebErrorHandler;
51// Re-export WebError for context-level usage
52pub use crate::page::page_error::WebError;
53
54/// Default test ID attribute name.
55pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
56
57/// An isolated browser context.
58///
59/// Browser contexts are similar to incognito windows - they have their own
60/// cookies, cache, and storage that are isolated from other contexts.
61///
62/// # Features
63///
64/// - **Cookie Management**: Add, get, and clear cookies
65/// - **Storage State**: Save and restore browser state
66/// - **Permissions**: Grant permissions like geolocation, camera, etc.
67/// - **Geolocation**: Mock browser location
68/// - **HTTP Credentials**: Basic/Digest authentication
69/// - **Extra Headers**: Add headers to all requests
70/// - **Offline Mode**: Simulate network offline conditions
71/// - **Event Handling**: Listen for page creation and context close events
72/// - **Init Scripts**: Scripts that run before every page load
73/// - **Custom Test ID**: Configure which attribute is used for test IDs
74///
75/// # Ownership
76///
77/// Contexts can be either "owned" (created by us) or "external" (discovered when
78/// connecting to an existing browser). When closing an external context, the
79/// underlying browser context is not disposed - only our connection to it is closed.
80pub struct BrowserContext {
81    /// CDP connection.
82    connection: Arc<CdpConnection>,
83    /// Browser context ID.
84    context_id: String,
85    /// Whether the context has been closed.
86    closed: bool,
87    /// Whether we own this context (created it) vs discovered it.
88    /// Owned contexts are disposed when closed; external contexts are not.
89    owned: bool,
90    /// Created pages (weak tracking for `pages()` method).
91    pages: Arc<RwLock<Vec<PageInfo>>>,
92    /// Default timeout for actions.
93    default_timeout: Duration,
94    /// Default timeout for navigation.
95    default_navigation_timeout: Duration,
96    /// Context options used to create this context.
97    options: ContextOptions,
98    /// Web error handler.
99    weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
100    /// Event manager for context-level events.
101    event_manager: Arc<ContextEventManager>,
102    /// Context-level route registry.
103    route_registry: Arc<routing::ContextRouteRegistry>,
104    /// Context-level binding registry.
105    binding_registry: Arc<binding::ContextBindingRegistry>,
106    /// Init scripts to run on every page load.
107    init_scripts: Arc<RwLock<Vec<String>>>,
108    /// Custom test ID attribute name (defaults to "data-testid").
109    test_id_attribute: Arc<RwLock<String>>,
110    /// HAR recorder for capturing network traffic.
111    har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
112    /// Shared tracing state for persistent tracing across `tracing()` calls.
113    tracing_state: Arc<RwLock<TracingState>>,
114}
115
116// Manual Debug implementation since WebErrorHandler doesn't implement Debug
117impl std::fmt::Debug for BrowserContext {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("BrowserContext")
120            .field("context_id", &self.context_id)
121            .field("closed", &self.closed)
122            .field("owned", &self.owned)
123            .field("default_timeout", &self.default_timeout)
124            .field(
125                "default_navigation_timeout",
126                &self.default_navigation_timeout,
127            )
128            .finish_non_exhaustive()
129    }
130}
131
132/// Information about a page in the context.
133#[derive(Debug, Clone)]
134pub struct PageInfo {
135    /// Target ID.
136    pub target_id: String,
137    /// Session ID (may be empty if not tracked).
138    pub session_id: String,
139}
140
141impl BrowserContext {
142    // Construction methods (new, with_options, from_existing, apply_options) are in construction.rs
143
144    // Page management methods (new_page, pages) are in page_management.rs
145
146    // Cookie methods are in cookies.rs
147
148    // Storage state methods are in storage.rs
149
150    // Permissions methods are in permissions.rs
151
152    // =========================================================================
153    // Geolocation
154    // =========================================================================
155
156    /// Set the geolocation.
157    ///
158    /// # Example
159    ///
160    /// ```no_run
161    /// use viewpoint_core::BrowserContext;
162    ///
163    /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
164    /// // San Francisco
165    /// context.set_geolocation(37.7749, -122.4194).await?;
166    /// # Ok(())
167    /// # }
168    /// ```
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if setting geolocation fails.
173    pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
174        SetGeolocationBuilder::new(self, latitude, longitude)
175    }
176
177    // clear_geolocation, set_extra_http_headers, set_offline are in emulation.rs
178
179    // =========================================================================
180    // Ownership and Status
181    // =========================================================================
182
183    /// Check if this context is owned (created by us) or external.
184    ///
185    /// External contexts are discovered when connecting to an already-running browser.
186    /// They are not disposed when closed.
187    pub fn is_owned(&self) -> bool {
188        self.owned
189    }
190
191    /// Check if this is the default browser context.
192    ///
193    /// The default context represents the browser's main profile and has an empty ID.
194    pub fn is_default(&self) -> bool {
195        self.context_id.is_empty()
196    }
197
198    // =========================================================================
199    // Timeout Configuration
200    // =========================================================================
201
202    /// Set the default timeout for actions.
203    ///
204    /// This timeout is used for actions like clicking, typing, etc.
205    pub fn set_default_timeout(&mut self, timeout: Duration) {
206        self.default_timeout = timeout;
207    }
208
209    /// Get the default timeout for actions.
210    pub fn default_timeout(&self) -> Duration {
211        self.default_timeout
212    }
213
214    /// Set the default navigation timeout.
215    ///
216    /// This timeout is used for navigation operations like goto, reload, etc.
217    pub fn set_default_navigation_timeout(&mut self, timeout: Duration) {
218        self.default_navigation_timeout = timeout;
219    }
220
221    /// Get the default navigation timeout.
222    pub fn default_navigation_timeout(&self) -> Duration {
223        self.default_navigation_timeout
224    }
225
226    // Init script methods are in scripts.rs
227
228    // =========================================================================
229    // Context Lifecycle
230    // =========================================================================
231
232    /// Close this browser context and all its pages.
233    ///
234    /// For contexts we created (owned), this disposes the context via CDP.
235    /// For external contexts (discovered when connecting to an existing browser),
236    /// this only closes our connection without disposing the context.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if closing fails.
241    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id, owned = self.owned))]
242    pub async fn close(&mut self) -> Result<(), ContextError> {
243        if self.closed {
244            debug!("Context already closed");
245            return Ok(());
246        }
247
248        info!("Closing browser context");
249
250        // Auto-save HAR if recording is active
251        if let Some(recorder) = self.har_recorder.write().await.take() {
252            if let Err(e) = recorder.save().await {
253                debug!("Failed to auto-save HAR on close: {}", e);
254            } else {
255                debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
256            }
257        }
258
259        // Emit close event before cleanup
260        self.event_manager.emit_close().await;
261
262        // Only dispose the context if we own it
263        // External contexts (from connecting to existing browser) should not be disposed
264        if self.owned && !self.context_id.is_empty() {
265            debug!("Disposing owned browser context");
266            self.connection
267                .send_command::<_, serde_json::Value>(
268                    "Target.disposeBrowserContext",
269                    Some(DisposeBrowserContextParams {
270                        browser_context_id: self.context_id.clone(),
271                    }),
272                    None,
273                )
274                .await?;
275        } else {
276            debug!("Skipping dispose for external/default context");
277        }
278
279        // Clear all event handlers
280        self.event_manager.clear().await;
281
282        self.closed = true;
283        info!("Browser context closed");
284        Ok(())
285    }
286
287    /// Get the context ID.
288    pub fn id(&self) -> &str {
289        &self.context_id
290    }
291
292    /// Check if this context has been closed.
293    pub fn is_closed(&self) -> bool {
294        self.closed
295    }
296
297    /// Get a reference to the CDP connection.
298    pub fn connection(&self) -> &Arc<CdpConnection> {
299        &self.connection
300    }
301
302    /// Get the context ID.
303    pub fn context_id(&self) -> &str {
304        &self.context_id
305    }
306
307    // Web error event methods are in weberror.rs (on_weberror, off_weberror)
308
309    // Page and close event methods are in page_events.rs (on_page, off_page, on_close, off_close, wait_for_page)
310
311    // Context-level routing methods are in routing_impl.rs
312
313    // HAR recording methods are in har.rs
314
315    // Exposed function methods are in binding.rs (expose_function, remove_exposed_function)
316
317    // API request context methods are in api.rs (request, sync_cookies_from_api)
318
319    // Tracing method is in tracing_access.rs
320
321    // Test ID attribute methods are in test_id.rs
322}
323
324// ClearCookiesBuilder is in cookies.rs
325// SetGeolocationBuilder is in emulation.rs