rusty_driver/
lib.rs

1//! A high-level API for programmatically interacting with web pages
2//! through WebDriver.
3//!
4//! [WebDriver protocol]: https://www.w3.org/TR/webdriver/
5//! [CSS selectors]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
6//! [powerful]: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
7//! [operators]: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
8//! [WebDriver compatible]: https://github.com/Fyrd/caniuse/issues/2757#issuecomment-304529217
9//! [`geckodriver`]: https://github.com/mozilla/geckodriver
10
11#[macro_use]
12extern crate error_chain;
13
14pub mod error;
15mod protocol;
16
17use crate::error::*;
18pub use hyper::Method;
19use protocol::Client;
20use serde_json::Value;
21use std::time::Duration;
22use tokio::time::sleep;
23use webdriver::{
24    command::{SwitchToFrameParameters, SwitchToWindowParameters, WebDriverCommand},
25    common::{FrameId, WebElement, ELEMENT_KEY},
26    error::{ErrorStatus, WebDriverError},
27};
28
29#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)]
30pub enum Locator {
31    Css(String),
32    LinkText(String),
33    XPath(String),
34}
35
36impl Into<webdriver::command::LocatorParameters> for Locator {
37    fn into(self) -> webdriver::command::LocatorParameters {
38        match self {
39            Locator::Css(s) => webdriver::command::LocatorParameters {
40                using: webdriver::common::LocatorStrategy::CSSSelector,
41                value: s,
42            },
43            Locator::XPath(s) => webdriver::command::LocatorParameters {
44                using: webdriver::common::LocatorStrategy::XPath,
45                value: s,
46            },
47            Locator::LinkText(s) => webdriver::command::LocatorParameters {
48                using: webdriver::common::LocatorStrategy::LinkText,
49                value: s,
50            },
51        }
52    }
53}
54
55pub struct Driver(Client);
56
57macro_rules! generate_wait_for_find {
58    ($name:ident, $search_fn:ident, $return_typ:ty) => {
59        /// Wait for the specified element(s) to appear on the page
60        pub async fn $name(
61            &self,
62            search: Locator,
63            root: Option<WebElement>
64        ) -> Result<$return_typ> {
65            loop {
66                match self.$search_fn(search.clone(), root.clone()).await {
67                    Ok(e) => break Ok(e),
68                    Err(Error(ErrorKind::WebDriver(
69                        WebDriverError {error: ErrorStatus::NoSuchElement, ..}
70                    ), _)) => sleep(Duration::from_millis(100)).await,
71                    Err(e) => break Err(e)
72                }
73            }
74        }
75    }
76}
77
78impl Driver {
79    /// Create a new webdriver session on the specified server
80    pub async fn new(webdriver_url: &str, user_agent: Option<String>) -> Result<Self> {
81        Ok(Driver(Client::new(webdriver_url, user_agent).await?))
82    }
83
84    /// Navigate directly to the given URL.
85    pub async fn goto<'a>(&'a self, url: &'a str) -> Result<()> {
86        let cmd = WebDriverCommand::Get(webdriver::command::GetParameters {
87            url: self.current_url().await?.join(url)?.into(),
88        });
89        self.0.issue_cmd(&cmd).await?;
90        Ok(())
91    }
92
93    /// Retrieve the currently active URL for this session.
94    pub async fn current_url(&self) -> Result<url::Url> {
95        match self.0.issue_cmd(&WebDriverCommand::GetCurrentUrl).await?.as_str() {
96            Some(url) => Ok(url.parse()?),
97            None => bail!(ErrorKind::NotW3C(Value::Null)),
98        }
99    }
100
101    /// Get the HTML source for the current page.
102    pub async fn source(&self) -> Result<String> {
103        match self.0.issue_cmd(&WebDriverCommand::GetPageSource).await?.as_str() {
104            Some(src) => Ok(src.to_string()),
105            None => bail!(ErrorKind::NotW3C(Value::Null)),
106        }
107    }
108
109    /// Go back to the previous page.
110    pub async fn back(&self) -> Result<()> {
111        self.0.issue_cmd(&WebDriverCommand::GoBack).await?;
112        Ok(())
113    }
114
115    /// Refresh the current previous page.
116    pub async fn refresh(&self) -> Result<()> {
117        self.0.issue_cmd(&WebDriverCommand::Refresh).await?;
118        Ok(())
119    }
120
121    /// Switch the focus to the frame contained in Element
122    pub async fn switch_to_frame(&self, frame: WebElement) -> Result<()> {
123        let p = SwitchToFrameParameters {
124            id: Some(FrameId::Element(frame)),
125        };
126        let cmd = WebDriverCommand::SwitchToFrame(p);
127        self.0.issue_cmd(&cmd).await?;
128        Ok(())
129    }
130
131    /// Switch the focus to this frame's parent frame
132    pub async fn switch_to_parent_frame(&self) -> Result<()> {
133        self.0.issue_cmd(&WebDriverCommand::SwitchToParentFrame).await?;
134        Ok(())
135    }
136
137    /// Switch the focus to the window identified by handle
138    pub async fn switch_to_window(&self, window: String) -> Result<()> {
139        let p = SwitchToWindowParameters { handle: window };
140        let cmd = WebDriverCommand::SwitchToWindow(p);
141        self.0.issue_cmd(&cmd).await?;
142        Ok(())
143    }
144
145    /// Execute the given JavaScript `script` in the current browser session.
146    ///
147    /// `args` is available to the script inside the `arguments`
148    /// array. Since `Element` implements `ToJson`, you can also
149    /// provide serialized `Element`s as arguments, and they will
150    /// correctly serialize to DOM elements on the other side.
151    pub async fn execute(&self, script: String, mut args: Vec<Value>) -> Result<Value> {
152        self.fixup_elements(&mut args);
153        let cmd = webdriver::command::JavascriptCommandParameters {
154            script: script,
155            args: Some(args),
156        };
157        let cmd = WebDriverCommand::ExecuteScript(cmd);
158        self.0.issue_cmd(&cmd).await
159    }
160
161    /// Wait for the page to navigate to a new URL before proceeding.
162    ///
163    /// If the `current` URL is not provided, `self.current_url()`
164    /// will be used. Note however that this introduces a race
165    /// condition: the browser could finish navigating *before* we
166    /// call `current_url()`, which would lead to an eternal wait.
167    pub async fn wait_for_navigation(&self, current: Option<url::Url>) -> Result<()> {
168        let current = match current {
169            Some(current) => current,
170            None => self.current_url().await?,
171        };
172        loop {
173            if self.current_url().await? != current {
174                break Ok(());
175            }
176            sleep(Duration::from_millis(100)).await
177        }
178    }
179
180    /// Starting from the document root, find the first element on the page that
181    /// matches the specified selector.
182    pub async fn find(
183        &self,
184        locator: Locator,
185        root: Option<WebElement>,
186    ) -> Result<WebElement> {
187        let cmd = match root {
188            Option::None => WebDriverCommand::FindElement(locator.into()),
189            Option::Some(elt) => {
190                WebDriverCommand::FindElementElement(elt, locator.into())
191            }
192        };
193        let res = self.0.issue_cmd(&cmd).await?;
194        Ok(self.parse_lookup(res)?)
195    }
196
197    pub async fn find_all(
198        &self,
199        locator: Locator,
200        root: Option<WebElement>,
201    ) -> Result<Vec<WebElement>> {
202        let cmd = match root {
203            Option::None => WebDriverCommand::FindElements(locator.into()),
204            Option::Some(elt) => {
205                WebDriverCommand::FindElementElements(elt, locator.into())
206            }
207        };
208        match self.0.issue_cmd(&cmd).await? {
209            Value::Array(a) => Ok(a
210                .into_iter()
211                .map(|e| self.parse_lookup(e))
212                .collect::<Result<Vec<WebElement>>>()?),
213            r => bail!(ErrorKind::NotW3C(r)),
214        }
215    }
216
217    generate_wait_for_find!(wait_for_find, find, WebElement);
218    generate_wait_for_find!(wait_for_find_all, find_all, Vec<WebElement>);
219
220    /// Extract the `WebElement` from a `FindElement` or `FindElementElement` command.
221    fn parse_lookup(&self, mut res: Value) -> Result<WebElement> {
222        let key = if self.0.legacy {
223            "ELEMENT"
224        } else {
225            ELEMENT_KEY
226        };
227        let o = {
228            if let Some(o) = res.as_object_mut() {
229                o
230            } else {
231                bail!(ErrorKind::NotW3C(res))
232            }
233        };
234        match o.remove(key) {
235            None => bail!(ErrorKind::NotW3C(res)),
236            Some(Value::String(wei)) => Ok(webdriver::common::WebElement(wei)),
237            Some(v) => {
238                o.insert(key.to_string(), v);
239                bail!(ErrorKind::NotW3C(res))
240            }
241        }
242    }
243
244    fn fixup_elements(&self, args: &mut [Value]) {
245        if self.0.legacy {
246            for arg in args {
247                // the serialization of WebElement uses the W3C index,
248                // but legacy implementations need us to use the "ELEMENT" index
249                if let Value::Object(ref mut o) = *arg {
250                    if let Some(wei) = o.remove(ELEMENT_KEY) {
251                        o.insert("ELEMENT".to_string(), wei);
252                    }
253                }
254            }
255        }
256    }
257
258    /// Look up an attribute value for this element by name.
259    pub async fn attr(
260        &self,
261        eid: WebElement,
262        attribute: String,
263    ) -> Result<Option<String>> {
264        let cmd = WebDriverCommand::GetElementAttribute(eid, attribute);
265        match self.0.issue_cmd(&cmd).await? {
266            Value::String(v) => Ok(Some(v)),
267            Value::Null => Ok(None),
268            v => bail!(ErrorKind::NotW3C(v)),
269        }
270    }
271
272    /// Look up a DOM property for this element by name.
273    pub async fn prop(&self, eid: WebElement, prop: String) -> Result<Option<String>> {
274        let cmd = WebDriverCommand::GetElementProperty(eid, prop);
275        match self.0.issue_cmd(&cmd).await? {
276            Value::String(v) => Ok(Some(v)),
277            Value::Null => Ok(None),
278            v => bail!(ErrorKind::NotW3C(v)),
279        }
280    }
281
282    /// Retrieve the text contents of this elment.
283    pub async fn text(&self, eid: WebElement) -> Result<String> {
284        let cmd = WebDriverCommand::GetElementText(eid);
285        match self.0.issue_cmd(&cmd).await? {
286            Value::String(v) => Ok(v),
287            v => bail!(ErrorKind::NotW3C(v)),
288        }
289    }
290
291    /// Retrieve the HTML contents of this element. if inner is true,
292    /// also return the wrapping nodes html. Note: this is the same as
293    /// calling `prop("innerHTML")` or `prop("outerHTML")`.
294    pub async fn html(&self, eid: WebElement, inner: bool) -> Result<String> {
295        let prop = if inner { "innerHTML" } else { "outerHTML" };
296        self.prop(eid, prop.to_owned()).await?
297            .ok_or_else(|| Error::from(ErrorKind::NotW3C(Value::Null)))
298    }
299
300    /// Click on this element
301    pub async fn click(&self, eid: WebElement) -> Result<()> {
302        let cmd = WebDriverCommand::ElementClick(eid);
303        let r = self.0.issue_cmd(&cmd).await?;
304        if r.is_null() || r.as_object().map(|o| o.is_empty()).unwrap_or(false) {
305            // geckodriver returns {} :(
306            Ok(())
307        } else {
308            bail!(ErrorKind::NotW3C(r))
309        }
310    }
311
312    /// Scroll this element into view
313    pub async fn scroll_into_view(&self, eid: WebElement) -> Result<()> {
314        let args = vec![serde_json::to_value(eid)?];
315        let js = "arguments[0].scrollIntoView(true)".to_string();
316        self.clone().execute(js, args).await?;
317        Ok(())
318    }
319
320    /// Follow the `href` target of the element matching the given CSS
321    /// selector *without* causing a click interaction.
322    pub async fn follow(&self, eid: WebElement) -> Result<()> {
323        match self.clone().attr(eid.clone(), String::from("href")).await? {
324            None => bail!("no href attribute"),
325            Some(href) => {
326                let current = self.current_url().await?.join(&href)?;
327                self.goto(current.as_str()).await
328            }
329        }
330    }
331
332    /// Set the `value` of the input element named `name` which is a child of `eid`
333    pub async fn set_by_name(
334        &self,
335        eid: WebElement,
336        name: String,
337        value: String,
338    ) -> Result<()> {
339        let locator = Locator::Css(format!("input[name='{}']", name));
340        let elt = self.clone().find(locator.into(), Some(eid)).await?;
341        let args = {
342            let mut a = vec![serde_json::to_value(elt)?, Value::String(value)];
343            self.fixup_elements(&mut a);
344            a
345        };
346        let js = "arguments[0].value = arguments[1]".to_string();
347        let res = self.clone().execute(js, args).await?;
348        if res.is_null() {
349            Ok(())
350        } else {
351            bail!(ErrorKind::NotW3C(res))
352        }
353    }
354
355    /// Submit the form specified by `eid` with the first submit button
356    pub async fn submit(&self, eid: WebElement) -> Result<()> {
357        let l = Locator::Css("input[type=submit],button[type=submit]".into());
358        self.submit_with(eid, l).await
359    }
360
361    /// Submit the form `eid` using the button matched by the given selector.
362    pub async fn submit_with(&self, eid: WebElement, button: Locator) -> Result<()> {
363        let elt = self.clone().find(button.into(), Some(eid)).await?;
364        Ok(self.clone().click(elt).await?)
365    }
366
367    /// Submit this form using the form submit button with the given
368    /// label (case-insensitive).
369    pub async fn submit_using(&self, eid: WebElement, button_label: String) -> Result<()> {
370        let escaped = button_label.replace('\\', "\\\\").replace('"', "\\\"");
371        let btn = format!(
372            "input[type=submit][value=\"{}\" i],\
373             button[type=submit][value=\"{}\" i]",
374            escaped, escaped
375        );
376        Ok(self.submit_with(eid, Locator::Css(btn)).await?)
377    }
378
379    /// Submit this form directly, without clicking any buttons.
380    ///
381    /// This can be useful to bypass forms that perform various magic
382    /// when the submit button is clicked, or that hijack click events
383    /// altogether.
384    ///
385    /// Note that since no button is actually clicked, the
386    /// `name=value` pair for the submit button will not be
387    /// submitted. This can be circumvented by using `submit_sneaky`
388    /// instead.
389    pub async fn submit_direct(&self, eid: WebElement) -> Result<()> {
390        // some sites are silly, and name their submit button
391        // "submit". this ends up overwriting the "submit" function of
392        // the form with a reference to the submit button itself, so
393        // we can't call .submit(). we get around this by creating a
394        // *new* form, and using *its* submit() handler but with this
395        // pointed to the real form. solution from here:
396        // https://stackoverflow.com/q/833032/472927#comment23038712_834197
397        let js = "document.createElement('form').submit.call(arguments[0])".to_string();
398        let args = {
399            let mut a = vec![serde_json::to_value(eid)?];
400            self.fixup_elements(&mut a);
401            a
402        };
403        self.clone().execute(js, args).await?;
404        Ok(())
405    }
406
407    /// Submit this form directly, without clicking any buttons, and
408    /// with an extra field.
409    ///
410    /// Like `submit_direct`, this method will submit this form
411    /// without clicking a submit button.  However, it will *also*
412    /// inject a hidden input element on the page that carries the
413    /// given `field=value` mapping. This allows you to emulate the
414    /// form data as it would have been *if* the submit button was
415    /// indeed clicked.
416    pub async fn submit_sneaky(
417        &self,
418        eid: WebElement,
419        field: String,
420        value: String,
421    ) -> Result<()> {
422        let js = r#"
423            var h = document.createElement('input');
424            h.setAttribute('type', 'hidden');
425            h.setAttribute('name', arguments[1]);
426            h.value = arguments[2];
427            arguments[0].appendChild(h);
428        "#
429        .to_string();
430        let args = {
431            let mut a = vec![
432                serde_json::to_value(eid)?,
433                Value::String(field),
434                Value::String(value),
435            ];
436            self.fixup_elements(&mut a);
437            a
438        };
439        self.execute(js, args).await?;
440        Ok(())
441    }
442}