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 engine_type(&self) -> EngineType {
80 EngineType::Fantoccini
81 }
82 async fn url(&self) -> Result<String, EngineError> {
83 Ok(self.client.current_url().await.map_err(error)?.into())
84 }
85 async fn goto(&self, url: &str) -> Result<(), EngineError> {
86 self.client.goto(url).await.map_err(error)
87 }
88 async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError> {
89 Ok(self.elements(selector).await?.into_iter().next())
90 }
91 async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError> {
92 self.elements(selector).await
93 }
94 async fn count(&self, selector: &str) -> Result<usize, EngineError> {
95 Ok(self
96 .client
97 .find_all(Locator::Css(selector))
98 .await
99 .map_err(error)?
100 .len())
101 }
102 async fn click(&self, selector: &str) -> Result<(), EngineError> {
103 self.client
104 .find(Locator::Css(selector))
105 .await
106 .map_err(error)?
107 .click()
108 .await
109 .map_err(error)
110 }
111 async fn mouse_click(&self, x: f64, y: f64) -> Result<(), EngineError> {
112 self.client
113 .perform_actions(
114 MouseActions::new("bc-pointer".into())
115 .then(PointerAction::MoveTo {
116 duration: None,
117 x,
118 y,
119 })
120 .then(PointerAction::Down { button: 0 })
121 .then(PointerAction::Up { button: 0 }),
122 )
123 .await
124 .map_err(error)
125 }
126 async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError> {
127 let element = self
128 .client
129 .find(Locator::Css(selector))
130 .await
131 .map_err(error)?;
132 element.clear().await.map_err(error)?;
133 element.send_keys(text).await.map_err(error)
134 }
135 async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError> {
136 self.client
137 .find(Locator::Css(selector))
138 .await
139 .map_err(error)?
140 .send_keys(text)
141 .await
142 .map_err(error)
143 }
144 async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError> {
145 Ok(self
146 .client
147 .execute(
148 "return document.querySelector(arguments[0])?.textContent ?? null",
149 vec![json!(selector)],
150 )
151 .await
152 .map_err(error)?
153 .as_str()
154 .map(str::to_owned))
155 }
156 async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError> {
157 Ok(self
158 .client
159 .execute(
160 "return document.querySelector(arguments[0])?.value ?? null",
161 vec![json!(selector)],
162 )
163 .await
164 .map_err(error)?
165 .as_str()
166 .map(str::to_owned))
167 }
168 async fn get_attribute(
169 &self,
170 selector: &str,
171 attribute: &str,
172 ) -> Result<Option<String>, EngineError> {
173 Ok(self
174 .client
175 .execute(
176 "return document.querySelector(arguments[0])?.getAttribute(arguments[1]) ?? null",
177 vec![json!(selector), json!(attribute)],
178 )
179 .await
180 .map_err(error)?
181 .as_str()
182 .map(str::to_owned))
183 }
184 async fn is_visible(&self, selector: &str) -> Result<bool, EngineError> {
185 Ok(self
186 .query_selector(selector)
187 .await?
188 .is_some_and(|element| element.is_visible))
189 }
190 async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError> {
191 Ok(self
192 .query_selector(selector)
193 .await?
194 .is_some_and(|element| element.is_enabled))
195 }
196 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError> {
197 tokio::time::timeout(Duration::from_millis(timeout_ms), async {
198 loop {
199 if self.count(selector).await? > 0 {
200 return Ok(());
201 }
202 tokio::time::sleep(Duration::from_millis(50)).await;
203 }
204 })
205 .await
206 .map_err(|_| EngineError::Timeout(format!("selector {selector}")))?
207 }
208 async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError> {
209 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)?;
210 if found == true {
211 Ok(())
212 } else {
213 Err(EngineError::ElementNotFound(selector.into()))
214 }
215 }
216 async fn evaluate(&self, script: &str) -> Result<Value, EngineError> {
217 self.client
218 .execute("return (0,eval)(arguments[0])", vec![json!(script)])
219 .await
220 .map_err(error)
221 }
222 async fn read_browser_version_page(&self) -> Result<Value, EngineError> {
223 let previous = self.client.window().await.map_err(error)?;
224 let tab = self.client.new_window(true).await.map_err(error)?;
225 let read = async {
226 self.client
227 .switch_to_window(tab.handle)
228 .await
229 .map_err(error)?;
230 self.client.goto("chrome://version").await.map_err(error)?;
231 tokio::time::timeout(Duration::from_secs(10), async {
232 loop {
233 let value = self.evaluate(crate::parity::VERSION_EXPRESSION).await?;
234 if !value.is_null() {
235 return Ok::<_, EngineError>(value);
236 }
237 tokio::time::sleep(Duration::from_millis(100)).await;
238 }
239 })
240 .await
241 .map_err(|_| EngineError::Timeout("chrome://version did not render".into()))?
242 }
243 .await;
244 let _ = self.client.close_window().await;
245 let _ = self.client.switch_to_window(previous).await;
246 read
247 }
248 async fn restore_storage_state(&self, state: Value) -> Result<(), EngineError> {
249 self.restore_state(serde_json::from_value(state).map_err(error)?)
250 .await
251 .map_err(error)
252 }
253 async fn export_storage_state(&self) -> Result<Value, EngineError> {
254 serde_json::to_value(self.save_state().await.map_err(error)?).map_err(error)
255 }
256 async fn screenshot(&self) -> Result<Vec<u8>, EngineError> {
257 self.client.screenshot().await.map_err(error)
258 }
259 async fn pdf(&self, options: PdfOptions) -> Result<Vec<u8>, EngineError> {
260 use fantoccini::wd::{PrintConfiguration, PrintMargins, PrintSize};
261 let size = match options.format.as_deref().unwrap_or("A4") {
262 "A4" => PrintSize::A4,
263 "Letter" => PrintSize::US_LETTER,
264 "Legal" => PrintSize::US_LEGAL,
265 other => {
266 return Err(error(format!(
267 "Unsupported WebDriver paper format: {other}"
268 )))
269 }
270 };
271 fn cm(value: Option<&str>) -> Result<f64, EngineError> {
272 let Some(value) = value else { return Ok(1.0) };
273 for (unit, factor) in [("cm", 1.0), ("mm", 0.1), ("in", 2.54), ("px", 2.54 / 96.0)] {
274 if let Some(number) = value.strip_suffix(unit) {
275 return number
276 .trim()
277 .parse::<f64>()
278 .map(|n| n * factor)
279 .map_err(error);
280 }
281 }
282 value.parse().map_err(error)
283 }
284 let margins = PrintMargins {
285 top: cm(options.margin_top.as_deref())?,
286 bottom: cm(options.margin_bottom.as_deref())?,
287 left: cm(options.margin_left.as_deref())?,
288 right: cm(options.margin_right.as_deref())?,
289 };
290 let config = PrintConfiguration::builder()
291 .size(size)
292 .background(options.print_background)
293 .scale(options.scale.unwrap_or(1.0))
294 .margins(margins)
295 .build()
296 .map_err(error)?;
297 let bytes = self.client.print(config).await.map_err(error)?;
298 if let Some(path) = options.path {
299 std::fs::write(path, &bytes).map_err(error)?;
300 }
301 Ok(bytes)
302 }
303 async fn bring_to_front(&self) -> Result<(), EngineError> {
304 let handle = self.client.window().await.map_err(error)?;
305 self.client.switch_to_window(handle).await.map_err(error)
306 }
307 async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError> {
308 tokio::time::timeout(Duration::from_millis(timeout_ms), async {
309 loop {
310 if self.evaluate("document.readyState").await? == "complete" {
311 return Ok(());
312 }
313 tokio::time::sleep(Duration::from_millis(50)).await;
314 }
315 })
316 .await
317 .map_err(|_| EngineError::Timeout("document readiness".into()))?
318 }
319 async fn keyboard_press(&self, keys: &str) -> Result<(), EngineError> {
320 let values = keys.split('+').map(key).collect::<Result<Vec<_>, _>>()?;
321 let mut actions = values
322 .iter()
323 .map(|value| KeyAction::Down { value: *value })
324 .collect::<Vec<_>>();
325 actions.extend(
326 values
327 .into_iter()
328 .rev()
329 .map(|value| KeyAction::Up { value }),
330 );
331 self.keys(actions).await
332 }
333 async fn keyboard_type(&self, text: &str) -> Result<(), EngineError> {
334 self.client
335 .active_element()
336 .await
337 .map_err(error)?
338 .send_keys(text)
339 .await
340 .map_err(error)
341 }
342 async fn keyboard_down(&self, value: &str) -> Result<(), EngineError> {
343 self.keys(vec![KeyAction::Down { value: key(value)? }])
344 .await
345 }
346 async fn keyboard_up(&self, value: &str) -> Result<(), EngineError> {
347 self.keys(vec![KeyAction::Up { value: key(value)? }]).await
348 }
349}