use super::BrowserContext;
use crate::error::{Error, Result};
use crate::server::channel_owner::ChannelOwner;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
impl BrowserContext {
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
pub async fn storage_state(
&self,
options: impl Into<Option<StorageStateOptions>>,
) -> Result<StorageState> {
let params = serde_json::to_value(options.into().unwrap_or_default())
.map_err(|e| Error::ProtocolError(format!("Failed to serialize options: {e}")))?;
let response: StorageState = self.channel().send("storageState", params).await?;
Ok(response)
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
pub async fn set_storage_state(&self, state: StorageState) -> Result<()> {
let storage_state = serde_json::to_value(&state)
.map_err(|e| Error::ProtocolError(format!("Failed to serialize storage state: {e}")))?;
self.channel()
.send_no_result(
"setStorageState",
serde_json::json!({ "storageState": storage_state }),
)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = cookies.len()))]
pub async fn add_cookies(&self, cookies: &[Cookie]) -> Result<()> {
self.channel()
.send_no_result(
"addCookies",
serde_json::json!({
"cookies": cookies
}),
)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = tracing::field::Empty))]
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?;
tracing::Span::current().record("count", response.cookies.len());
Ok(response.cookies)
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
pub async fn clear_cookies(
&self,
options: impl Into<Option<ClearCookiesOptions>>,
) -> Result<()> {
let options = options.into();
self.channel()
.send_no_result("clearCookies", options.unwrap_or_default())
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), count = headers.len()))]
pub async fn set_extra_http_headers(&self, headers: HashMap<String, String>) -> Result<()> {
let headers_array = crate::protocol::route_params::header_array(headers);
self.channel()
.send_no_result(
"setExtraHTTPHeaders",
serde_json::json!({ "headers": headers_array }),
)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
pub async fn grant_permissions(
&self,
permissions: &[&str],
options: impl Into<Option<GrantPermissionsOptions>>,
) -> Result<()> {
let options = options.into();
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
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
pub async fn clear_permissions(&self) -> Result<()> {
self.channel()
.send_no_result("clearPermissions", serde_json::json!({}))
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid()))]
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_http_credentials(&self, credentials: Vec<HttpCredentials>) -> Result<()> {
self.channel()
.send_no_result(
"setHTTPCredentials",
serde_json::json!({ "httpCredentials": credentials }),
)
.await
}
#[tracing::instrument(level = "debug", skip_all, fields(guid = %self.guid(), offline))]
pub async fn set_offline(&self, offline: bool) -> Result<()> {
self.channel()
.send_no_result("setOffline", serde_json::json!({ "offline": offline }))
.await
}
}
#[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, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum HttpCredentialsSend {
Always,
Unauthorized,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HttpCredentials {
pub username: String,
pub password: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub origin: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send: Option<HttpCredentialsSend>,
}
impl HttpCredentials {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: username.into(),
password: password.into(),
origin: None,
send: None,
}
}
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.origin = Some(origin.into());
self
}
pub fn send(mut self, send: HttpCredentialsSend) -> Self {
self.send = Some(send);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Cookie {
pub name: String,
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "String::is_empty")]
pub domain: String,
#[serde(skip_serializing_if = "String::is_empty")]
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>,
}
impl Cookie {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
url: None,
domain: String::new(),
path: "/".to_string(),
expires: -1.0,
http_only: false,
secure: false,
same_site: None,
}
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self.domain.clear();
self.path.clear();
self
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.domain = domain.into();
self.url = None;
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self.url = None;
self
}
pub fn expires(mut self, expires: f64) -> Self {
self.expires = expires;
self
}
pub fn http_only(mut self, http_only: bool) -> Self {
self.http_only = http_only;
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn same_site(mut self, same_site: impl Into<String>) -> Self {
self.same_site = Some(same_site.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LocalStorageItem {
pub name: String,
pub value: String,
}
impl LocalStorageItem {
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Origin {
pub origin: String,
pub local_storage: Vec<LocalStorageItem>,
#[serde(rename = "indexedDB", default, skip_serializing_if = "Option::is_none")]
pub indexed_db: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub opfs: Option<serde_json::Value>,
}
impl Origin {
pub fn new(origin: impl Into<String>, local_storage: Vec<LocalStorageItem>) -> Self {
Self {
origin: origin.into(),
local_storage,
indexed_db: None,
opfs: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StorageState {
pub cookies: Vec<Cookie>,
pub origins: Vec<Origin>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credentials: Option<Vec<crate::protocol::VirtualCredential>>,
}
impl StorageState {
pub fn cookies(mut self, cookies: Vec<Cookie>) -> Self {
self.cookies = cookies;
self
}
pub fn origins(mut self, origins: Vec<Origin>) -> Self {
self.origins = origins;
self
}
pub fn credentials(mut self, credentials: Vec<crate::protocol::VirtualCredential>) -> Self {
self.credentials = Some(credentials);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct StorageStateOptions {
#[serde(rename = "indexedDB", skip_serializing_if = "Option::is_none")]
pub indexed_db: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub opfs: Option<bool>,
}
impl StorageStateOptions {
pub fn indexed_db(mut self, include: bool) -> Self {
self.indexed_db = Some(include);
self
}
pub fn credentials(mut self, include: bool) -> Self {
self.credentials = Some(include);
self
}
pub fn opfs(mut self, include: bool) -> Self {
self.opfs = Some(include);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ClearCookiesOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_regex_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name_regex_flags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_regex_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain_regex_flags: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_regex_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_regex_flags: Option<String>,
}
impl ClearCookiesOptions {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self.name_regex_source = None;
self.name_regex_flags = None;
self
}
pub fn name_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
self.name = None;
self.name_regex_source = Some(source.into());
self.name_regex_flags = Some(flags.into());
self
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self.domain_regex_source = None;
self.domain_regex_flags = None;
self
}
pub fn domain_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
self.domain = None;
self.domain_regex_source = Some(source.into());
self.domain_regex_flags = Some(flags.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self.path_regex_source = None;
self.path_regex_flags = None;
self
}
pub fn path_regex(mut self, source: impl Into<String>, flags: impl Into<String>) -> Self {
self.path = None;
self.path_regex_source = Some(source.into());
self.path_regex_flags = Some(flags.into());
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct GrantPermissionsOptions {
pub origin: Option<String>,
}
impl GrantPermissionsOptions {
pub fn origin(mut self, origin: impl Into<String>) -> Self {
self.origin = Some(origin.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_cookie_for_a_url_sends_the_url_instead_of_a_domain_and_path() {
let sent =
serde_json::to_value(Cookie::new("a", "b").url("http://localhost:8080/app")).unwrap();
assert_eq!(sent["url"], "http://localhost:8080/app");
assert!(sent.get("domain").is_none(), "{sent}");
assert!(sent.get("path").is_none(), "{sent}");
let scoped = serde_json::to_value(
Cookie::new("a", "b")
.url("http://localhost:8080/app")
.domain("example.com")
.path("/"),
)
.unwrap();
assert_eq!(
(scoped["domain"].as_str(), scoped["path"].as_str()),
(Some("example.com"), Some("/"))
);
assert!(scoped.get("url").is_none(), "{scoped}");
}
#[test]
fn clear_cookies_filters_send_a_string_or_a_regex_under_the_driver_spelling() {
use crate::protocol_spec::{
API_REQUEST_CONTEXT, BROWSER_CONTEXT, command_parameters, keys,
};
let options = ClearCookiesOptions::default()
.name_regex("^sess", "")
.domain_regex(r"one\.com$", "i")
.path("/app");
let sent = serde_json::to_value(&options).unwrap();
assert_eq!(
sent,
serde_json::json!({
"nameRegexSource": "^sess",
"nameRegexFlags": "",
"domainRegexSource": "one\\.com$",
"domainRegexFlags": "i",
"path": "/app"
})
);
assert!(keys(&sent).is_subset(&command_parameters(BROWSER_CONTEXT, "clearCookies")));
assert!(keys(&sent).is_subset(&command_parameters(API_REQUEST_CONTEXT, "clearCookies")));
}
#[test]
fn a_clear_cookies_filter_is_a_string_or_a_regex_whichever_was_set_last() {
let regex_then_string = ClearCookiesOptions::default()
.name_regex("^sess", "")
.domain_regex("x", "")
.path_regex("y", "")
.name("sid")
.domain("example.com")
.path("/");
assert_eq!(
serde_json::to_value(®ex_then_string).unwrap(),
serde_json::json!({"name": "sid", "domain": "example.com", "path": "/"})
);
let string_then_regex = ClearCookiesOptions::default()
.name("sid")
.domain("example.com")
.path("/")
.name_regex("^sess", "")
.domain_regex("x", "")
.path_regex("y", "m");
assert_eq!(
serde_json::to_value(&string_then_regex).unwrap(),
serde_json::json!({
"nameRegexSource": "^sess", "nameRegexFlags": "",
"domainRegexSource": "x", "domainRegexFlags": "",
"pathRegexSource": "y", "pathRegexFlags": "m"
})
);
}
#[test]
fn storage_state_options_serialize_with_protocol_casing() {
let opts = StorageStateOptions::default()
.indexed_db(true)
.credentials(true);
let value = serde_json::to_value(&opts).unwrap();
assert_eq!(
value,
serde_json::json!({ "indexedDB": true, "credentials": true })
);
}
#[test]
fn http_credentials_serialize_with_protocol_casing() {
let bare = HttpCredentials::new("user", "secret");
assert_eq!(
serde_json::to_value(&bare).unwrap(),
serde_json::json!({ "username": "user", "password": "secret" })
);
let full = HttpCredentials::new("user", "secret")
.origin("https://example.test")
.send(HttpCredentialsSend::Always);
assert_eq!(
serde_json::to_value(&full).unwrap(),
serde_json::json!({
"username": "user",
"password": "secret",
"origin": "https://example.test",
"send": "always",
})
);
assert_eq!(
serde_json::to_value(HttpCredentialsSend::Unauthorized).unwrap(),
serde_json::json!("unauthorized")
);
}
#[test]
fn storage_state_round_trips_credentials_content() {
let json = serde_json::json!({
"cookies": [],
"origins": [],
"credentials": [{
"id": "Y3JlZA",
"rpId": "example.com",
"userHandle": "dXNlcg",
"privateKey": "cGtleQ",
"publicKey": "cHVi",
"signCount": 43
}]
});
let state: StorageState = serde_json::from_value(json.clone()).unwrap();
let back = serde_json::to_value(&state).unwrap();
assert_eq!(back["credentials"], json["credentials"]);
}
#[test]
fn origin_round_trips_indexed_db_payload_verbatim() {
let json = serde_json::json!({
"origin": "https://example.com",
"localStorage": [],
"indexedDB": [{"name": "db", "version": 1, "stores": []}]
});
let origin: Origin = serde_json::from_value(json.clone()).unwrap();
let back = serde_json::to_value(&origin).unwrap();
assert_eq!(back["indexedDB"], json["indexedDB"]);
assert!(back.get("indexedDb").is_none());
}
}