browser_commander/browser/webdriver/
capabilities.rs1use super::{WebDriverBrowser, WebDriverOptions};
2use anyhow::{anyhow, Result};
3use serde_json::{json, Value};
4use std::path::Path;
5
6pub fn build_capabilities(
10 options: &WebDriverOptions,
11 profile: &Path,
12 staging: Option<&Path>,
13) -> Result<serde_json::Map<String, Value>> {
14 let mut preferences = options
15 .preferences
16 .as_object()
17 .cloned()
18 .ok_or_else(|| anyhow!("WebDriver preferences must be an object"))?;
19 let mut caps = options.capabilities.clone();
20 if options.browser.is_safari() {
21 use crate::browser::safari::unsupported;
22 for (feature, requested) in [
23 ("headless", options.headless),
24 ("persistent profile", options.user_data_dir.is_some()),
25 ("browser arguments", !options.args.is_empty()),
26 (
27 "preferences",
28 !preferences.is_empty() || options.local_state != json!({}),
29 ),
30 (
31 "browser executable (use driver_executable)",
32 options.browser_executable.is_some(),
33 ),
34 (
35 "managed downloads",
36 !matches!(options.downloads, crate::downloads::DownloadSetting::Off),
37 ),
38 (
39 "first-run settings",
40 options.first_run || options.default_browser_check == Some(true),
41 ),
42 ] {
43 if requested {
44 return Err(unsupported(feature).into());
45 }
46 }
47 for key in ["goog:chromeOptions", "moz:firefoxOptions", "webSocketUrl"] {
48 if caps.contains_key(key) {
49 return Err(unsupported(key).into());
50 }
51 }
52 caps.insert(
53 "browserName".into(),
54 json!(
55 if options.browser == WebDriverBrowser::SafariTechnologyPreview {
56 "Safari Technology Preview"
57 } else {
58 "safari"
59 }
60 ),
61 );
62 return Ok(caps);
63 }
64 let mut args = options.args.clone();
65 let key = match options.browser {
66 WebDriverBrowser::Chrome => "goog:chromeOptions",
67 WebDriverBrowser::Firefox => "moz:firefoxOptions",
68 _ => unreachable!("Safari capabilities returned above"),
69 };
70 let mut vendor = match caps.remove(key) {
71 Some(Value::Object(options)) => options,
72 Some(_) => return Err(anyhow!("{key} must be an object")),
73 None => Default::default(),
74 };
75 if let Some(extra) = vendor.remove("args") {
76 let extra: Vec<String> = serde_json::from_value(extra)?;
77 args.extend(extra);
78 }
79 if let Some(extra) = vendor.remove("prefs") {
80 let Value::Object(extra) = extra else {
81 return Err(anyhow!("{key}.prefs must be an object"));
82 };
83 for (key, value) in extra {
84 preferences.entry(key).or_insert(value);
85 }
86 }
87 match options.browser {
88 WebDriverBrowser::Chrome => {
89 if options.automation_parity {
90 let mut excluded: Vec<String> = vendor
91 .remove("excludeSwitches")
92 .map(serde_json::from_value)
93 .transpose()?
94 .unwrap_or_default();
95 for name in [
97 "allow-pre-commit-input",
98 "disable-background-networking",
99 "disable-background-timer-throttling",
100 "disable-backgrounding-occluded-windows",
101 "disable-client-side-phishing-detection",
102 "disable-default-apps",
103 "disable-features",
104 "disable-hang-monitor",
105 "disable-popup-blocking",
106 "disable-prompt-on-repost",
107 "disable-sync",
108 "enable-automation",
109 "enable-logging",
110 "log-level",
111 "no-first-run",
112 "no-service-autorun",
113 "password-store",
114 "test-type",
115 "use-mock-keychain",
116 ] {
117 if !excluded.iter().any(|value| value == name) {
118 excluded.push(name.into());
119 }
120 }
121 vendor.insert("excludeSwitches".into(), json!(excluded));
122 }
123 if args
124 .iter()
125 .any(|arg| arg.trim_start_matches('-').starts_with("user-data-dir"))
126 {
127 return Err(anyhow!(
128 "set user_data_dir instead of a Chrome profile argument"
129 ));
130 }
131 args.push(format!("--user-data-dir={}", profile.display()));
132 if options.headless {
133 args.push("--headless=new".into());
134 }
135 if !options.sandbox {
136 args.push("--no-sandbox".into());
137 }
138 if let Some(path) = staging {
139 preferences.insert("download.default_directory".into(), json!(path));
140 preferences.insert("download.prompt_for_download".into(), json!(false));
141 preferences.insert("download.directory_upgrade".into(), json!(true));
142 }
143 caps.insert("browserName".into(), json!("chrome"));
144 }
145 WebDriverBrowser::Firefox => {
146 if vendor.contains_key("profile") {
147 return Err(anyhow!(
148 "set user_data_dir instead of moz:firefoxOptions.profile"
149 ));
150 }
151 if args.iter().any(|arg| {
152 matches!(arg.as_str(), "-profile" | "--profile") || arg.starts_with("--profile=")
153 }) {
154 return Err(anyhow!(
155 "set user_data_dir instead of a Firefox profile argument"
156 ));
157 }
158 args.extend(["-profile".into(), profile.to_string_lossy().into_owned()]);
159 if options.headless {
160 args.push("-headless".into());
161 }
162 if let Some(path) = staging {
163 preferences.insert("browser.download.dir".into(), json!(path));
164 preferences.insert("browser.download.folderList".into(), json!(2));
165 preferences.insert("browser.download.useDownloadDir".into(), json!(true));
166 preferences
167 .entry("browser.helperApps.neverAsk.saveToDisk")
168 .or_insert(json!(
169 "application/octet-stream,text/plain,application/pdf,text/csv,application/zip"
170 ));
171 preferences.insert("pdfjs.disabled".into(), json!(true));
172 }
173 caps.insert("browserName".into(), json!("firefox"));
174 }
175 _ => unreachable!("Safari capabilities returned above"),
176 }
177 vendor.insert("args".into(), json!(args));
178 vendor.insert("prefs".into(), json!(preferences));
179 if let Some(binary) = &options.browser_executable {
180 vendor.insert("binary".into(), json!(binary));
181 }
182 caps.insert(key.into(), json!(vendor));
183 if options.bidi {
184 caps.insert("webSocketUrl".into(), json!(true));
185 }
186 Ok(caps)
187}