1mod api;
4pub mod binding;
5mod cookies;
6mod emulation;
7pub mod events;
8mod har;
9mod page_events;
10mod page_factory;
11mod permissions;
12pub mod routing;
13mod routing_impl;
14mod scripts;
15pub mod storage;
16mod storage_restore;
17mod test_id;
18pub mod trace;
19mod tracing_access;
20pub mod types;
21mod weberror;
22
23pub use cookies::ClearCookiesBuilder;
24pub use emulation::SetGeolocationBuilder;
25
26use std::sync::Arc;
28use std::time::Duration;
29
30use tokio::sync::RwLock;
31use tracing::{debug, info, instrument};
32
33use viewpoint_cdp::CdpConnection;
34use viewpoint_cdp::protocol::target_domain::{
35 DisposeBrowserContextParams, GetTargetsParams, GetTargetsResult,
36};
37
38use crate::error::ContextError;
39use crate::page::Page;
40
41pub use events::{ContextEventManager, HandlerId};
42pub use storage::{StorageStateBuilder, StorageStateOptions};
43use trace::TracingState;
44pub use trace::{Tracing, TracingOptions};
45pub use types::{
46 ColorScheme, ContextOptions, ContextOptionsBuilder, Cookie, ForcedColors, Geolocation,
47 HttpCredentials, IndexedDbDatabase, IndexedDbEntry, IndexedDbIndex, IndexedDbObjectStore,
48 LocalStorageEntry, Permission, ReducedMotion, SameSite, StorageOrigin, StorageState,
49 StorageStateSource, ViewportSize,
50};
51pub use weberror::WebErrorHandler;
52pub use crate::page::page_error::WebError;
54
55pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
57
58pub struct BrowserContext {
82 connection: Arc<CdpConnection>,
84 context_id: String,
86 closed: bool,
88 owned: bool,
91 pages: Arc<RwLock<Vec<PageInfo>>>,
93 default_timeout: Duration,
95 default_navigation_timeout: Duration,
97 options: ContextOptions,
99 weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
101 event_manager: Arc<ContextEventManager>,
103 route_registry: Arc<routing::ContextRouteRegistry>,
105 binding_registry: Arc<binding::ContextBindingRegistry>,
107 init_scripts: Arc<RwLock<Vec<String>>>,
109 test_id_attribute: Arc<RwLock<String>>,
111 har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
113 tracing_state: Arc<RwLock<TracingState>>,
115}
116
117impl std::fmt::Debug for BrowserContext {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("BrowserContext")
121 .field("context_id", &self.context_id)
122 .field("closed", &self.closed)
123 .field("owned", &self.owned)
124 .field("default_timeout", &self.default_timeout)
125 .field(
126 "default_navigation_timeout",
127 &self.default_navigation_timeout,
128 )
129 .finish_non_exhaustive()
130 }
131}
132
133#[derive(Debug, Clone)]
135pub struct PageInfo {
136 pub target_id: String,
138 pub session_id: String,
140}
141
142impl BrowserContext {
143 pub(crate) fn new(connection: Arc<CdpConnection>, context_id: String) -> Self {
145 debug!(context_id = %context_id, "Created BrowserContext");
146 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
147 connection.clone(),
148 context_id.clone(),
149 ));
150 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
151 let ctx = Self {
152 connection: connection.clone(),
153 context_id: context_id.clone(),
154 closed: false,
155 owned: true, pages: Arc::new(RwLock::new(Vec::new())),
157 default_timeout: Duration::from_secs(30),
158 default_navigation_timeout: Duration::from_secs(30),
159 options: ContextOptions::default(),
160 weberror_handler: Arc::new(RwLock::new(None)),
161 event_manager: Arc::new(ContextEventManager::new()),
162 route_registry,
163 binding_registry,
164 init_scripts: Arc::new(RwLock::new(Vec::new())),
165 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
166 har_recorder: Arc::new(RwLock::new(None)),
167 tracing_state: Arc::new(RwLock::new(TracingState::default())),
168 };
169 ctx.start_weberror_listener();
170 ctx
171 }
172
173 pub(crate) fn with_options(
175 connection: Arc<CdpConnection>,
176 context_id: String,
177 options: ContextOptions,
178 ) -> Self {
179 debug!(context_id = %context_id, "Created BrowserContext with options");
180 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
181 connection.clone(),
182 context_id.clone(),
183 ));
184 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
185 let ctx = Self {
186 connection: connection.clone(),
187 context_id: context_id.clone(),
188 closed: false,
189 owned: true, pages: Arc::new(RwLock::new(Vec::new())),
191 default_timeout: options.default_timeout.unwrap_or(Duration::from_secs(30)),
192 default_navigation_timeout: options
193 .default_navigation_timeout
194 .unwrap_or(Duration::from_secs(30)),
195 options,
196 weberror_handler: Arc::new(RwLock::new(None)),
197 event_manager: Arc::new(ContextEventManager::new()),
198 route_registry,
199 binding_registry,
200 init_scripts: Arc::new(RwLock::new(Vec::new())),
201 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
202 har_recorder: Arc::new(RwLock::new(None)),
203 tracing_state: Arc::new(RwLock::new(TracingState::default())),
204 };
205 ctx.start_weberror_listener();
206 ctx
207 }
208
209 pub(crate) fn from_existing(connection: Arc<CdpConnection>, context_id: String) -> Self {
215 let is_default = context_id.is_empty();
216 debug!(context_id = %context_id, is_default = is_default, "Wrapping existing BrowserContext");
217 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
218 connection.clone(),
219 context_id.clone(),
220 ));
221 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
222 let ctx = Self {
223 connection: connection.clone(),
224 context_id: context_id.clone(),
225 closed: false,
226 owned: false, pages: Arc::new(RwLock::new(Vec::new())),
228 default_timeout: Duration::from_secs(30),
229 default_navigation_timeout: Duration::from_secs(30),
230 options: ContextOptions::default(),
231 weberror_handler: Arc::new(RwLock::new(None)),
232 event_manager: Arc::new(ContextEventManager::new()),
233 route_registry,
234 binding_registry,
235 init_scripts: Arc::new(RwLock::new(Vec::new())),
236 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
237 har_recorder: Arc::new(RwLock::new(None)),
238 tracing_state: Arc::new(RwLock::new(TracingState::default())),
239 };
240 ctx.start_weberror_listener();
241 ctx
242 }
243
244 pub(crate) async fn apply_options(&self) -> Result<(), ContextError> {
251 if let Some(ref geo) = self.options.geolocation {
253 self.set_geolocation(geo.latitude, geo.longitude)
254 .accuracy(geo.accuracy)
255 .await?;
256 }
257
258 if !self.options.permissions.is_empty() {
260 self.grant_permissions(self.options.permissions.clone())
261 .await?;
262 }
263
264 if !self.options.extra_http_headers.is_empty() {
266 self.set_extra_http_headers(self.options.extra_http_headers.clone())
267 .await?;
268 }
269
270 if self.options.offline {
272 self.set_offline(true).await?;
273 }
274
275 Ok(())
276 }
277
278 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
288 pub async fn new_page(&self) -> Result<Page, ContextError> {
289 if self.closed {
290 return Err(ContextError::Closed);
291 }
292
293 info!("Creating new page");
294
295 let (create_result, attach_result) =
297 page_factory::create_and_attach_target(&self.connection, &self.context_id).await?;
298
299 let target_id = &create_result.target_id;
300 let session_id = &attach_result.session_id;
301
302 page_factory::enable_page_domains(&self.connection, session_id).await?;
304
305 page_factory::apply_emulation_settings(&self.connection, session_id, &self.options).await?;
307
308 let frame_id = page_factory::get_main_frame_id(&self.connection, session_id).await?;
310
311 page_factory::track_page(
313 &self.pages,
314 create_result.target_id.clone(),
315 attach_result.session_id.clone(),
316 )
317 .await;
318
319 if let Err(e) = self.apply_init_scripts_to_session(session_id).await {
321 debug!("Failed to apply init scripts: {}", e);
322 }
323
324 info!(target_id = %target_id, session_id = %session_id, frame_id = %frame_id, "Page created successfully");
325
326 let test_id_attr = self.test_id_attribute.read().await.clone();
328
329 let http_credentials = page_factory::convert_http_credentials(&self.options);
331
332 let page = page_factory::create_page_instance(
334 self.connection.clone(),
335 create_result,
336 attach_result,
337 frame_id,
338 &self.options,
339 test_id_attr,
340 self.route_registry.clone(),
341 http_credentials,
342 )
343 .await;
344
345 if let Err(e) = page.enable_fetch_for_context_routes().await {
348 debug!("Failed to enable Fetch for context routes: {}", e);
349 }
350
351 self.event_manager.emit_page(page.clone_internal()).await;
353
354 Ok(page)
355 }
356
357 pub async fn pages(&self) -> Result<Vec<PageInfo>, ContextError> {
363 if self.closed {
364 return Err(ContextError::Closed);
365 }
366
367 let result: GetTargetsResult = self
368 .connection
369 .send_command("Target.getTargets", Some(GetTargetsParams::default()), None)
370 .await?;
371
372 let pages: Vec<PageInfo> = result
373 .target_infos
374 .into_iter()
375 .filter(|t| {
376 let matches_context = if self.context_id.is_empty() {
379 t.browser_context_id.as_deref().is_none()
381 || t.browser_context_id.as_deref() == Some("")
382 } else {
383 t.browser_context_id.as_deref() == Some(&self.context_id)
385 };
386 matches_context && t.target_type == "page"
387 })
388 .map(|t| PageInfo {
389 target_id: t.target_id,
390 session_id: String::new(), })
392 .collect();
393
394 Ok(pages)
395 }
396
397 pub fn is_owned(&self) -> bool {
402 self.owned
403 }
404
405 pub fn is_default(&self) -> bool {
409 self.context_id.is_empty()
410 }
411
412 pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
440 SetGeolocationBuilder::new(self, latitude, longitude)
441 }
442
443 pub fn set_default_timeout(&mut self, timeout: Duration) {
453 self.default_timeout = timeout;
454 }
455
456 pub fn default_timeout(&self) -> Duration {
458 self.default_timeout
459 }
460
461 pub fn set_default_navigation_timeout(&mut self, timeout: Duration) {
465 self.default_navigation_timeout = timeout;
466 }
467
468 pub fn default_navigation_timeout(&self) -> Duration {
470 self.default_navigation_timeout
471 }
472
473 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id, owned = self.owned))]
489 pub async fn close(&mut self) -> Result<(), ContextError> {
490 if self.closed {
491 debug!("Context already closed");
492 return Ok(());
493 }
494
495 info!("Closing browser context");
496
497 if let Some(recorder) = self.har_recorder.write().await.take() {
499 if let Err(e) = recorder.save().await {
500 debug!("Failed to auto-save HAR on close: {}", e);
501 } else {
502 debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
503 }
504 }
505
506 self.event_manager.emit_close().await;
508
509 if self.owned && !self.context_id.is_empty() {
512 debug!("Disposing owned browser context");
513 self.connection
514 .send_command::<_, serde_json::Value>(
515 "Target.disposeBrowserContext",
516 Some(DisposeBrowserContextParams {
517 browser_context_id: self.context_id.clone(),
518 }),
519 None,
520 )
521 .await?;
522 } else {
523 debug!("Skipping dispose for external/default context");
524 }
525
526 self.event_manager.clear().await;
528
529 self.closed = true;
530 info!("Browser context closed");
531 Ok(())
532 }
533
534 pub fn id(&self) -> &str {
536 &self.context_id
537 }
538
539 pub fn is_closed(&self) -> bool {
541 self.closed
542 }
543
544 pub fn connection(&self) -> &Arc<CdpConnection> {
546 &self.connection
547 }
548
549 pub fn context_id(&self) -> &str {
551 &self.context_id
552 }
553
554 }
570
571