Skip to main content

browser_commander/browser/webdriver/
adapter.rs

1//! Common browser operations backed by the typed W3C client.
2
3use 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}