use super::{Geolocation, HttpCredentials, StorageState};
use crate::api::launch_options::IgnoreDefaultArgs;
use crate::protocol::ProxySettings;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Viewport {
pub width: u32,
pub height: u32,
}
impl From<crate::protocol::DeviceViewport> for Viewport {
fn from(size: crate::protocol::DeviceViewport) -> Self {
Self {
width: size.width,
height: size.height,
}
}
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
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>,
}
impl RecordHar {
pub fn new(path: impl Into<String>) -> Self {
Self {
path: path.into(),
omit_content: None,
content: None,
mode: None,
url_filter: None,
}
}
pub fn omit_content(mut self, omit_content: bool) -> Self {
self.omit_content = Some(omit_content);
self
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = Some(content.into());
self
}
pub fn mode(mut self, mode: impl Into<String>) -> Self {
self.mode = Some(mode.into());
self
}
pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
self.url_filter = Some(url_filter.into());
self
}
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RecordVideo {
pub dir: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<Viewport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_actions: Option<crate::protocol::ShowActionsOptions>,
}
impl RecordVideo {
pub fn new(dir: impl Into<String>) -> Self {
Self {
dir: dir.into(),
..Self::default()
}
}
pub fn size(mut self, size: Viewport) -> Self {
self.size = Some(size);
self
}
pub fn fps(mut self, fps: u32) -> Self {
self.fps = Some(fps);
self
}
pub fn show_actions(mut self, show_actions: crate::protocol::ShowActionsOptions) -> Self {
self.show_actions = Some(show_actions);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum AcceptDownloads {
#[serde(rename = "accept")]
Accept,
#[serde(rename = "deny")]
Deny,
#[serde(rename = "internal")]
Internal,
}
impl From<bool> for AcceptDownloads {
fn from(value: bool) -> Self {
if value { Self::Accept } else { Self::Deny }
}
}
fn serialize_header_pairs<S: serde::Serializer>(
headers: &Option<HashMap<String, String>>,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
#[derive(Serialize)]
struct NameValue<'a> {
name: &'a str,
value: &'a str,
}
match headers {
Some(headers) => serializer.collect_seq(
headers
.iter()
.map(|(name, value)| NameValue { name, value }),
),
None => serializer.serialize_none(),
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
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 screen: Option<Viewport>,
#[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(rename = "httpCredentials", skip_serializing_if = "Option::is_none")]
pub http_credentials: Option<Vec<HttpCredentials>>,
#[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(rename = "javaScriptEnabled", 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<AcceptDownloads>,
#[serde(rename = "bypassCSP", skip_serializing_if = "Option::is_none")]
pub bypass_csp: Option<bool>,
#[serde(rename = "ignoreHTTPSErrors", 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(
rename = "extraHTTPHeaders",
serialize_with = "serialize_header_pairs",
skip_serializing_if = "Option::is_none"
)]
pub extra_http_headers: Option<HashMap<String, String>>,
#[serde(rename = "baseURL", 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>,
#[deprecated(
since = "0.20.0",
note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` (persistent contexts) or `LaunchOptions::args` instead"
)]
#[serde(skip_serializing)]
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)]
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>,
screen: Option<Viewport>,
user_agent: Option<String>,
locale: Option<String>,
timezone_id: Option<String>,
geolocation: Option<Geolocation>,
http_credentials: Option<Vec<HttpCredentials>>,
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<AcceptDownloads>,
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 screen(mut self, screen: impl Into<Viewport>) -> Self {
self.screen = Some(screen.into());
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 http_credentials(mut self, credentials: Vec<HttpCredentials>) -> Self {
self.http_credentials = Some(credentials);
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: impl Into<AcceptDownloads>) -> Self {
self.accept_downloads = Some(accept_downloads.into());
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
}
#[deprecated(
since = "0.20.0",
note = "removed from the protocol in Playwright 1.58; setting it has no effect. For Chromium, pass `--auto-open-devtools-for-tabs` in `args` (persistent contexts) or `LaunchOptions::args` instead"
)]
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,
screen: self.screen,
user_agent: self.user_agent,
locale: self.locale,
timezone_id: self.timezone_id,
geolocation: self.geolocation,
http_credentials: self.http_credentials,
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,
#[allow(deprecated)]
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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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());
}
#[test]
fn every_key_sent_is_a_parameter_the_driver_declares() {
use crate::protocol_spec::{
BROWSER, BROWSER_TYPE, command_parameters, keys, mixin_properties, nested_properties,
};
let options = BrowserContextOptions {
viewport: Some(Viewport {
width: 1,
height: 1,
}),
no_viewport: Some(false),
screen: Some(Viewport {
width: 1,
height: 1,
}),
user_agent: Some(String::new()),
locale: Some(String::new()),
timezone_id: Some(String::new()),
geolocation: Some(crate::protocol::Geolocation {
latitude: 0.0,
longitude: 0.0,
accuracy: None,
}),
http_credentials: Some(vec![crate::protocol::HttpCredentials::new("u", "p")]),
permissions: Some(vec![]),
proxy: Some(crate::protocol::ProxySettings::new("http://proxy")),
color_scheme: Some(String::new()),
has_touch: Some(false),
is_mobile: Some(false),
javascript_enabled: Some(false),
offline: Some(false),
accept_downloads: Some(AcceptDownloads::Accept),
bypass_csp: Some(false),
ignore_https_errors: Some(false),
device_scale_factor: Some(1.0),
extra_http_headers: Some(HashMap::new()),
base_url: Some(String::new()),
storage_state: Some(crate::protocol::StorageState::default()),
storage_state_path: Some(String::new()),
args: Some(vec![]),
channel: Some(String::new()),
chromium_sandbox: Some(false),
#[allow(deprecated)]
devtools: Some(false),
downloads_path: Some(String::new()),
executable_path: Some(String::new()),
firefox_user_prefs: Some(HashMap::new()),
headless: Some(true),
ignore_default_args: Some(IgnoreDefaultArgs::Array(vec![])),
slow_mo: Some(0.0),
timeout: Some(0.0),
traces_dir: Some(String::new()),
strict_selectors: Some(false),
reduced_motion: Some(String::new()),
forced_colors: Some(String::new()),
service_workers: Some(String::new()),
record_har: Some(RecordHar::new("h.har")),
record_video: Some(
RecordVideo::new("videos")
.size(Viewport {
width: 1,
height: 1,
})
.fps(30)
.show_actions(
crate::protocol::ShowActionsOptions::default()
.duration(1.0)
.style(crate::protocol::ShowActionsStyle::default().title("x")),
),
),
};
let value = serde_json::to_value(&options).unwrap();
let video = &value["recordVideo"];
assert!(keys(video).is_subset(&nested_properties("ContextOptions", "recordVideo")));
assert!(keys(&video["showActions"]).is_subset(&mixin_properties("ShowActionsOptions")));
let sent = keys(&value);
let mut declared = command_parameters(BROWSER, "newContext");
declared.extend(command_parameters(BROWSER_TYPE, "launchPersistentContext"));
let undeclared: Vec<&str> = sent
.iter()
.filter(|key| !declared.contains(*key))
.map(String::as_str)
.collect();
assert_eq!(undeclared, ["storageStatePath", "timeout"]);
}
#[test]
fn acronym_options_use_the_driver_spelling() {
let options = BrowserContextOptions::builder()
.javascript_enabled(false)
.bypass_csp(true)
.ignore_https_errors(true)
.base_url("http://localhost".to_string())
.build();
let value = serde_json::to_value(&options).unwrap();
assert_eq!(value["javaScriptEnabled"], serde_json::json!(false));
assert_eq!(value["bypassCSP"], serde_json::json!(true));
assert_eq!(value["ignoreHTTPSErrors"], serde_json::json!(true));
assert_eq!(value["baseURL"], serde_json::json!("http://localhost"));
for wrong in [
"javascriptEnabled",
"bypassCsp",
"ignoreHttpsErrors",
"baseUrl",
] {
assert!(value.get(wrong).is_none(), "sent the unknown key {wrong}");
}
}
#[test]
fn extra_http_headers_serialize_as_name_value_pairs() {
let headers = HashMap::from([
("X-One".to_string(), "1".to_string()),
("X-Two".to_string(), "2".to_string()),
]);
let options = BrowserContextOptions::builder()
.extra_http_headers(headers)
.build();
let value = serde_json::to_value(&options).unwrap();
assert!(value.get("extraHttpHeaders").is_none());
let mut pairs = value["extraHTTPHeaders"]
.as_array()
.expect("extraHTTPHeaders is a list")
.clone();
pairs.sort_by_key(|pair| pair["name"].as_str().unwrap_or_default().to_string());
assert_eq!(
pairs,
vec![
serde_json::json!({"name": "X-One", "value": "1"}),
serde_json::json!({"name": "X-Two", "value": "2"}),
]
);
}
#[test]
fn test_accept_downloads_serializes_as_protocol_string() {
for (variant, expected) in [
(AcceptDownloads::Accept, "accept"),
(AcceptDownloads::Deny, "deny"),
(AcceptDownloads::Internal, "internal"),
] {
let options = BrowserContextOptions::builder()
.accept_downloads(variant)
.build();
let value = serde_json::to_value(&options).unwrap();
assert_eq!(value["acceptDownloads"], serde_json::json!(expected));
}
}
#[test]
fn test_accept_downloads_bool_compatibility() {
let opts = BrowserContextOptions::builder()
.accept_downloads(true)
.build();
assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Accept));
let opts = BrowserContextOptions::builder()
.accept_downloads(false)
.build();
assert_eq!(opts.accept_downloads, Some(AcceptDownloads::Deny));
}
}