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};
45use trace::TracingState;
46pub use types::{
47 ColorScheme, ContextOptions, ContextOptionsBuilder, Cookie, ForcedColors, Geolocation,
48 HttpCredentials, IndexedDbDatabase, IndexedDbEntry, IndexedDbIndex, IndexedDbObjectStore,
49 LocalStorageEntry, Permission, ReducedMotion, SameSite, StorageOrigin,
50 StorageState, StorageStateSource, ViewportSize,
51};
52pub use crate::page::page_error::WebError;
54
55pub const DEFAULT_TEST_ID_ATTRIBUTE: &str = "data-testid";
57
58pub struct BrowserContext {
76 connection: Arc<CdpConnection>,
78 context_id: String,
80 closed: bool,
82 pages: Arc<RwLock<Vec<PageInfo>>>,
84 default_timeout: Duration,
86 default_navigation_timeout: Duration,
88 options: ContextOptions,
90 weberror_handler: Arc<RwLock<Option<WebErrorHandler>>>,
92 event_manager: Arc<ContextEventManager>,
94 route_registry: Arc<routing::ContextRouteRegistry>,
96 binding_registry: Arc<binding::ContextBindingRegistry>,
98 init_scripts: Arc<RwLock<Vec<String>>>,
100 test_id_attribute: Arc<RwLock<String>>,
102 har_recorder: Arc<RwLock<Option<crate::network::HarRecorder>>>,
104 tracing_state: Arc<RwLock<TracingState>>,
106}
107
108impl std::fmt::Debug for BrowserContext {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("BrowserContext")
112 .field("context_id", &self.context_id)
113 .field("closed", &self.closed)
114 .field("default_timeout", &self.default_timeout)
115 .field("default_navigation_timeout", &self.default_navigation_timeout)
116 .finish_non_exhaustive()
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct PageInfo {
123 pub target_id: String,
125 pub session_id: String,
127}
128
129impl BrowserContext {
130 pub(crate) fn new(connection: Arc<CdpConnection>, context_id: String) -> Self {
132 debug!(context_id = %context_id, "Created BrowserContext");
133 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
134 connection.clone(),
135 context_id.clone(),
136 ));
137 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
138 let ctx = Self {
139 connection: connection.clone(),
140 context_id: context_id.clone(),
141 closed: false,
142 pages: Arc::new(RwLock::new(Vec::new())),
143 default_timeout: Duration::from_secs(30),
144 default_navigation_timeout: Duration::from_secs(30),
145 options: ContextOptions::default(),
146 weberror_handler: Arc::new(RwLock::new(None)),
147 event_manager: Arc::new(ContextEventManager::new()),
148 route_registry,
149 binding_registry,
150 init_scripts: Arc::new(RwLock::new(Vec::new())),
151 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
152 har_recorder: Arc::new(RwLock::new(None)),
153 tracing_state: Arc::new(RwLock::new(TracingState::default())),
154 };
155 ctx.start_weberror_listener();
156 ctx
157 }
158
159 pub(crate) fn with_options(
161 connection: Arc<CdpConnection>,
162 context_id: String,
163 options: ContextOptions,
164 ) -> Self {
165 debug!(context_id = %context_id, "Created BrowserContext with options");
166 let route_registry = Arc::new(routing::ContextRouteRegistry::new(
167 connection.clone(),
168 context_id.clone(),
169 ));
170 let binding_registry = Arc::new(binding::ContextBindingRegistry::new());
171 let ctx = Self {
172 connection: connection.clone(),
173 context_id: context_id.clone(),
174 closed: false,
175 pages: Arc::new(RwLock::new(Vec::new())),
176 default_timeout: options.default_timeout.unwrap_or(Duration::from_secs(30)),
177 default_navigation_timeout: options
178 .default_navigation_timeout
179 .unwrap_or(Duration::from_secs(30)),
180 options,
181 weberror_handler: Arc::new(RwLock::new(None)),
182 event_manager: Arc::new(ContextEventManager::new()),
183 route_registry,
184 binding_registry,
185 init_scripts: Arc::new(RwLock::new(Vec::new())),
186 test_id_attribute: Arc::new(RwLock::new(DEFAULT_TEST_ID_ATTRIBUTE.to_string())),
187 har_recorder: Arc::new(RwLock::new(None)),
188 tracing_state: Arc::new(RwLock::new(TracingState::default())),
189 };
190 ctx.start_weberror_listener();
191 ctx
192 }
193
194 pub(crate) async fn apply_options(&self) -> Result<(), ContextError> {
201 if let Some(ref geo) = self.options.geolocation {
203 self.set_geolocation(geo.latitude, geo.longitude)
204 .accuracy(geo.accuracy)
205 .await?;
206 }
207
208 if !self.options.permissions.is_empty() {
210 self.grant_permissions(self.options.permissions.clone())
211 .await?;
212 }
213
214 if !self.options.extra_http_headers.is_empty() {
216 self.set_extra_http_headers(self.options.extra_http_headers.clone())
217 .await?;
218 }
219
220 if self.options.offline {
222 self.set_offline(true).await?;
223 }
224
225 Ok(())
226 }
227
228 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
238 pub async fn new_page(&self) -> Result<Page, ContextError> {
239 if self.closed {
240 return Err(ContextError::Closed);
241 }
242
243 info!("Creating new page");
244
245 let (create_result, attach_result) =
247 page_factory::create_and_attach_target(&self.connection, &self.context_id).await?;
248
249 let target_id = &create_result.target_id;
250 let session_id = &attach_result.session_id;
251
252 page_factory::enable_page_domains(&self.connection, session_id).await?;
254
255 page_factory::apply_emulation_settings(&self.connection, session_id, &self.options).await?;
257
258 let frame_id = page_factory::get_main_frame_id(&self.connection, session_id).await?;
260
261 page_factory::track_page(
263 &self.pages,
264 create_result.target_id.clone(),
265 attach_result.session_id.clone(),
266 )
267 .await;
268
269 if let Err(e) = self.apply_init_scripts_to_session(session_id).await {
271 debug!("Failed to apply init scripts: {}", e);
272 }
273
274 info!(target_id = %target_id, session_id = %session_id, frame_id = %frame_id, "Page created successfully");
275
276 let test_id_attr = self.test_id_attribute.read().await.clone();
278
279 let http_credentials = page_factory::convert_http_credentials(&self.options);
281
282 let page = page_factory::create_page_instance(
284 self.connection.clone(),
285 create_result,
286 attach_result,
287 frame_id,
288 &self.options,
289 test_id_attr,
290 self.route_registry.clone(),
291 http_credentials,
292 )
293 .await;
294
295 if let Err(e) = page.enable_fetch_for_context_routes().await {
298 debug!("Failed to enable Fetch for context routes: {}", e);
299 }
300
301 self.event_manager.emit_page(page.clone_internal()).await;
303
304 Ok(page)
305 }
306
307 pub async fn pages(&self) -> Result<Vec<PageInfo>, ContextError> {
313 if self.closed {
314 return Err(ContextError::Closed);
315 }
316
317 let result: GetTargetsResult = self
318 .connection
319 .send_command("Target.getTargets", Some(GetTargetsParams::default()), None)
320 .await?;
321
322 let pages: Vec<PageInfo> = result
323 .target_infos
324 .into_iter()
325 .filter(|t| {
326 t.browser_context_id.as_deref() == Some(&self.context_id)
327 && t.target_type == "page"
328 })
329 .map(|t| PageInfo {
330 target_id: t.target_id,
331 session_id: String::new(), })
333 .collect();
334
335 Ok(pages)
336 }
337
338 pub fn set_geolocation(&self, latitude: f64, longitude: f64) -> SetGeolocationBuilder<'_> {
366 SetGeolocationBuilder::new(self, latitude, longitude)
367 }
368
369 pub fn set_default_timeout(&mut self, timeout: Duration) {
379 self.default_timeout = timeout;
380 }
381
382 pub fn default_timeout(&self) -> Duration {
384 self.default_timeout
385 }
386
387 pub fn set_default_navigation_timeout(&mut self, timeout: Duration) {
391 self.default_navigation_timeout = timeout;
392 }
393
394 pub fn default_navigation_timeout(&self) -> Duration {
396 self.default_navigation_timeout
397 }
398
399 #[instrument(level = "info", skip(self), fields(context_id = %self.context_id))]
411 pub async fn close(&mut self) -> Result<(), ContextError> {
412 if self.closed {
413 debug!("Context already closed");
414 return Ok(());
415 }
416
417 info!("Closing browser context");
418
419 if let Some(recorder) = self.har_recorder.write().await.take() {
421 if let Err(e) = recorder.save().await {
422 debug!("Failed to auto-save HAR on close: {}", e);
423 } else {
424 debug!(path = %recorder.path().display(), "Auto-saved HAR on close");
425 }
426 }
427
428 self.event_manager.emit_close().await;
430
431 self.connection
432 .send_command::<_, serde_json::Value>(
433 "Target.disposeBrowserContext",
434 Some(DisposeBrowserContextParams {
435 browser_context_id: self.context_id.clone(),
436 }),
437 None,
438 )
439 .await?;
440
441 self.event_manager.clear().await;
443
444 self.closed = true;
445 info!("Browser context closed");
446 Ok(())
447 }
448
449 pub fn id(&self) -> &str {
451 &self.context_id
452 }
453
454 pub fn is_closed(&self) -> bool {
456 self.closed
457 }
458
459 pub fn connection(&self) -> &Arc<CdpConnection> {
461 &self.connection
462 }
463
464 pub fn context_id(&self) -> &str {
466 &self.context_id
467 }
468
469 }
485
486