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;
17pub mod trace;
18mod test_id;
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::protocol::target_domain::{
34 DisposeBrowserContextParams, GetTargetsParams, GetTargetsResult,
35};
36use viewpoint_cdp::CdpConnection;
37
38use crate::error::ContextError;
39use crate::page::Page;
40
41pub use events::{ContextEventManager, HandlerId};
42pub use weberror::WebErrorHandler;
43pub use storage::{StorageStateBuilder, StorageStateOptions};
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,
49 StorageState, StorageStateSource, ViewportSize,
50};
51pub use crate::page::page_error::WebError;
53
54pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
56
57pub struct BrowserContext {
75 connection: Arc<CdpConnection>,
77 context_id: String,
79 closed: bool,
81 pages: Arc<RwLock<Vec<PageInfo>>>,
83 default_timeout: Duration,
85 default_navigation_timeout: Duration,
87 options: ContextOptions,
89 weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
91 event_manager: Arc<ContextEventManager>,
93 route_registry: Arc<routing::ContextRouteRegistry>,
95 binding_registry: Arc<binding::ContextBindingRegistry>,
97 init_scripts: Arc<RwLock<Vec<String>>>,
99 test_id_attribute: Arc<RwLock<String>>,
101 har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
103}
104
105impl std::fmt::Debug for BrowserContext {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 f.debug_struct("BrowserContext")
109 .field("context_id", &self.context_id)
110 .field("closed", &self.closed)
111 .field("default_timeout", &self.default_timeout)
112 .field("default_navigation_timeout", &self.default_navigation_timeout)
113 .finish_non_exhaustive()
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct PageInfo {
120 pub target_id: String,
122 pub session_id: String,
124}
125
126impl BrowserContext {
127 pub(crate) fn new(connection: Arc<CdpConnection>, context_id: String) -> Self {
129 debug!(context_id = %context_id, "Created BrowserContext");
130 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
131 connection.clone(),
132 context_id.clone(),
133 ));
134 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
135 let ctx = Self {
136 connection: connection.clone(),
137 context_id: context_id.clone(),
138 closed: false,
139 pages: Arc::new(RwLock::new(Vec::new())),
140 default_timeout: Duration::from_secs(30),
141 default_navigation_timeout: Duration::from_secs(30),
142 options: ContextOptions::default(),
143 weberror_handler: Arc::new(RwLock::new(None)),
144 event_manager: Arc::new(ContextEventManager::new()),
145 route_registry,
146 binding_registry,
147 init_scripts: Arc::new(RwLock::new(Vec::new())),
148 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
149 har_recorder: Arc::new(RwLock::new(None)),
150 };
151 ctx.start_weberror_listener();
152 ctx
153 }
154
155 pub(crate) fn with_options(
157 connection: Arc<CdpConnection>,
158 context_id: String,
159 options: ContextOptions,
160 ) -> Self {
161 debug!(context_id = %context_id, "Created BrowserContext with options");
162 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
163 connection.clone(),
164 context_id.clone(),
165 ));
166 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
167 let ctx = Self {
168 connection: connection.clone(),
169 context_id: context_id.clone(),
170 closed: false,
171 pages: Arc::new(RwLock::new(Vec::new())),
172 default_timeout: options.default_timeout.unwrap_or(Duration::from_secs(30)),
173 default_navigation_timeout: options
174 .default_navigation_timeout
175 .unwrap_or(Duration::from_secs(30)),
176 options,
177 weberror_handler: Arc::new(RwLock::new(None)),
178 event_manager: Arc::new(ContextEventManager::new()),
179 route_registry,
180 binding_registry,
181 init_scripts: Arc::new(RwLock::new(Vec::new())),
182 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
183 har_recorder: Arc::new(RwLock::new(None)),
184 };
185 ctx.start_weberror_listener();
186 ctx
187 }
188
189 pub(crate) async fn apply_options(&self) -> Result<(), ContextError> {
196 if let Some(ref geo) = self.options.geolocation {
198 self.set_geolocation(geo.latitude, geo.longitude)
199 .accuracy(geo.accuracy)
200 .await?;
201 }
202
203 if !self.options.permissions.is_empty() {
205 self.grant_permissions(self.options.permissions.clone())
206 .await?;
207 }
208
209 if !self.options.extra_http_headers.is_empty() {
211 self.set_extra_http_headers(self.options.extra_http_headers.clone())
212 .await?;
213 }
214
215 if self.options.offline {
217 self.set_offline(true).await?;
218 }
219
220 Ok(())
221 }
222
223 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
233 pub async fn new_page(&self) -> Result<Page, ContextError> {
234 if self.closed {
235 return Err(ContextError::Closed);
236 }
237
238 info!("Creating new page");
239
240 let (create_result, attach_result) =
242 page_factory::create_and_attach_target(&self.connection, &self.context_id).await?;
243
244 let target_id = &create_result.target_id;
245 let session_id = &attach_result.session_id;
246
247 page_factory::enable_page_domains(&self.connection, session_id).await?;
249
250 page_factory::apply_emulation_settings(&self.connection, session_id, &self.options).await?;
252
253 let frame_id = page_factory::get_main_frame_id(&self.connection, session_id).await?;
255
256 page_factory::track_page(
258 &self.pages,
259 create_result.target_id.clone(),
260 attach_result.session_id.clone(),
261 )
262 .await;
263
264 if let Err(e) = self.apply_init_scripts_to_session(session_id).await {
266 debug!("Failed to apply init scripts: {}", e);
267 }
268
269 info!(target_id = %target_id, session_id = %session_id, frame_id = %frame_id, "Page created successfully");
270
271 let test_id_attr = self.test_id_attribute.read().await.clone();
273
274 let http_credentials = page_factory::convert_http_credentials(&self.options);
276
277 let page = page_factory::create_page_instance(
279 self.connection.clone(),
280 create_result,
281 attach_result,
282 frame_id,
283 &self.options,
284 test_id_attr,
285 self.route_registry.clone(),
286 http_credentials,
287 )
288 .await;
289
290 if let Err(e) = page.enable_fetch_for_context_routes().await {
293 debug!("Failed to enable Fetch for context routes: {}", e);
294 }
295
296 self.event_manager.emit_page(page.clone_internal()).await;
298
299 Ok(page)
300 }
301
302 pub async fn pages(&self) -> Result<Vec<PageInfo>, ContextError> {
308 if self.closed {
309 return Err(ContextError::Closed);
310 }
311
312 let result: GetTargetsResult = self
313 .connection
314 .send_command("Target.getTargets", Some(GetTargetsParams::default()), None)
315 .await?;
316
317 let pages: Vec<PageInfo> = result
318 .target_infos
319 .into_iter()
320 .filter(|t| {
321 t.browser_context_id.as_deref() == Some(&self.context_id)
322 && t.target_type == "page"
323 })
324 .map(|t| PageInfo {
325 target_id: t.target_id,
326 session_id: String::new(), })
328 .collect();
329
330 Ok(pages)
331 }
332
333 pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
361 SetGeolocationBuilder::new(self, latitude, longitude)
362 }
363
364 pub fn set_default_timeout(&mut self, timeout: Duration) {
374 self.default_timeout = timeout;
375 }
376
377 pub fn default_timeout(&self) -> Duration {
379 self.default_timeout
380 }
381
382 pub fn set_default_navigation_timeout(&mut self, timeout: Duration) {
386 self.default_navigation_timeout = timeout;
387 }
388
389 pub fn default_navigation_timeout(&self) -> Duration {
391 self.default_navigation_timeout
392 }
393
394 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
406 pub async fn close(&mut self) -> Result<(), ContextError> {
407 if self.closed {
408 debug!("Context already closed");
409 return Ok(());
410 }
411
412 info!("Closing browser context");
413
414 if let Some(recorder) = self.har_recorder.write().await.take() {
416 if let Err(e) = recorder.save().await {
417 debug!("Failed to auto-save HAR on close: {}", e);
418 } else {
419 debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
420 }
421 }
422
423 self.event_manager.emit_close().await;
425
426 self.connection
427 .send_command::<_, serde_json::Value>(
428 "Target.disposeBrowserContext",
429 Some(DisposeBrowserContextParams {
430 browser_context_id: self.context_id.clone(),
431 }),
432 None,
433 )
434 .await?;
435
436 self.event_manager.clear().await;
438
439 self.closed = true;
440 info!("Browser context closed");
441 Ok(())
442 }
443
444 pub fn id(&self) -> &str {
446 &self.context_id
447 }
448
449 pub fn is_closed(&self) -> bool {
451 self.closed
452 }
453
454 pub fn connection(&self) -> &Arc<CdpConnection> {
456 &self.connection
457 }
458
459 pub fn context_id(&self) -> &str {
461 &self.context_id
462 }
463
464 }
480
481