use crate::api::launch_options::IgnoreDefaultArgs;
use crate::error::Result;
use crate::protocol::api_request_context::APIRequestContext;
use crate::protocol::cdp_session::CDPSession;
use crate::protocol::event_waiter::EventWaiter;
use crate::protocol::route::UnrouteBehavior;
use crate::protocol::tracing::Tracing;
use crate::protocol::{Browser, Page, ProxySettings, Request, ResponseObject, Route};
use crate::server::channel::Channel;
use crate::server::channel_owner::{ChannelOwner, ChannelOwnerImpl, ParentOrConnection};
use crate::server::connection::ConnectionExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::any::Any;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use tokio::sync::oneshot;
type RouteHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type PageHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type CloseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type RequestHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type ResponseHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type DialogHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type BindingCallbackFuture = Pin<Box<dyn Future<Output = serde_json::Value> + Send>>;
type PageHandler = Arc<dyn Fn(Page) -> PageHandlerFuture + Send + Sync>;
type CloseHandler = Arc<dyn Fn() -> CloseHandlerFuture + Send + Sync>;
type RequestHandler = Arc<dyn Fn(Request) -> RequestHandlerFuture + Send + Sync>;
type ResponseHandler = Arc<dyn Fn(ResponseObject) -> ResponseHandlerFuture + Send + Sync>;
type DialogHandler = Arc<dyn Fn(crate::protocol::Dialog) -> DialogHandlerFuture + Send + Sync>;
type ConsoleHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type ConsoleHandler =
Arc<dyn Fn(crate::protocol::ConsoleMessage) -> ConsoleHandlerFuture + Send + Sync>;
type WebErrorHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type WebErrorHandler =
Arc<dyn Fn(crate::protocol::WebError) -> WebErrorHandlerFuture + Send + Sync>;
type ServiceWorkerHandlerFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
type ServiceWorkerHandler =
Arc<dyn Fn(crate::protocol::Worker) -> ServiceWorkerHandlerFuture + Send + Sync>;
type BindingCallback = Arc<dyn Fn(Vec<serde_json::Value>) -> BindingCallbackFuture + Send + Sync>;
#[derive(Clone)]
struct RouteHandlerEntry {
pattern: String,
handler: Arc<dyn Fn(Route) -> RouteHandlerFuture + Send + Sync>,
}
#[derive(Clone)]
pub struct BrowserContext {
base: ChannelOwnerImpl,
browser: Option<Browser>,
pages: Arc<Mutex<Vec<Page>>>,
route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>,
request_context_guid: Option<String>,
tracing_guid: Option<String>,
default_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
default_navigation_timeout_ms: Arc<std::sync::atomic::AtomicU64>,
page_handlers: Arc<Mutex<Vec<PageHandler>>>,
close_handlers: Arc<Mutex<Vec<CloseHandler>>>,
request_handlers: Arc<Mutex<Vec<RequestHandler>>>,
request_finished_handlers: Arc<Mutex<Vec<RequestHandler>>>,
request_failed_handlers: Arc<Mutex<Vec<RequestHandler>>>,
response_handlers: Arc<Mutex<Vec<ResponseHandler>>>,
page_waiters: Arc<Mutex<Vec<oneshot::Sender<Page>>>>,
close_waiters: Arc<Mutex<Vec<oneshot::Sender<()>>>>,
dialog_handlers: Arc<Mutex<Vec<DialogHandler>>>,
binding_callbacks: Arc<Mutex<HashMap<String, BindingCallback>>>,
console_handlers: Arc<Mutex<Vec<ConsoleHandler>>>,
console_waiters: Arc<Mutex<Vec<oneshot::Sender<crate::protocol::ConsoleMessage>>>>,
weberror_handlers: Arc<Mutex<Vec<WebErrorHandler>>>,
serviceworker_handlers: Arc<Mutex<Vec<ServiceWorkerHandler>>>,
}
impl BrowserContext {
pub fn new(
parent: Arc<dyn ChannelOwner>,
type_name: String,
guid: Arc<str>,
initializer: Value,
) -> Result<Self> {
let request_context_guid = initializer
.get("requestContext")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tracing_guid = initializer
.get("tracing")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let base = ChannelOwnerImpl::new(
ParentOrConnection::Parent(parent.clone()),
type_name,
guid,
initializer,
);
let browser = parent.as_any().downcast_ref::<Browser>().cloned();
let context = Self {
base,
browser,
pages: Arc::new(Mutex::new(Vec::new())),
route_handlers: Arc::new(Mutex::new(Vec::new())),
request_context_guid,
tracing_guid,
default_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
crate::DEFAULT_TIMEOUT_MS.to_bits(),
)),
default_navigation_timeout_ms: Arc::new(std::sync::atomic::AtomicU64::new(
crate::DEFAULT_TIMEOUT_MS.to_bits(),
)),
page_handlers: Arc::new(Mutex::new(Vec::new())),
close_handlers: Arc::new(Mutex::new(Vec::new())),
request_handlers: Arc::new(Mutex::new(Vec::new())),
request_finished_handlers: Arc::new(Mutex::new(Vec::new())),
request_failed_handlers: Arc::new(Mutex::new(Vec::new())),
response_handlers: Arc::new(Mutex::new(Vec::new())),
page_waiters: Arc::new(Mutex::new(Vec::new())),
close_waiters: Arc::new(Mutex::new(Vec::new())),
dialog_handlers: Arc::new(Mutex::new(Vec::new())),
binding_callbacks: Arc::new(Mutex::new(HashMap::new())),
console_handlers: Arc::new(Mutex::new(Vec::new())),
console_waiters: Arc::new(Mutex::new(Vec::new())),
weberror_handlers: Arc::new(Mutex::new(Vec::new())),
serviceworker_handlers: Arc::new(Mutex::new(Vec::new())),
};
let channel = context.channel().clone();
tokio::spawn(async move {
_ = channel.update_subscription("dialog", true).await;
});
Ok(context)
}
fn channel(&self) -> &Channel {
self.base.channel()
}
pub async fn add_init_script(&self, script: &str) -> Result<()> {
self.channel()
.send_no_result("addInitScript", serde_json::json!({ "source": script }))
.await
}
pub async fn new_page(&self) -> Result<Page> {
#[derive(Deserialize)]
struct NewPageResponse {
page: GuidRef,
}
#[derive(Deserialize)]
struct GuidRef {
#[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
guid: Arc<str>,
}
let response: NewPageResponse = self
.channel()
.send("newPage", serde_json::json!({}))
.await?;
let page: Page = self
.connection()
.get_typed::<Page>(&response.page.guid)
.await?;
let ctx_timeout = self.default_timeout_ms();
let ctx_nav_timeout = self.default_navigation_timeout_ms();
if ctx_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
page.set_default_timeout(ctx_timeout).await;
}
if ctx_nav_timeout.to_bits() != crate::DEFAULT_TIMEOUT_MS.to_bits() {
page.set_default_navigation_timeout(ctx_nav_timeout).await;
}
Ok(page)
}
pub fn pages(&self) -> Vec<Page> {
self.pages.lock().unwrap().clone()
}
pub fn browser(&self) -> Option<Browser> {
self.browser.clone()
}
pub async fn request(&self) -> Result<APIRequestContext> {
let guid = self.request_context_guid.as_ref().ok_or_else(|| {
crate::error::Error::ProtocolError(
"No APIRequestContext available for this context".to_string(),
)
})?;
self.connection().get_typed::<APIRequestContext>(guid).await
}
pub async fn new_cdp_session(&self, page: &Page) -> Result<CDPSession> {
#[derive(serde::Deserialize)]
struct NewCDPSessionResponse {
session: GuidRef,
}
#[derive(serde::Deserialize)]
struct GuidRef {
#[serde(deserialize_with = "crate::server::connection::deserialize_arc_str")]
guid: Arc<str>,
}
let response: NewCDPSessionResponse = self
.channel()
.send(
"newCDPSession",
serde_json::json!({ "page": { "guid": page.guid() } }),
)
.await?;
self.connection()
.get_typed::<CDPSession>(&response.session.guid)
.await
}
pub async fn tracing(&self) -> Result<Tracing> {
let guid = self.tracing_guid.as_ref().ok_or_else(|| {
crate::error::Error::ProtocolError(
"No Tracing object available for this context".to_string(),
)
})?;
self.connection().get_typed::<Tracing>(guid).await
}
pub async fn close(&self) -> Result<()> {
let selectors = self.connection().selectors();
selectors.remove_context(self.channel());
self.channel()
.send_no_result("close", serde_json::json!({}))
.await
}
pub async fn set_default_timeout(&self, timeout: f64) {
self.default_timeout_ms
.store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
let pages: Vec<Page> = self.pages.lock().unwrap().clone();
for page in pages {
page.set_default_timeout(timeout).await;
}
crate::protocol::page::set_timeout_and_notify(
self.channel(),
"setDefaultTimeoutNoReply",
timeout,
)
.await;
}
pub async fn set_default_navigation_timeout(&self, timeout: f64) {
self.default_navigation_timeout_ms
.store(timeout.to_bits(), std::sync::atomic::Ordering::Relaxed);
let pages: Vec<Page> = self.pages.lock().unwrap().clone();
for page in pages {
page.set_default_navigation_timeout(timeout).await;
}
crate::protocol::page::set_timeout_and_notify(
self.channel(),
"setDefaultNavigationTimeoutNoReply",
timeout,
)
.await;
}
fn default_timeout_ms(&self) -> f64 {
f64::from_bits(
self.default_timeout_ms
.load(std::sync::atomic::Ordering::Relaxed),
)
}
fn default_navigation_timeout_ms(&self) -> f64 {
f64::from_bits(
self.default_navigation_timeout_ms
.load(std::sync::atomic::Ordering::Relaxed),
)
}
pub async fn pause(&self) -> Result<()> {
self.channel()
.send_no_result("pause", serde_json::Value::Null)
.await
}
pub async fn storage_state(&self) -> Result<StorageState> {
let response: StorageState = self
.channel()
.send("storageState", serde_json::json!({}))
.await?;
Ok(response)
}
pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
self.channel()
.send_no_result(
"addCookies",
serde_json::json!({
"cookies": cookies
}),
)
.await
}
pub async fn cookies(&self, urls: Option<&[&str]>) -> Result<Vec<Cookie>> {
let url_list: Vec<&str> = urls.unwrap_or(&[]).to_vec();
#[derive(serde::Deserialize)]
struct CookiesResponse {
cookies: Vec<Cookie>,
}
let response: CookiesResponse = self
.channel()
.send("cookies", serde_json::json!({ "urls": url_list }))
.await?;
Ok(response.cookies)
}
pub async fn clear_cookies(&self, options: Option<ClearCookiesOptions>) -> Result<()> {
let params = match options {
None => serde_json::json!({}),
Some(opts) => serde_json::to_value(opts).unwrap_or(serde_json::json!({})),
};
self.channel().send_no_result("clearCookies", params).await
}
pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
let headers_array: Vec<serde_json::Value> = headers
.into_iter()
.map(|(name, value)| serde_json::json!({ "name": name, "value": value }))
.collect();
self.channel()
.send_no_result(
"setExtraHTTPHeaders",
serde_json::json!({ "headers": headers_array }),
)
.await
}
pub async fn grant_permissions(
&self,
permissions: &[&str],
options: Option<GrantPermissionsOptions>,
) -> Result<()> {
let mut params = serde_json::json!({ "permissions": permissions });
if let Some(opts) = options
&& let Some(origin) = opts.origin
{
params["origin"] = serde_json::Value::String(origin);
}
self.channel()
.send_no_result("grantPermissions", params)
.await
}
pub async fn clear_permissions(&self) -> Result<()> {
self.channel()
.send_no_result("clearPermissions", serde_json::json!({}))
.await
}
pub async fn set_geolocation(&self, geolocation: Option<Geolocation>) -> Result<()> {
let params = match geolocation {
Some(geo) => serde_json::json!({ "geolocation": geo }),
None => serde_json::json!({}),
};
self.channel()
.send_no_result("setGeolocation", params)
.await
}
pub async fn set_offline(&self, offline: bool) -> Result<()> {
self.channel()
.send_no_result("setOffline", serde_json::json!({ "offline": offline }))
.await
}
pub async fn route<F, Fut>(&self, pattern: &str, handler: F) -> Result<()>
where
F: Fn(Route) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler =
Arc::new(move |route: Route| -> RouteHandlerFuture { Box::pin(handler(route)) });
self.route_handlers.lock().unwrap().push(RouteHandlerEntry {
pattern: pattern.to_string(),
handler,
});
self.enable_network_interception().await
}
pub async fn unroute(&self, pattern: &str) -> Result<()> {
self.route_handlers
.lock()
.unwrap()
.retain(|entry| entry.pattern != pattern);
self.enable_network_interception().await
}
pub async fn unroute_all(&self, _behavior: Option<UnrouteBehavior>) -> Result<()> {
self.route_handlers.lock().unwrap().clear();
self.enable_network_interception().await
}
pub async fn on_page<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Page) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |page: Page| -> PageHandlerFuture { Box::pin(handler(page)) });
self.page_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_close<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move || -> CloseHandlerFuture { Box::pin(handler()) });
self.close_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("request", true).await;
}
self.request_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request_finished<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_finished_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self
.channel()
.update_subscription("requestFinished", true)
.await;
}
self.request_finished_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_request_failed<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(Request) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |request: Request| -> RequestHandlerFuture {
Box::pin(handler(request))
});
let needs_subscription = self.request_failed_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self
.channel()
.update_subscription("requestFailed", true)
.await;
}
self.request_failed_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_response<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(ResponseObject) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(move |response: ResponseObject| -> ResponseHandlerFuture {
Box::pin(handler(response))
});
let needs_subscription = self.response_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("response", true).await;
}
self.response_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_dialog<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(crate::protocol::Dialog) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(
move |dialog: crate::protocol::Dialog| -> DialogHandlerFuture {
Box::pin(handler(dialog))
},
);
self.dialog_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_console<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(crate::protocol::ConsoleMessage) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(
move |msg: crate::protocol::ConsoleMessage| -> ConsoleHandlerFuture {
Box::pin(handler(msg))
},
);
let needs_subscription = self.console_handlers.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("console", true).await;
}
self.console_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_weberror<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(crate::protocol::WebError) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(
move |web_error: crate::protocol::WebError| -> WebErrorHandlerFuture {
Box::pin(handler(web_error))
},
);
self.weberror_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn on_serviceworker<F, Fut>(&self, handler: F) -> Result<()>
where
F: Fn(crate::protocol::Worker) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<()>> + Send + 'static,
{
let handler = Arc::new(
move |worker: crate::protocol::Worker| -> ServiceWorkerHandlerFuture {
Box::pin(handler(worker))
},
);
self.serviceworker_handlers.lock().unwrap().push(handler);
Ok(())
}
pub async fn expose_function<F, Fut>(&self, name: &str, callback: F) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
self.expose_binding_internal(name, false, callback).await
}
pub async fn expose_binding<F, Fut>(&self, name: &str, callback: F) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
self.expose_binding_internal(name, true, callback).await
}
async fn expose_binding_internal<F, Fut>(
&self,
name: &str,
_needs_handle: bool,
callback: F,
) -> Result<()>
where
F: Fn(Vec<serde_json::Value>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = serde_json::Value> + Send + 'static,
{
let callback: BindingCallback = Arc::new(move |args: Vec<serde_json::Value>| {
Box::pin(callback(args)) as BindingCallbackFuture
});
self.binding_callbacks
.lock()
.unwrap()
.insert(name.to_string(), callback);
self.channel()
.send_no_result(
"exposeBinding",
serde_json::json!({ "name": name, "needsHandle": false }),
)
.await
}
pub async fn expect_page(&self, timeout: Option<f64>) -> Result<EventWaiter<Page>> {
let (tx, rx) = oneshot::channel();
self.page_waiters.lock().unwrap().push(tx);
Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
}
pub async fn expect_close(&self, timeout: Option<f64>) -> Result<EventWaiter<()>> {
let (tx, rx) = oneshot::channel();
self.close_waiters.lock().unwrap().push(tx);
Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
}
pub async fn expect_console_message(
&self,
timeout: Option<f64>,
) -> Result<EventWaiter<crate::protocol::ConsoleMessage>> {
let needs_subscription = self.console_handlers.lock().unwrap().is_empty()
&& self.console_waiters.lock().unwrap().is_empty();
if needs_subscription {
_ = self.channel().update_subscription("console", true).await;
}
let (tx, rx) = oneshot::channel();
self.console_waiters.lock().unwrap().push(tx);
Ok(EventWaiter::new(rx, timeout.or(Some(30_000.0))))
}
async fn enable_network_interception(&self) -> Result<()> {
let patterns: Vec<serde_json::Value> = self
.route_handlers
.lock()
.unwrap()
.iter()
.map(|entry| serde_json::json!({ "glob": entry.pattern }))
.collect();
self.channel()
.send_no_result(
"setNetworkInterceptionPatterns",
serde_json::json!({ "patterns": patterns }),
)
.await
}
pub fn deserialize_binding_args_pub(raw_args: &Value) -> Vec<Value> {
Self::deserialize_binding_args(raw_args)
}
fn deserialize_binding_args(raw_args: &Value) -> Vec<Value> {
let Some(arr) = raw_args.as_array() else {
return vec![];
};
arr.iter()
.map(|arg| {
crate::protocol::evaluate_conversion::parse_value(arg, None)
})
.collect()
}
async fn on_route_event(route_handlers: Arc<Mutex<Vec<RouteHandlerEntry>>>, route: Route) {
let handlers = route_handlers.lock().unwrap().clone();
let url = route.request().url().to_string();
for entry in handlers.iter().rev() {
if crate::protocol::route::matches_pattern(&entry.pattern, &url) {
let handler = entry.handler.clone();
if let Err(e) = handler(route.clone()).await {
tracing::warn!("Context route handler error: {}", e);
break;
}
if !route.was_handled() {
continue;
}
break;
}
}
}
fn dispatch_request_event(&self, method: &str, params: Value) {
if let Some(request_guid) = params
.get("request")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let request_guid_owned = request_guid.to_owned();
let page_guid_owned = params
.get("page")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|v| v.to_owned());
let failure_text = params
.get("failureText")
.and_then(|v| v.as_str())
.map(|s| s.to_owned());
let response_guid_owned = params
.get("response")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|s| s.to_owned());
let response_end_timing = params.get("responseEndTiming").and_then(|v| v.as_f64());
let method = method.to_owned();
let ctx_request_handlers = self.request_handlers.clone();
let ctx_request_finished_handlers = self.request_finished_handlers.clone();
let ctx_request_failed_handlers = self.request_failed_handlers.clone();
tokio::spawn(async move {
let request: Request =
match connection.get_typed::<Request>(&request_guid_owned).await {
Ok(r) => r,
Err(_) => return,
};
if let Some(text) = failure_text {
request.set_failure_text(text);
}
if method == "requestFinished"
&& let Some(timing) =
extract_timing(&connection, response_guid_owned, response_end_timing).await
{
request.set_timing(timing);
}
let ctx_handlers = match method.as_str() {
"request" => ctx_request_handlers.lock().unwrap().clone(),
"requestFinished" => ctx_request_finished_handlers.lock().unwrap().clone(),
"requestFailed" => ctx_request_failed_handlers.lock().unwrap().clone(),
_ => vec![],
};
for handler in ctx_handlers {
if let Err(e) = handler(request.clone()).await {
tracing::warn!("Context {} handler error: {}", method, e);
}
}
if let Some(page_guid) = page_guid_owned {
let page: Page = match connection.get_typed::<Page>(&page_guid).await {
Ok(p) => p,
Err(_) => return,
};
match method.as_str() {
"request" => page.trigger_request_event(request).await,
"requestFailed" => page.trigger_request_failed_event(request).await,
"requestFinished" => page.trigger_request_finished_event(request).await,
_ => unreachable!("Unreachable method {}", method),
}
}
});
}
}
fn dispatch_response_event(&self, _method: &str, params: Value) {
if let Some(response_guid) = params
.get("response")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let response_guid_owned = response_guid.to_owned();
let page_guid_owned = params
.get("page")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|v| v.to_owned());
let ctx_response_handlers = self.response_handlers.clone();
tokio::spawn(async move {
let response: ResponseObject = match connection
.get_typed::<ResponseObject>(&response_guid_owned)
.await
{
Ok(r) => r,
Err(_) => return,
};
let ctx_handlers = ctx_response_handlers.lock().unwrap().clone();
for handler in ctx_handlers {
if let Err(e) = handler(response.clone()).await {
tracing::warn!("Context response handler error: {}", e);
}
}
if let Some(page_guid) = page_guid_owned {
let page: Page = match connection.get_typed::<Page>(&page_guid).await {
Ok(p) => p,
Err(_) => return,
};
page.trigger_response_event(response).await;
}
});
}
}
}
impl ChannelOwner for BrowserContext {
fn guid(&self) -> &str {
self.base.guid()
}
fn type_name(&self) -> &str {
self.base.type_name()
}
fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
self.base.parent()
}
fn connection(&self) -> Arc<dyn crate::server::connection::ConnectionLike> {
self.base.connection()
}
fn initializer(&self) -> &Value {
self.base.initializer()
}
fn channel(&self) -> &Channel {
self.base.channel()
}
fn dispose(&self, reason: crate::server::channel_owner::DisposeReason) {
self.base.dispose(reason)
}
fn adopt(&self, child: Arc<dyn ChannelOwner>) {
self.base.adopt(child)
}
fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
self.base.add_child(guid, child)
}
fn remove_child(&self, guid: &str) {
self.base.remove_child(guid)
}
fn on_event(&self, method: &str, params: Value) {
match method {
"request" | "requestFailed" | "requestFinished" => {
self.dispatch_request_event(method, params)
}
"response" => self.dispatch_response_event(method, params),
"close" => {
let close_handlers = self.close_handlers.clone();
let close_waiters = self.close_waiters.clone();
tokio::spawn(async move {
let handlers = close_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler().await {
tracing::warn!("Context close handler error: {}", e);
}
}
let waiters: Vec<_> = close_waiters.lock().unwrap().drain(..).collect();
for tx in waiters {
let _ = tx.send(());
}
});
}
"page" => {
if let Some(page_guid) = params
.get("page")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let page_guid_owned = page_guid.to_string();
let pages = self.pages.clone();
let page_handlers = self.page_handlers.clone();
let page_waiters = self.page_waiters.clone();
tokio::spawn(async move {
let page: Page = match connection.get_typed::<Page>(&page_guid_owned).await
{
Ok(p) => p,
Err(_) => return,
};
pages.lock().unwrap().push(page.clone());
if let Some(opener_guid) = page
.initializer()
.get("opener")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
&& let Ok(opener) = connection.get_typed::<Page>(opener_guid).await
{
opener.trigger_popup_event(page.clone()).await;
}
let handlers = page_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(page.clone()).await {
tracing::warn!("Context page handler error: {}", e);
}
}
if let Some(tx) = page_waiters.lock().unwrap().pop() {
let _ = tx.send(page);
}
});
}
}
"pageError" => {
let message = params
.get("error")
.and_then(|e| e.get("error"))
.and_then(|e| e.get("message"))
.and_then(|m| m.as_str())
.unwrap_or("")
.to_string();
let page_guid_owned = params
.get("page")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let connection = self.connection();
let weberror_handlers = self.weberror_handlers.clone();
tokio::spawn(async move {
let page = if let Some(ref guid) = page_guid_owned {
connection.get_typed::<Page>(guid).await.ok()
} else {
None
};
let web_error = crate::protocol::WebError::new(message.clone(), page.clone());
let handlers = weberror_handlers.lock().unwrap().clone();
for handler in handlers {
if let Err(e) = handler(web_error.clone()).await {
tracing::warn!("Context weberror handler error: {}", e);
}
}
if let Some(p) = page {
p.trigger_pageerror_event(message).await;
}
});
}
"dialog" => {
if let Some(dialog_guid) = params
.get("dialog")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let dialog_guid_owned = dialog_guid.to_string();
let dialog_handlers = self.dialog_handlers.clone();
tokio::spawn(async move {
let dialog: crate::protocol::Dialog = match connection
.get_typed::<crate::protocol::Dialog>(&dialog_guid_owned)
.await
{
Ok(d) => d,
Err(_) => return,
};
let ctx_handlers = dialog_handlers.lock().unwrap().clone();
for handler in ctx_handlers {
if let Err(e) = handler(dialog.clone()).await {
tracing::warn!("Context dialog handler error: {}", e);
}
}
let page: Page =
match crate::server::connection::downcast_parent::<Page>(&dialog) {
Some(p) => p,
None => return,
};
page.trigger_dialog_event(dialog).await;
});
}
}
"bindingCall" => {
if let Some(binding_guid) = params
.get("binding")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let binding_guid_owned = binding_guid.to_string();
let binding_callbacks = self.binding_callbacks.clone();
tokio::spawn(async move {
let binding_call: crate::protocol::BindingCall = match connection
.get_typed::<crate::protocol::BindingCall>(&binding_guid_owned)
.await
{
Ok(bc) => bc,
Err(e) => {
tracing::warn!("Failed to get BindingCall object: {}", e);
return;
}
};
let name = binding_call.name().to_string();
let callback = {
let callbacks = binding_callbacks.lock().unwrap();
callbacks.get(&name).cloned()
};
let Some(callback) = callback else {
tracing::warn!("No callback registered for binding '{}'", name);
let _ = binding_call
.reject(&format!("No Rust handler for binding '{name}'"))
.await;
return;
};
let raw_args = binding_call.args();
let args = Self::deserialize_binding_args(raw_args);
let result_value = callback(args).await;
let serialized =
crate::protocol::evaluate_conversion::serialize_argument(&result_value);
if let Err(e) = binding_call.resolve(serialized).await {
tracing::warn!("Failed to resolve BindingCall '{}': {}", name, e);
}
});
}
}
"route" => {
if let Some(route_guid) = params
.get("route")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let route_guid_owned = route_guid.to_string();
let route_handlers = self.route_handlers.clone();
let request_context_guid = self.request_context_guid.clone();
tokio::spawn(async move {
let route: Route =
match connection.get_typed::<Route>(&route_guid_owned).await {
Ok(r) => r,
Err(e) => {
tracing::warn!("Failed to get route object: {}", e);
return;
}
};
if let Some(ref guid) = request_context_guid
&& let Ok(api_ctx) =
connection.get_typed::<APIRequestContext>(guid).await
{
route.set_api_request_context(api_ctx);
}
BrowserContext::on_route_event(route_handlers, route).await;
});
}
}
"console" => {
let type_ = params
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("log")
.to_string();
let text = params
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let loc_url = params
.get("location")
.and_then(|v| v.get("url"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let loc_line = params
.get("location")
.and_then(|v| v.get("lineNumber"))
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32;
let loc_col = params
.get("location")
.and_then(|v| v.get("columnNumber"))
.and_then(|v| v.as_i64())
.unwrap_or(0) as i32;
let page_guid_owned = params
.get("page")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let arg_guids: Vec<String> = params
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| {
v.get("guid")
.and_then(|g| g.as_str())
.map(|s| s.to_string())
})
.collect()
})
.unwrap_or_default();
let connection = self.connection();
let ctx_console_handlers = self.console_handlers.clone();
let ctx_console_waiters = self.console_waiters.clone();
tokio::spawn(async move {
use crate::protocol::JSHandle;
use crate::protocol::console_message::{
ConsoleMessage, ConsoleMessageLocation,
};
let page = if let Some(ref guid) = page_guid_owned {
connection.get_typed::<Page>(guid).await.ok()
} else {
None
};
let args: Vec<std::sync::Arc<JSHandle>> = {
let mut resolved = Vec::with_capacity(arg_guids.len());
for guid in &arg_guids {
if let Ok(handle) = connection.get_typed::<JSHandle>(guid).await {
resolved.push(std::sync::Arc::new(handle));
}
}
resolved
};
let location = ConsoleMessageLocation {
url: loc_url,
line_number: loc_line,
column_number: loc_col,
};
let msg = ConsoleMessage::new(type_, text, location, page.clone(), args);
if let Some(tx) = ctx_console_waiters.lock().unwrap().pop() {
let _ = tx.send(msg.clone());
}
let ctx_handlers = ctx_console_handlers.lock().unwrap().clone();
for handler in ctx_handlers {
if let Err(e) = handler(msg.clone()).await {
tracing::warn!("Context console handler error: {}", e);
}
}
if let Some(p) = page {
p.trigger_console_event(msg).await;
}
});
}
"serviceWorker" => {
if let Some(worker_guid) = params
.get("worker")
.and_then(|v| v.get("guid"))
.and_then(|v| v.as_str())
{
let connection = self.connection();
let worker_guid_owned = worker_guid.to_string();
let serviceworker_handlers = self.serviceworker_handlers.clone();
tokio::spawn(async move {
let worker: crate::protocol::Worker = match connection
.get_typed::<crate::protocol::Worker>(&worker_guid_owned)
.await
{
Ok(w) => w,
Err(e) => {
tracing::warn!(
"Failed to get Worker object for serviceWorker event: {}",
e
);
return;
}
};
let handlers = serviceworker_handlers.lock().unwrap().clone();
for handler in handlers {
let worker_clone = worker.clone();
tokio::spawn(async move {
if let Err(e) = handler(worker_clone).await {
tracing::error!("Error in serviceworker handler: {}", e);
}
});
}
});
}
}
_ => {
}
}
}
fn was_collected(&self) -> bool {
self.base.was_collected()
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl std::fmt::Debug for BrowserContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BrowserContext")
.field("guid", &self.guid())
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Viewport {
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Geolocation {
pub latitude: f64,
pub longitude: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub accuracy: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub expires: f64,
pub http_only: bool,
pub secure: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub same_site: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocalStorageItem {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Origin {
pub origin: String,
pub local_storage: Vec<LocalStorageItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageState {
pub cookies: Vec<Cookie>,
pub origins: Vec<Origin>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClearCookiesOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct GrantPermissionsOptions {
pub origin: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct RecordHar {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub omit_content: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_filter: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RecordVideo {
pub dir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<Viewport>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowserContextOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub viewport: Option<Viewport>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "noDefaultViewport")]
pub no_viewport: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timezone_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geolocation: Option<Geolocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub proxy: Option<ProxySettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color_scheme: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_touch: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_mobile: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub javascript_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offline: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub accept_downloads: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bypass_csp: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_https_errors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_scale_factor: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_http_headers: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_state: Option<StorageState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_state_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chromium_sandbox: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub devtools: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub downloads_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub executable_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headless: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_default_args: Option<IgnoreDefaultArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub slow_mo: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub traces_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub strict_selectors: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reduced_motion: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub forced_colors: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_workers: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub record_har: Option<RecordHar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub record_video: Option<RecordVideo>,
}
impl BrowserContextOptions {
pub fn builder() -> BrowserContextOptionsBuilder {
BrowserContextOptionsBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct BrowserContextOptionsBuilder {
viewport: Option<Viewport>,
no_viewport: Option<bool>,
user_agent: Option<String>,
locale: Option<String>,
timezone_id: Option<String>,
geolocation: Option<Geolocation>,
permissions: Option<Vec<String>>,
proxy: Option<ProxySettings>,
color_scheme: Option<String>,
has_touch: Option<bool>,
is_mobile: Option<bool>,
javascript_enabled: Option<bool>,
offline: Option<bool>,
accept_downloads: Option<bool>,
bypass_csp: Option<bool>,
ignore_https_errors: Option<bool>,
device_scale_factor: Option<f64>,
extra_http_headers: Option<HashMap<String, String>>,
base_url: Option<String>,
storage_state: Option<StorageState>,
storage_state_path: Option<String>,
args: Option<Vec<String>>,
channel: Option<String>,
chromium_sandbox: Option<bool>,
devtools: Option<bool>,
downloads_path: Option<String>,
executable_path: Option<String>,
firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
headless: Option<bool>,
ignore_default_args: Option<IgnoreDefaultArgs>,
slow_mo: Option<f64>,
timeout: Option<f64>,
traces_dir: Option<String>,
strict_selectors: Option<bool>,
reduced_motion: Option<String>,
forced_colors: Option<String>,
service_workers: Option<String>,
record_har: Option<RecordHar>,
record_video: Option<RecordVideo>,
}
impl BrowserContextOptionsBuilder {
pub fn viewport(mut self, viewport: Viewport) -> Self {
self.viewport = Some(viewport);
self.no_viewport = None; self
}
pub fn no_viewport(mut self, no_viewport: bool) -> Self {
self.no_viewport = Some(no_viewport);
if no_viewport {
self.viewport = None; }
self
}
pub fn user_agent(mut self, user_agent: String) -> Self {
self.user_agent = Some(user_agent);
self
}
pub fn locale(mut self, locale: String) -> Self {
self.locale = Some(locale);
self
}
pub fn timezone_id(mut self, timezone_id: String) -> Self {
self.timezone_id = Some(timezone_id);
self
}
pub fn geolocation(mut self, geolocation: Geolocation) -> Self {
self.geolocation = Some(geolocation);
self
}
pub fn permissions(mut self, permissions: Vec<String>) -> Self {
self.permissions = Some(permissions);
self
}
pub fn proxy(mut self, proxy: ProxySettings) -> Self {
self.proxy = Some(proxy);
self
}
pub fn color_scheme(mut self, color_scheme: String) -> Self {
self.color_scheme = Some(color_scheme);
self
}
pub fn has_touch(mut self, has_touch: bool) -> Self {
self.has_touch = Some(has_touch);
self
}
pub fn is_mobile(mut self, is_mobile: bool) -> Self {
self.is_mobile = Some(is_mobile);
self
}
pub fn javascript_enabled(mut self, javascript_enabled: bool) -> Self {
self.javascript_enabled = Some(javascript_enabled);
self
}
pub fn offline(mut self, offline: bool) -> Self {
self.offline = Some(offline);
self
}
pub fn accept_downloads(mut self, accept_downloads: bool) -> Self {
self.accept_downloads = Some(accept_downloads);
self
}
pub fn bypass_csp(mut self, bypass_csp: bool) -> Self {
self.bypass_csp = Some(bypass_csp);
self
}
pub fn ignore_https_errors(mut self, ignore_https_errors: bool) -> Self {
self.ignore_https_errors = Some(ignore_https_errors);
self
}
pub fn device_scale_factor(mut self, device_scale_factor: f64) -> Self {
self.device_scale_factor = Some(device_scale_factor);
self
}
pub fn extra_http_headers(mut self, extra_http_headers: HashMap<String, String>) -> Self {
self.extra_http_headers = Some(extra_http_headers);
self
}
pub fn base_url(mut self, base_url: String) -> Self {
self.base_url = Some(base_url);
self
}
pub fn storage_state(mut self, storage_state: StorageState) -> Self {
self.storage_state = Some(storage_state);
self.storage_state_path = None; self
}
pub fn storage_state_path(mut self, path: String) -> Self {
self.storage_state_path = Some(path);
self.storage_state = None; self
}
pub fn args(mut self, args: Vec<String>) -> Self {
self.args = Some(args);
self
}
pub fn channel(mut self, channel: String) -> Self {
self.channel = Some(channel);
self
}
pub fn chromium_sandbox(mut self, enabled: bool) -> Self {
self.chromium_sandbox = Some(enabled);
self
}
pub fn devtools(mut self, enabled: bool) -> Self {
self.devtools = Some(enabled);
self
}
pub fn downloads_path(mut self, path: String) -> Self {
self.downloads_path = Some(path);
self
}
pub fn executable_path(mut self, path: String) -> Self {
self.executable_path = Some(path);
self
}
pub fn firefox_user_prefs(mut self, prefs: HashMap<String, serde_json::Value>) -> Self {
self.firefox_user_prefs = Some(prefs);
self
}
pub fn headless(mut self, enabled: bool) -> Self {
self.headless = Some(enabled);
self
}
pub fn ignore_default_args(mut self, args: IgnoreDefaultArgs) -> Self {
self.ignore_default_args = Some(args);
self
}
pub fn slow_mo(mut self, ms: f64) -> Self {
self.slow_mo = Some(ms);
self
}
pub fn timeout(mut self, ms: f64) -> Self {
self.timeout = Some(ms);
self
}
pub fn traces_dir(mut self, path: String) -> Self {
self.traces_dir = Some(path);
self
}
pub fn strict_selectors(mut self, enabled: bool) -> Self {
self.strict_selectors = Some(enabled);
self
}
pub fn reduced_motion(mut self, value: String) -> Self {
self.reduced_motion = Some(value);
self
}
pub fn forced_colors(mut self, value: String) -> Self {
self.forced_colors = Some(value);
self
}
pub fn service_workers(mut self, value: String) -> Self {
self.service_workers = Some(value);
self
}
pub fn record_har(mut self, record_har: RecordHar) -> Self {
self.record_har = Some(record_har);
self
}
pub fn record_video(mut self, record_video: RecordVideo) -> Self {
self.record_video = Some(record_video);
self
}
pub fn build(self) -> BrowserContextOptions {
BrowserContextOptions {
viewport: self.viewport,
no_viewport: self.no_viewport,
user_agent: self.user_agent,
locale: self.locale,
timezone_id: self.timezone_id,
geolocation: self.geolocation,
permissions: self.permissions,
proxy: self.proxy,
color_scheme: self.color_scheme,
has_touch: self.has_touch,
is_mobile: self.is_mobile,
javascript_enabled: self.javascript_enabled,
offline: self.offline,
accept_downloads: self.accept_downloads,
bypass_csp: self.bypass_csp,
ignore_https_errors: self.ignore_https_errors,
device_scale_factor: self.device_scale_factor,
extra_http_headers: self.extra_http_headers,
base_url: self.base_url,
storage_state: self.storage_state,
storage_state_path: self.storage_state_path,
args: self.args,
channel: self.channel,
chromium_sandbox: self.chromium_sandbox,
devtools: self.devtools,
downloads_path: self.downloads_path,
executable_path: self.executable_path,
firefox_user_prefs: self.firefox_user_prefs,
headless: self.headless,
ignore_default_args: self.ignore_default_args,
slow_mo: self.slow_mo,
timeout: self.timeout,
traces_dir: self.traces_dir,
strict_selectors: self.strict_selectors,
reduced_motion: self.reduced_motion,
forced_colors: self.forced_colors,
service_workers: self.service_workers,
record_har: self.record_har,
record_video: self.record_video,
}
}
}
async fn extract_timing(
connection: &std::sync::Arc<dyn crate::server::connection::ConnectionLike>,
response_guid: Option<String>,
response_end_timing: Option<f64>,
) -> Option<serde_json::Value> {
let resp_guid = response_guid?;
let resp_obj: crate::protocol::ResponseObject = connection
.get_typed::<crate::protocol::ResponseObject>(&resp_guid)
.await
.ok()?;
let mut timing = resp_obj.initializer().get("timing")?.clone();
if let (Some(end), Some(obj)) = (response_end_timing, timing.as_object_mut())
&& let Some(n) = serde_json::Number::from_f64(end)
{
obj.insert("responseEnd".to_string(), serde_json::Value::Number(n));
}
Some(timing)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::launch_options::IgnoreDefaultArgs;
#[test]
fn test_browser_context_options_ignore_default_args_bool_serialization() {
let options = BrowserContextOptions::builder()
.ignore_default_args(IgnoreDefaultArgs::Bool(true))
.build();
let value = serde_json::to_value(&options).unwrap();
assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(true));
}
#[test]
fn test_browser_context_options_ignore_default_args_array_serialization() {
let options = BrowserContextOptions::builder()
.ignore_default_args(IgnoreDefaultArgs::Array(vec!["--foo".to_string()]))
.build();
let value = serde_json::to_value(&options).unwrap();
assert_eq!(value["ignoreDefaultArgs"], serde_json::json!(["--foo"]));
}
#[test]
fn test_browser_context_options_ignore_default_args_absent() {
let options = BrowserContextOptions::builder().build();
let value = serde_json::to_value(&options).unwrap();
assert!(value.get("ignoreDefaultArgs").is_none());
}
}