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 test_id;
176pub mod trace;
177mod tracing_access;
178pub mod types;
179mod weberror;
180
181pub use cookies::ClearCookiesBuilder;
182pub use emulation::SetGeolocationBuilder;
183
184// HashMap is used in emulation.rs
185use std::sync::Arc;
186use std::time::Duration;
187
188use tokio::sync::RwLock;
189use tracing::{debug, info, instrument};
190
191use viewpoint_cdp::CdpConnection;
192use viewpoint_cdp::protocol::target_domain::DisposeBrowserContextParams;
193
194use crate::error::ContextError;
195
196pub use events::{ContextEventManager, HandlerId};
197pub use storage::{StorageStateBuilder, StorageStateOptions};
198use trace::TracingState;
199pub use trace::{Tracing, TracingOptions};
200pub use types::{
201 ColorScheme, ContextOptions, ContextOptionsBuilder, Cookie, ForcedColors, Geolocation,
202 HttpCredentials, IndexedDbDatabase, IndexedDbEntry, IndexedDbIndex, IndexedDbObjectStore,
203 LocalStorageEntry, Permission, ReducedMotion, SameSite, StorageOrigin, StorageState,
204 StorageStateSource, ViewportSize,
205};
206pub use weberror::WebErrorHandler;
207// Re-export WebError for context-level usage
208pub use crate::page::page_error::WebError;
209
210/// Default test ID attribute name.
211pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
212
213/// An isolated browser context.
214///
215/// Browser contexts are similar to incognito windows - they have their own
216/// cookies, cache, and storage that are isolated from other contexts.
217///
218/// # Features
219///
220/// - **Cookie Management**: Add, get, and clear cookies
221/// - **Storage State**: Save and restore browser state
222/// - **Permissions**: Grant permissions like geolocation, camera, etc.
223/// - **Geolocation**: Mock browser location
224/// - **HTTP Credentials**: Basic/Digest authentication
225/// - **Extra Headers**: Add headers to all requests
226/// - **Offline Mode**: Simulate network offline conditions
227/// - **Event Handling**: Listen for page creation and context close events
228/// - **Init Scripts**: Scripts that run before every page load
229/// - **Custom Test ID**: Configure which attribute is used for test IDs
230///
231/// # Ownership
232///
233/// Contexts can be either "owned" (created by us) or "external" (discovered when
234/// connecting to an existing browser). When closing an external context, the
235/// underlying browser context is not disposed - only our connection to it is closed.
236pub struct BrowserContext {
237 /// CDP connection.
238 connection: Arc<CdpConnection>,
239 /// Browser context ID.
240 context_id: String,
241 /// Whether the context has been closed.
242 closed: bool,
243 /// Whether we own this context (created it) vs discovered it.
244 /// Owned contexts are disposed when closed; external contexts are not.
245 owned: bool,
246 /// Created pages (weak tracking for `pages()` method).
247 pages: Arc<RwLock<Vec<PageInfo>>>,
248 /// Default timeout for actions.
249 default_timeout: Duration,
250 /// Default timeout for navigation.
251 default_navigation_timeout: Duration,
252 /// Context options used to create this context.
253 options: ContextOptions,
254 /// Web error handler.
255 weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
256 /// Event manager for context-level events.
257 event_manager: Arc<ContextEventManager>,
258 /// Context-level route registry.
259 route_registry: Arc<routing::ContextRouteRegistry>,
260 /// Context-level binding registry.
261 binding_registry: Arc<binding::ContextBindingRegistry>,
262 /// Init scripts to run on every page load.
263 init_scripts: Arc<RwLock<Vec<String>>>,
264 /// Custom test ID attribute name (defaults to "data-testid").
265 test_id_attribute: Arc<RwLock<String>>,
266 /// HAR recorder for capturing network traffic.
267 har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
268 /// Shared tracing state for persistent tracing across `tracing()` calls.
269 tracing_state: Arc<RwLock<TracingState>>,
270}
271
272// Manual Debug implementation since WebErrorHandler doesn't implement Debug
273impl std::fmt::Debug for BrowserContext {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 f.debug_struct("BrowserContext")
276 .field("context_id", &self.context_id)
277 .field("closed", &self.closed)
278 .field("owned", &self.owned)
279 .field("default_timeout", &self.default_timeout)
280 .field(
281 "default_navigation_timeout",
282 &self.default_navigation_timeout,
283 )
284 .finish_non_exhaustive()
285 }
286}
287
288/// Information about a page in the context.
289#[derive(Debug, Clone)]
290pub struct PageInfo {
291 /// Target ID.
292 pub target_id: String,
293 /// Session ID (may be empty if not tracked).
294 pub session_id: String,
295}
296
297impl BrowserContext {
298 // Construction methods (new, with_options, from_existing, apply_options) are in construction.rs
299
300 // Page management methods (new_page, pages) are in page_management.rs
301
302 // Cookie methods are in cookies.rs
303
304 // Storage state methods are in storage.rs
305
306 // Permissions methods are in permissions.rs
307
308 // =========================================================================
309 // Geolocation
310 // =========================================================================
311
312 /// Set the geolocation.
313 ///
314 /// # Example
315 ///
316 /// ```no_run
317 /// use viewpoint_core::BrowserContext;
318 ///
319 /// # async fn example(context: &BrowserContext) -> Result<(), viewpoint_core::CoreError> {
320 /// // San Francisco
321 /// context.set_geolocation(37.7749, -122.4194).await?;
322 /// # Ok(())
323 /// # }
324 /// ```
325 ///
326 /// # Errors
327 ///
328 /// Returns an error if setting geolocation fails.
329 pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
330 SetGeolocationBuilder::new(self, latitude, longitude)
331 }
332
333 // clear_geolocation, set_extra_http_headers, set_offline are in emulation.rs
334
335 // =========================================================================
336 // Ownership and Status
337 // =========================================================================
338
339 /// Check if this context is owned (created by us) or external.
340 ///
341 /// External contexts are discovered when connecting to an already-running browser.
342 /// They are not disposed when closed.
343 pub fn is_owned(&self) -> bool {
344 self.owned
345 }
346
347 /// Check if this is the default browser context.
348 ///
349 /// The default context represents the browser's main profile and has an empty ID.
350 pub fn is_default(&self) -> bool {
351 self.context_id.is_empty()
352 }
353
354 // =========================================================================
355 // Timeout Configuration
356 // =========================================================================
357
358 /// Set the default timeout for actions.
359 ///
360 /// This timeout is used for actions like clicking, typing, etc.
361 pub fn set_default_timeout(&mut self, timeout: Duration) {
362 self.default_timeout = timeout;
363 }
364
365 /// Get the default timeout for actions.
366 pub fn default_timeout(&self) -> Duration {
367 self.default_timeout
368 }
369
370 /// Set the default navigation timeout.
371 ///
372 /// This timeout is used for navigation operations like goto, reload, etc.
373 pub fn set_default_navigation_timeout(&mut self, timeout: Duration) {
374 self.default_navigation_timeout = timeout;
375 }
376
377 /// Get the default navigation timeout.
378 pub fn default_navigation_timeout(&self) -> Duration {
379 self.default_navigation_timeout
380 }
381
382 // Init script methods are in scripts.rs
383
384 // =========================================================================
385 // Context Lifecycle
386 // =========================================================================
387
388 /// Close this browser context and all its pages.
389 ///
390 /// For contexts we created (owned), this disposes the context via CDP.
391 /// For external contexts (discovered when connecting to an existing browser),
392 /// this only closes our connection without disposing the context.
393 ///
394 /// # Errors
395 ///
396 /// Returns an error if closing fails.
397 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id, owned = self.owned))]
398 pub async fn close(&mut self) -> Result<(), ContextError> {
399 if self.closed {
400 debug!("Context already closed");
401 return Ok(());
402 }
403
404 info!("Closing browser context");
405
406 // Auto-save HAR if recording is active
407 if let Some(recorder) = self.har_recorder.write().await.take() {
408 if let Err(e) = recorder.save().await {
409 debug!("Failed to auto-save HAR on close: {}", e);
410 } else {
411 debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
412 }
413 }
414
415 // Emit close event before cleanup
416 self.event_manager.emit_close().await;
417
418 // Only dispose the context if we own it
419 // External contexts (from connecting to existing browser) should not be disposed
420 if self.owned && !self.context_id.is_empty() {
421 debug!("Disposing owned browser context");
422 self.connection
423 .send_command::<_, serde_json::Value>(
424 "Target.disposeBrowserContext",
425 Some(DisposeBrowserContextParams {
426 browser_context_id: self.context_id.clone(),
427 }),
428 None,
429 )
430 .await?;
431 } else {
432 debug!("Skipping dispose for external/default context");
433 }
434
435 // Clear all event handlers
436 self.event_manager.clear().await;
437
438 self.closed = true;
439 info!("Browser context closed");
440 Ok(())
441 }
442
443 /// Get the context ID.
444 pub fn id(&self) -> &str {
445 &self.context_id
446 }
447
448 /// Check if this context has been closed.
449 pub fn is_closed(&self) -> bool {
450 self.closed
451 }
452
453 /// Get a reference to the CDP connection.
454 pub fn connection(&self) -> &Arc<CdpConnection> {
455 &self.connection
456 }
457
458 /// Get the context ID.
459 pub fn context_id(&self) -> &str {
460 &self.context_id
461 }
462
463 // Web error event methods are in weberror.rs (on_weberror, off_weberror)
464
465 // Page and close event methods are in page_events.rs (on_page, off_page, on_close, off_close, wait_for_page)
466
467 // Context-level routing methods are in routing_impl.rs
468
469 // HAR recording methods are in har.rs
470
471 // Exposed function methods are in binding.rs (expose_function, remove_exposed_function)
472
473 // API request context methods are in api.rs (request, sync_cookies_from_api)
474
475 // Tracing method is in tracing_access.rs
476
477 // Test ID attribute methods are in test_id.rs
478}
479
480// ClearCookiesBuilder is in cookies.rs
481// SetGeolocationBuilder is in emulation.rs