browser_commander/browser/webdriver/
adapter.rs1use super::ManagedWebDriver;
4use crate::core::engine::{ElementInfo, EngineAdapter, EngineError, EngineType, PdfOptions};
5use async_trait::async_trait;
6use fantoccini::{
7 actions::{InputSource, KeyAction, KeyActions, MouseActions, PointerAction},
8 Locator,
9};
10use serde_json::{json, Value};
11use std::time::Duration;
12
13fn error(error: impl std::fmt::Display) -> EngineError {
14 EngineError::Browser(error.to_string())
15}
16fn key(value: &str) -> Result<char, EngineError> {
17 use fantoccini::key::Key;
18 Ok(match value {
19 "Enter" => Key::Enter.into(),
20 "Tab" => Key::Tab.into(),
21 "Escape" | "Esc" => Key::Escape.into(),
22 "Shift" => Key::Shift.into(),
23 "Control" | "Ctrl" => Key::Control.into(),
24 "Alt" => Key::Alt.into(),
25 "Meta" => Key::Meta.into(),
26 "Backspace" => Key::Backspace.into(),
27 "Delete" => Key::Delete.into(),
28 "Space" => Key::Space.into(),
29 "ArrowUp" => Key::Up.into(),
30 "ArrowDown" => Key::Down.into(),
31 "ArrowLeft" => Key::Left.into(),
32 "ArrowRight" => Key::Right.into(),
33 "Home" => Key::Home.into(),
34 "End" => Key::End.into(),
35 "PageUp" => Key::PageUp.into(),
36 "PageDown" => Key::PageDown.into(),
37 value if value.chars().count() == 1 => value.chars().next().unwrap(),
38 _ => return Err(error(format!("Unknown WebDriver key: {value}"))),
39 })
40}
41
42impl ManagedWebDriver {
43 async fn elements(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError> {
44 let result=self.client.execute(r#"return [...document.querySelectorAll(arguments[0])].map(e=>{
45 const r=e.getBoundingClientRect(); const style=getComputedStyle(e);
46 return {tag:e.tagName,text:e.textContent,visible:!!(r.width&&r.height)&&style.visibility!=='hidden'&&style.display!=='none',
47 enabled:!e.disabled,box:[r.x,r.y,r.width,r.height]};
48 })"#,vec![json!(selector)]).await.map_err(error)?;
49 let values = result
50 .as_array()
51 .ok_or_else(|| error("Invalid WebDriver element response"))?;
52 values
53 .iter()
54 .map(|value| {
55 Ok(ElementInfo {
56 tag_name: value["tag"].as_str().unwrap_or("").into(),
57 text_content: value["text"].as_str().map(str::to_owned),
58 is_visible: value["visible"].as_bool().unwrap_or(false),
59 is_enabled: value["enabled"].as_bool().unwrap_or(false),
60 bounding_box: Some(
61 serde_json::from_value(value["box"].clone()).map_err(error)?,
62 ),
63 })
64 })
65 .collect()
66 }
67 async fn keys(&self, actions: Vec<KeyAction>) -> Result<(), EngineError> {
68 let sequence = actions
69 .into_iter()
70 .fold(KeyActions::new("bc-keyboard".into()), |sequence, action| {
71 sequence.then(action)
72 });
73 self.client.perform_actions(sequence).await.map_err(error)
74 }
75}
76
77#[async_trait]
78impl EngineAdapter for ManagedWebDriver {
79 fn require_feature(&self, feature: &str) -> Result<(), EngineError> {
80 ManagedWebDriver::require_feature(self, feature)
81 }
82 fn engine_type(&self) -> EngineType {
83 EngineType::Fantoccini
84 }
85 async fn url(&self) -> Result<String, EngineError> {
86 Ok(self.client.current_url().await.map_err(error)?.into())
87 }
88 async fn goto(&self, url: &str) -> Result<(), EngineError> {
89 self.client.goto(url).await.map_err(error)
90 }
91 async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError> {
92 Ok(self.elements(selector).await?.into_iter().next())
93 }
94 async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError> {
95 self.elements(selector).await
96 }
97 async fn count(&self, selector: &str) -> Result<usize, EngineError> {
98 Ok(self
99 .client
100 .find_all(Locator::Css(selector))
101 .await
102 .map_err(error)?
103 .len())
104 }
105 async fn click(&self, selector: &str) -> Result<(), EngineError> {
106 self.client
107 .find(Locator::Css(selector))
108 .await
109 .map_err(error)?
110 .click()
111 .await
112 .map_err(error)
113 }
114 async fn mouse_click(&self, x: f64, y: f64) -> Result<(), EngineError> {
115 self.client
116 .perform_actions(
117 MouseActions::new("bc-pointer".into())
118 .then(PointerAction::MoveTo {
119 duration: None,
120 x,
121 y,
122 })
123 .then(PointerAction::Down { button: 0 })
124 .then(PointerAction::Up { button: 0 }),
125 )
126 .await
127 .map_err(error)
128 }
129 async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError> {
130 let element = self
131 .client
132 .find(Locator::Css(selector))
133 .await
134 .map_err(error)?;
135 element.clear().await.map_err(error)?;
136 element.send_keys(text).await.map_err(error)
137 }
138 async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError> {
139 self.client
140 .find(Locator::Css(selector))
141 .await
142 .map_err(error)?
143 .send_keys(text)
144 .await
145 .map_err(error)
146 }
147 async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError> {
148 Ok(self
149 .client
150 .execute(
151 "return document.querySelector(arguments[0])?.textContent ?? null",
152 vec![json!(selector)],
153 )
154 .await
155 .map_err(error)?
156 .as_str()
157 .map(str::to_owned))
158 }
159 async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError> {
160 Ok(self
161 .client
162 .execute(
163 "return document.querySelector(arguments[0])?.value ?? null",
164 vec![json!(selector)],
165 )
166 .await
167 .map_err(error)?
168 .as_str()
169 .map(str::to_owned))
170 }
171 async fn get_attribute(
172 &self,
173 selector: &str,
174 attribute: &str,
175 ) -> Result<Option<String>, EngineError> {
176 Ok(self
177 .client
178 .execute(
179 "return document.querySelector(arguments[0])?.getAttribute(arguments[1]) ?? null",
180 vec![json!(selector), json!(attribute)],
181 )
182 .await
183 .map_err(error)?
184 .as_str()
185 .map(str::to_owned))
186 }
187 async fn is_visible(&self, selector: &str) -> Result<bool, EngineError> {
188 Ok(self
189 .query_selector(selector)
190 .await?
191 .is_some_and(|element| element.is_visible))
192 }
193 async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError> {
194 Ok(self
195 .query_selector(selector)
196 .await?
197 .is_some_and(|element| element.is_enabled))
198 }
199 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError> {
200 tokio::time::timeout(Duration::from_millis(timeout_ms), async {
201 loop {
202 if self.count(selector).await? > 0 {
203 return Ok(());
204 }
205 tokio::time::sleep(Duration::from_millis(50)).await;
206 }
207 })
208 .await
209 .map_err(|_| EngineError::Timeout(format!("selector {selector}")))?
210 }
211 async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError> {
212 let found=self.client.execute("const e=document.querySelector(arguments[0]); if(!e)return false; e.scrollIntoView({block:'center',inline:'center'}); return true",vec![json!(selector)]).await.map_err(error)?;
213 if found == true {
214 Ok(())
215 } else {
216 Err(EngineError::ElementNotFound(selector.into()))
217 }
218 }
219 async fn evaluate(&self, script: &str) -> Result<Value, EngineError> {
220 self.client
221 .execute("return (0,eval)(arguments[0])", vec![json!(script)])
222 .await
223 .map_err(error)
224 }
225 async fn read_browser_version_page(&self) -> Result<Value, EngineError> {
226 let previous = self.client.window().await.map_err(error)?;
227 let tab = self.client.new_window(true).await.map_err(error)?;
228 let read = async {
229 self.client
230 .switch_to_window(tab.handle)
231 .await
232 .map_err(error)?;
233 self.client.goto("chrome://version").await.map_err(error)?;
234 tokio::time::timeout(Duration::from_secs(10), async {
235 loop {
236 let value = self.evaluate(crate::parity::VERSION_EXPRESSION).await?;
237 if !value.is_null() {
238 return Ok::<_, EngineError>(value);
239 }
240 tokio::time::sleep(Duration::from_millis(100)).await;
241 }
242 })
243 .await
244 .map_err(|_| EngineError::Timeout("chrome://version did not render".into()))?
245 }
246 .await;
247 let _ = self.client.close_window().await;
248 let _ = self.client.switch_to_window(previous).await;
249 read
250 }
251 async fn restore_storage_state(&self, state: Value) -> Result<(), EngineError> {
252 self.restore_state(serde_json::from_value(state).map_err(error)?)
253 .await
254 .map_err(error)
255 }
256 async fn export_storage_state(&self) -> Result<Value, EngineError> {
257 serde_json::to_value(self.save_state().await.map_err(error)?).map_err(error)
258 }
259 async fn screenshot(&self) -> Result<Vec<u8>, EngineError> {
260 self.client.screenshot().await.map_err(error)
261 }
262 async fn pdf(&self, options: PdfOptions) -> Result<Vec<u8>, EngineError> {
263 self.require_feature("PDF")?;
264 use fantoccini::wd::{PrintConfiguration, PrintMargins, PrintSize};
265 let size = match options.format.as_deref().unwrap_or("A4") {
266 "A4" => PrintSize::A4,
267 "Letter" => PrintSize::US_LETTER,
268 "Legal" => PrintSize::US_LEGAL,
269 other => {
270 return Err(error(format!(
271 "Unsupported WebDriver paper format: {other}"
272 )))
273 }
274 };
275 fn cm(value: Option<&str>) -> Result<f64, EngineError> {
276 let Some(value) = value else { return Ok(1.0) };
277 for (unit, factor) in [("cm", 1.0), ("mm", 0.1), ("in", 2.54), ("px", 2.54 / 96.0)] {
278 if let Some(number) = value.strip_suffix(unit) {
279 return number
280 .trim()
281 .parse::<f64>()
282 .map(|n| n * factor)
283 .map_err(error);
284 }
285 }
286 value.parse().map_err(error)
287 }
288 let margins = PrintMargins {
289 top: cm(options.margin_top.as_deref())?,
290 bottom: cm(options.margin_bottom.as_deref())?,
291 left: cm(options.margin_left.as_deref())?,
292 right: cm(options.margin_right.as_deref())?,
293 };
294 let config = PrintConfiguration::builder()
295 .size(size)
296 .background(options.print_background)
297 .scale(options.scale.unwrap_or(1.0))
298 .margins(margins)
299 .build()
300 .map_err(error)?;
301 let bytes = self.client.print(config).await.map_err(error)?;
302 if let Some(path) = options.path {
303 std::fs::write(path, &bytes).map_err(error)?;
304 }
305 Ok(bytes)
306 }
307 async fn bring_to_front(&self) -> Result<(), EngineError> {
308 let handle = self.client.window().await.map_err(error)?;
309 self.client.switch_to_window(handle).await.map_err(error)
310 }
311 async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError> {
312 tokio::time::timeout(Duration::from_millis(timeout_ms), async {
313 loop {
314 if self.evaluate("document.readyState").await? == "complete" {
315 return Ok(());
316 }
317 tokio::time::sleep(Duration::from_millis(50)).await;
318 }
319 })
320 .await
321 .map_err(|_| EngineError::Timeout("document readiness".into()))?
322 }
323 async fn keyboard_press(&self, keys: &str) -> Result<(), EngineError> {
324 let values = keys.split('+').map(key).collect::<Result<Vec<_>, _>>()?;
325 let mut actions = values
326 .iter()
327 .map(|value| KeyAction::Down { value: *value })
328 .collect::<Vec<_>>();
329 actions.extend(
330 values
331 .into_iter()
332 .rev()
333 .map(|value| KeyAction::Up { value }),
334 );
335 self.keys(actions).await
336 }
337 async fn keyboard_type(&self, text: &str) -> Result<(), EngineError> {
338 self.client
339 .active_element()
340 .await
341 .map_err(error)?
342 .send_keys(text)
343 .await
344 .map_err(error)
345 }
346 async fn keyboard_down(&self, value: &str) -> Result<(), EngineError> {
347 self.keys(vec![KeyAction::Down { value: key(value)? }])
348 .await
349 }
350 async fn keyboard_up(&self, value: &str) -> Result<(), EngineError> {
351 self.keys(vec![KeyAction::Up { value: key(value)? }]).await
352 }
353}