viewpoint_core/context/
mod.rs

1//! # Browser Context Management
2//!
3//! Browser contexts are isolated environments within a browser, similar to incognito windows.
4//! Each context has its own cookies, cache, localStorage, and other browser storage.
5//!
6//! ## Features
7//!
8//! - **Isolation**: Each context is completely isolated from others
9//! - **Cookie Management**: Add, get, and clear cookies with [`BrowserContext::add_cookies`], [`BrowserContext::cookies`], [`BrowserContext::clear_cookies`]
10//! - **Storage State**: Save and restore browser state (cookies, localStorage) for authentication
11//! - **Permissions**: Grant permissions like geolocation, camera, microphone
12//! - **Geolocation**: Mock browser location with [`BrowserContext::set_geolocation`]
13//! - **HTTP Credentials**: Configure basic/digest authentication
14//! - **Extra Headers**: Add headers to all requests in the context
15//! - **Offline Mode**: Simulate network offline conditions
16//! - **Event Handling**: Listen for page creation and context close events
17//! - **Init Scripts**: Run scripts before every page load
18//! - **Custom Test ID**: Configure which attribute is used for test IDs
19//! - **Network Routing**: Intercept and mock requests at the context level
20//! - **HAR Recording**: Record network traffic for debugging
21//! - **Tracing**: Record traces for debugging
22//!
23//! ## Quick Start
24//!
25//! ```no_run
26//! use viewpoint_core::{Browser, BrowserContext, Permission};
27//!
28//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
29//! let browser = Browser::launch().headless(true).launch().await?;
30//!
31//! // Create a simple context
32//! let context = browser.new_context().await?;
33//!
34//! // Create a context with options
35//! let context = browser.new_context_builder()
36//!     .viewport(1920, 1080)
37//!     .geolocation(37.7749, -122.4194)
38//!     .permissions(vec![Permission::Geolocation])
39//!     .build()
40//!     .await?;
41//!
42//! // Create a page in the context
43//! let page = context.new_page().await?;
44//! page.goto("https://example.com").goto().await?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! ## Cookie Management
50//!
51//! ```ignore
52//! use viewpoint_core::{Browser, Cookie, SameSite};
53//!
54//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
55//! # let browser = Browser::launch().headless(true).launch().await?;
56//! # let context = browser.new_context().await?;
57//! // Add cookies
58//! context.add_cookies(vec![
59//!     Cookie {
60//!         name: "session".to_string(),
61//!         value: "abc123".to_string(),
62//!         domain: Some(".example.com".to_string()),
63//!         path: Some("/".to_string()),
64//!         expires: None,
65//!         http_only: Some(true),
66//!         secure: Some(true),
67//!         same_site: Some(SameSite::Lax),
68//!     }
69//! ]).await?;
70//!
71//! // Get all cookies
72//! let cookies = context.cookies(None).await?;
73//!
74//! // Get cookies for specific URLs
75//! let cookies = context.cookies(Some(&["https://example.com"])).await?;
76//!
77//! // Clear all cookies
78//! context.clear_cookies().clear().await?;
79//!
80//! // Clear cookies matching a pattern
81//! context.clear_cookies()
82//!     .domain("example.com")
83//!     .clear()
84//!     .await?;
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Storage State
90//!
91//! Save and restore browser state for authentication:
92//!
93//! ```ignore
94//! use viewpoint_core::{Browser, StorageStateSource};
95//!
96//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
97//! # let browser = Browser::launch().headless(true).launch().await?;
98//! # let context = browser.new_context().await?;
99//! // Save storage state after login
100//! context.storage_state()
101//!     .path("auth.json")
102//!     .save()
103//!     .await?;
104//!
105//! // Create a new context with saved state
106//! let context = browser.new_context_builder()
107//!     .storage_state_path("auth.json")
108//!     .build()
109//!     .await?;
110//! # Ok(())
111//! # }
112//! ```
113//!
114//! ## Event Handling
115//!
116//! ```ignore
117//! use viewpoint_core::Browser;
118//!
119//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
120//! # let browser = Browser::launch().headless(true).launch().await?;
121//! # let context = browser.new_context().await?;
122//! // Listen for new pages
123//! let handler_id = context.on_page(|page_info| async move {
124//!     println!("New page created: {}", page_info.target_id);
125//!     Ok(())
126//! }).await;
127//!
128//! // Listen for context close
129//! context.on_close(|| async {
130//!     println!("Context closed");
131//!     Ok(())
132//! }).await;
133//!
134//! // Remove handler later
135//! context.off_page(handler_id).await;
136//! # Ok(())
137//! # }
138//! ```
139//!
140//! ## Tracing
141//!
142//! ```ignore
143//! use viewpoint_core::Browser;
144//!
145//! # async fn example() -> Result<(), viewpoint_core::CoreError> {
146//! # let browser = Browser::launch().headless(true).launch().await?;
147//! # let context = browser.new_context().await?;
148//! // Start tracing
149//! context.tracing().start().await?;
150//!
151//! // ... perform actions ...
152//!
153//! // Stop and save trace
154//! context.tracing().stop("trace.zip").await?;
155//! # Ok(())
156//! # }
157//! ```
158
159mod api;
160pub mod binding;
161mod construction;
162mod cookies;
163mod emulation;
164pub mod events;
165mod har;
166mod page_events;
167mod page_factory;
168mod page_management;
169mod permissions;
170pub mod routing;
171mod routing_impl;
172mod scripts;
173pub mod storage;
174mod storage_restore;
175mod target_events;
176mod test_id;
177mod timeout;
178pub mod trace;
179mod tracing_access;
180pub mod types;
181mod weberror;
182
183pub use cookies::ClearCookiesBuilder;
184pub use emulation::SetGeolocationBuilder;
185
186// HashMap is used in emulation.rs
187use std::sync::Arc;
188use std::time::Duration;
189
190use tokio::sync::RwLock;
191use tracing::{debug, info, instrument};
192
193use viewpoint_cdp::CdpConnection;
194use viewpoint_cdp::protocol::target_domain::DisposeBrowserContextParams;
195
196use crate::error::ContextError;
197
198pub use events::{ContextEventManager, HandlerId};
199pub use storage::{StorageStateBuilder, StorageStateOptions};
200use trace::TracingState;
201pub use trace::{Tracing, TracingOptions};
202pub use types::{
203    ColorScheme, ContextOptions, ContextOptionsBuilder, Cookie, ForcedColors, Geolocation,
204    HttpCredentials, IndexedDbDatabase, IndexedDbEntry, IndexedDbIndex, IndexedDbObjectStore,
205    LocalStorageEntry, Permission, ProxyConfig, ReducedMotion, SameSite, StorageOrigin,
206    StorageState, StorageStateSource, ViewportSize,
207};
208pub use weberror::WebErrorHandler;
209// Re-export WebError for context-level usage
210pub use crate::page::page_error::WebError;
211
212/// Default test ID attribute name.
213pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
214
215/// An isolated browser context.
216///
217/// Browser contexts are similar to incognito windows - they have their own
218/// cookies, cache, and storage that are isolated from other contexts.
219///
220/// # Features
221///
222/// - **Cookie Management**: Add, get, and clear cookies
223/// - **Storage State**: Save and restore browser state
224/// - **Permissions**: Grant permissions like geolocation, camera, etc.
225/// - **Geolocation**: Mock browser location
226/// - **HTTP Credentials**: Basic/Digest authentication
227/// - **Extra Headers**: Add headers to all requests
228/// - **Offline Mode**: Simulate network offline conditions
229/// - **Event Handling**: Listen for page creation and context close events
230/// - **Init Scripts**: Scripts that run before every page load
231/// - **Custom Test ID**: Configure which attribute is used for test IDs
232///
233/// # Ownership
234///
235/// Contexts can be either "owned" (created by us) or "external" (discovered when
236/// connecting to an existing browser). When closing an external context, the
237/// underlying browser context is not disposed - only our connection to it is closed.
238pub struct BrowserContext {
239    /// CDP connection.
240    connection: Arc<CdpConnection>,
241    /// Browser context ID.
242    context_id: String,
243    /// Context index for element ref generation.
244    /// Each context is assigned a unique index for generating scoped element refs
245    /// in the format `c{contextIndex}p{pageIndex}e{counter}`.
246    context_index: usize,
247    /// Whether the context has been closed.
248    closed: bool,
249    /// Whether we own this context (created it) vs discovered it.
250    /// Owned contexts are disposed when closed; external contexts are not.
251    owned: bool,
252    /// Created pages for `pages()` method.
253    /// Stores actual Page objects to allow returning fully functional pages.
254    pages: Arc<RwLock<Vec<crate::page::Page>>>,
255    /// Counter for assigning page indices within this context.
256    /// Wrapped in Arc to share with the target event listener.
257    page_index_counter: Arc<std::sync::atomic::AtomicUsize>,
258    /// Default timeout for actions.
259    default_timeout: Duration,
260    /// Default timeout for navigation.
261    default_navigation_timeout: Duration,
262    /// Context options used to create this context.
263    options: ContextOptions,
264    /// Web error handler.
265    weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
266    /// Event manager for context-level events.
267    event_manager: Arc<ContextEventManager>,
268    /// Context-level route registry.
269    route_registry: Arc<routing::ContextRouteRegistry>,
270    /// Context-level binding registry.
271    binding_registry: Arc<binding::ContextBindingRegistry>,
272    /// Init scripts to run on every page load.
273    init_scripts: Arc<RwLock<Vec<String>>>,
274    /// Custom test ID attribute name (defaults to "data-testid").
275    test_id_attribute: Arc<RwLock<String>>,
276    /// HAR recorder for capturing network traffic.
277    har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
278    /// Shared tracing state for persistent tracing across `tracing()` calls.
279    tracing_state: Arc<RwLock<TracingState>>,
280}
281
282// Manual Debug implementation since WebErrorHandler doesn't implement Debug
283impl std::fmt::Debug for BrowserContext {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        f.debug_struct("BrowserContext")
286            .field("context_id", &self.context_id)
287            .field("context_index", &self.context_index)
288            .field("closed", &self.closed)
289            .field("owned", &self.owned)
290            .field("default_timeout", &self.default_timeout)
291            .field(
292                "default_navigation_timeout",
293                &self.default_navigation_timeout,
294            )
295            .finish_non_exhaustive()
296    }
297}
298
299/// Information about a page in the context.
300#[derive(Debug, Clone)]
301pub struct PageInfo {
302    /// Target ID.
303    pub target_id: String,
304    /// Session ID (may be empty if not tracked).
305    pub session_id: String,
306}
307
308impl BrowserContext {
309    // Construction methods (new, with_options, from_existing, apply_options) are in construction.rs
310
311    // Page management methods (new_page, pages) are in page_management.rs
312
313    // Cookie methods are in cookies.rs
314
315    // Storage state methods are in storage.rs
316
317    // Permissions methods are in permissions.rs
318
319    // =========================================================================
320    // Geolocation
321    // =========================================================================
322
323    /// Set the geolocation.
324    ///
325    /// # Example
326    ///
327    /// ```no_run
328    /// use viewpoint_core::BrowserContext;
329    ///
330    /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
331    /// // San Francisco
332    /// context.set_geolocation(37.7749, -122.4194).await?;
333    /// # Ok(())
334    /// # }
335    /// ```
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if setting geolocation fails.
340    pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
341        SetGeolocationBuilder::new(self, latitude, longitude)
342    }
343
344    // clear_geolocation, set_extra_http_headers, set_offline are in emulation.rs
345
346    // =========================================================================
347    // Ownership and Status
348    // =========================================================================
349
350    /// Check if this context is owned (created by us) or external.
351    ///
352    /// External contexts are discovered when connecting to an already-running browser.
353    /// They are not disposed when closed.
354    pub fn is_owned(&self) -> bool {
355        self.owned
356    }
357
358    /// Check if this is the default browser context.
359    ///
360    /// The default context represents the browser's main profile and has an empty ID.
361    pub fn is_default(&self) -> bool {
362        self.context_id.is_empty()
363    }
364
365    // Timeout configuration methods are in timeout.rs
366
367    // Init script methods are in scripts.rs
368
369    // =========================================================================
370    // Context Lifecycle
371    // =========================================================================
372
373    /// Close this browser context and all its pages.
374    ///
375    /// For contexts we created (owned), this disposes the context via CDP.
376    /// For external contexts (discovered when connecting to an existing browser),
377    /// this only closes our connection without disposing the context.
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if closing fails.
382    #[instrument(level = "info", skip(self), fields(context_id = %self.context_id, owned = self.owned))]
383    pub async fn close(&mut self) -> Result<(), ContextError> {
384        if self.closed {
385            debug!("Context already closed");
386            return Ok(());
387        }
388
389        info!("Closing browser context");
390
391        // Auto-save HAR if recording is active
392        if let Some(recorder) = self.har_recorder.write().await.take() {
393            if let Err(e) = recorder.save().await {
394                debug!("Failed to auto-save HAR on close: {}", e);
395            } else {
396                debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
397            }
398        }
399
400        // Emit close event before cleanup
401        self.event_manager.emit_close().await;
402
403        // Only dispose the context if we own it
404        // External contexts (from connecting to existing browser) should not be disposed
405        if self.owned && !self.context_id.is_empty() {
406            debug!("Disposing owned browser context");
407            self.connection
408                .send_command::<_, serde_json::Value>(
409                    "Target.disposeBrowserContext",
410                    Some(DisposeBrowserContextParams {
411                        browser_context_id: self.context_id.clone(),
412                    }),
413                    None,
414                )
415                .await?;
416        } else {
417            debug!("Skipping dispose for external/default context");
418        }
419
420        // Clear all event handlers
421        self.event_manager.clear().await;
422
423        self.closed = true;
424        info!("Browser context closed");
425        Ok(())
426    }
427
428    /// Get the context ID.
429    pub fn id(&self) -> &str {
430        &self.context_id
431    }
432
433    /// Get the context index.
434    ///
435    /// This index is used for generating scoped element refs in the format
436    /// `c{contextIndex}p{pageIndex}e{counter}`. Each context has a unique
437    /// index to prevent ref collisions across contexts.
438    pub fn index(&self) -> usize {
439        self.context_index
440    }
441
442    /// Get the next page index for this context.
443    ///
444    /// This is called internally when creating a new page to assign
445    /// a unique index within this context.
446    pub(crate) fn next_page_index(&self) -> usize {
447        self.page_index_counter
448            .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
449    }
450
451    /// Check if this context has been closed.
452    pub fn is_closed(&self) -> bool {
453        self.closed
454    }
455
456    /// Get a reference to the CDP connection.
457    pub fn connection(&self) -> &Arc<CdpConnection> {
458        &self.connection
459    }
460
461    /// Get the context ID.
462    pub fn context_id(&self) -> &str {
463        &self.context_id
464    }
465
466    // Web error event methods are in weberror.rs (on_weberror, off_weberror)
467
468    // Page and close event methods are in page_events.rs (on_page, off_page, on_close, off_close, wait_for_page)
469
470    // Context-level routing methods are in routing_impl.rs
471
472    // HAR recording methods are in har.rs
473
474    // Exposed function methods are in binding.rs (expose_function, remove_exposed_function)
475
476    // API request context methods are in api.rs (request, sync_cookies_from_api)
477
478    // Tracing method is in tracing_access.rs
479
480    // Test ID attribute methods are in test_id.rs
481}
482
483// ClearCookiesBuilder is in cookies.rs
484// SetGeolocationBuilder is in emulation.rs