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 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}