fantoccini-stable 0.7.2

High-level API for programmatically interacting with web pages through WebDriver.
Documentation
//! A high-level API for programmatically interacting with web pages through WebDriver.
//!
//! This crate uses the [WebDriver protocol] to drive a conforming (potentially headless) browser
//! through relatively high-level operations such as "click this element", "submit this form", etc.
//! It is currently nightly-only, but this will change once
//! [`conservative_impl_trait`](https://github.com/rust-lang/rust/issues/34511) lands in stable.
//!
//! Most interactions are driven by using [CSS selectors]. With most WebDriver-compatible browser
//! being fairly recent, the more expressive levels of the CSS standard are also supported, giving
//! fairly [powerful] [operators].
//!
//! Forms are managed by first calling `Client::form`, and then using the methods on `Form` to
//! manipulate the form's fields and eventually submitting it.
//!
//! For low-level access to the page, `Client::source` can be used to fetch the full page HTML
//! source code, and `Client::raw_client_for` to build a raw HTTP request for a particular URL.
//!
//! # Examples
//!
//! These examples all assume that you have a [WebDriver compatible] process running on port 4444.
//! A quick way to get one is to run [`geckodriver`] at the command line. The code also has
//! partial support for the legacy WebDriver protocol used by `chromedriver` and `ghostdriver`.
//!
//! The examples will be using `unwrap` generously --- you should probably not do that in your
//! code, and instead deal with errors when they occur. This is particularly true for methods that
//! you *expect* might fail, such as lookups by CSS selector.
//!
//! Let's start out clicking around on Wikipedia:
//!
//! ```no_run
//! # extern crate tokio_core;
//! # extern crate futures;
//! # extern crate fantoccini;
//! # fn main() {
//! # use fantoccini::Client;
//! # use futures::future::Future;
//! let mut core = tokio_core::reactor::Core::new().unwrap();
//! let (c, fin) = Client::new("http://localhost:4444", &core.handle());
//! let c = core.run(c).unwrap();
//!
//! {
//!     // we want to have a reference to c so we can use it in the and_thens below
//!     let c = &c;
//!
//!     // now let's set up the sequence of steps we want the browser to take
//!     // first, go to the Wikipedia page for Foobar
//!     let f = c.goto("https://en.wikipedia.org/wiki/Foobar")
//!         .and_then(move |_| c.current_url())
//!         .and_then(move |url| {
//!             assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foobar");
//!             // click "Foo (disambiguation)"
//!             c.by_selector(".mw-disambig")
//!         })
//!         .and_then(|e| e.click())
//!         .and_then(move |_| {
//!             // click "Foo Lake"
//!             c.by_link_text("Foo Lake")
//!         })
//!         .and_then(|e| e.click())
//!         .and_then(move |_| c.current_url())
//!         .and_then(|url| {
//!             assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foo_Lake");
//!             Ok(())
//!         });
//!
//!     // and set the browser off to do those things
//!     core.run(f).unwrap();
//! }
//!
//! // drop the client to delete the browser session
//! drop(c);
//! // and wait for cleanup to finish
//! core.run(fin).unwrap();
//! # }
//! ```
//!
//! How did we get to the Foobar page in the first place? We did a search!
//! Let's make the program do that for us instead:
//!
//! ```no_run
//! # extern crate tokio_core;
//! # extern crate futures;
//! # extern crate fantoccini;
//! # fn main() {
//! # use fantoccini::Client;
//! # use futures::future::Future;
//! # let mut core = tokio_core::reactor::Core::new().unwrap();
//! # let (c, fin) = Client::new("http://localhost:4444", &core.handle());
//! # let c = core.run(c).unwrap();
//! # {
//! #    let c = &c;
//! #    let f =
//! // -- snip wrapper code --
//! // go to the Wikipedia frontpage this time
//! c.goto("https://www.wikipedia.org/")
//!     .and_then(move |_| {
//!         // find the search form
//!         c.form("#search-form")
//!     })
//!     .and_then(|f| {
//!         // fill it out
//!         f.set_by_name("search", "foobar")
//!     })
//!     .and_then(|f| {
//!         // and submit it
//!         f.submit()
//!     })
//!     // we should now have ended up in the rigth place
//!     .and_then(move |_| c.current_url())
//!     .and_then(|url| {
//!         assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foobar");
//!         Ok(())
//!     })
//! // -- snip wrapper code --
//! #    ;
//! #    core.run(f).unwrap();
//! # }
//! # drop(c);
//! # core.run(fin).unwrap();
//! # }
//! ```
//!
//! What if we want to download a raw file? Fantoccini has you covered:
//!
//! ```no_run
//! # extern crate tokio_core;
//! # extern crate futures;
//! # extern crate fantoccini;
//! # fn main() {
//! # use fantoccini::Client;
//! # use futures::future::Future;
//! # let mut core = tokio_core::reactor::Core::new().unwrap();
//! # let (c, fin) = Client::new("http://localhost:4444", &core.handle());
//! # let c = core.run(c).unwrap();
//! # {
//! #    let c = &c;
//! #    let f =
//! // -- snip wrapper code --
//! // go back to the frontpage
//! c.goto("https://www.wikipedia.org/")
//!     .and_then(move |_| {
//!         // find the source for the Wikipedia globe
//!         c.by_selector("img.central-featured-logo")
//!     })
//!     .and_then(|img| {
//!         img.attr("src")
//!             .map(|src| src.expect("image should have a src"))
//!     })
//!     .and_then(move |src| {
//!         // now build a raw HTTP client request (which also has all current cookies)
//!         c.raw_client_for(fantoccini::Method::Get, &src)
//!     })
//!     .and_then(|raw| {
//!         use futures::Stream;
//!         // we then read out the image bytes
//!         raw.body().map_err(fantoccini::error::CmdError::from).fold(
//!             Vec::new(),
//!             |mut pixels, chunk| {
//!                 pixels.extend(&*chunk);
//!                 futures::future::ok::<Vec<u8>, fantoccini::error::CmdError>(pixels)
//!             },
//!         )
//!     })
//!     .and_then(|pixels| {
//!         // and voilla, we now have the bytes for the Wikipedia logo!
//!         assert!(pixels.len() > 0);
//!         println!("Wikipedia logo is {}b", pixels.len());
//!         Ok(())
//!     })
//! // -- snip wrapper code --
//! #    ;
//! #    core.run(f).unwrap();
//! # }
//! # drop(c);
//! # core.run(fin).unwrap();
//! # }
//! ```
//!
//! [WebDriver protocol]: https://www.w3.org/TR/webdriver/
//! [CSS selectors]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors
//! [powerful]: https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
//! [operators]: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
//! [WebDriver compatible]: https://github.com/Fyrd/caniuse/issues/2757#issuecomment-304529217
//! [`geckodriver`]: https://github.com/mozilla/geckodriver
#![deny(missing_docs)]

extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate rustc_serialize;
extern crate tokio_core;
extern crate url;
extern crate webdriver;

use webdriver::command::WebDriverCommand;
use webdriver::error::WebDriverError;
use webdriver::error::ErrorStatus;
use webdriver::common::ELEMENT_KEY;
use rustc_serialize::json::Json;
use futures::{future, Future, Stream};
use std::cell::RefCell;
use std::rc::Rc;

pub use hyper::Method;

/// Error types.
pub mod error;

type Cmd = WebDriverCommand<webdriver::command::VoidWebDriverExtensionCommand>;

/// State held by a `Client`
struct Inner {
    c: hyper::Client<hyper_tls::HttpsConnector<hyper::client::HttpConnector>, hyper::Body>,
    handle: tokio_core::reactor::Handle,
    wdb: url::Url,
    session: RefCell<Option<String>>,
    legacy: bool,
    ua: RefCell<Option<String>>,
    tx: futures::unsync::mpsc::Sender<()>,
}

impl Inner {
    fn shutdown(&mut self) -> bool {
        if self.session.borrow().is_some() {
            let url = {
                let s = self.session.borrow();
                self.wdb
                    .join(&format!("/session/{}", s.as_ref().unwrap()))
                    .unwrap()
            };
            *self.session.borrow_mut() = None;

            // TODO: ensure that there are no other outstanding futures
            // keep a copy of tx so the "final" future is not yet resolved
            let tx = self.tx.clone();
            let f = self.c
                .request(hyper::client::Request::new(
                    Method::Delete,
                    url.as_ref().parse().unwrap(),
                ))
                .then(move |_| {
                    drop(tx);
                    Ok(())
                });
            self.handle.spawn(f);
            true
        } else {
            false
        }
    }
}

impl Drop for Inner {
    fn drop(&mut self) {
        // NOTE: we must implement Drop for Inner, *not* for Client, since Client is dropped often
        self.shutdown();
    }
}

/// A WebDriver client tied to a single browser session.
pub struct Client(Rc<Inner>);

/// A single element on the current page.
pub struct Element {
    c: Client,
    e: webdriver::common::WebElement,
}

/// An HTML form on the current page.
pub struct Form {
    c: Client,
    f: webdriver::common::WebElement,
}

impl Client {
    fn init(
        mut self,
        params: webdriver::command::NewSessionParameters,
    ) -> Box<Future<Item = Self, Error = error::NewSessionError>> {
        if let webdriver::command::NewSessionParameters::Legacy(..) = params {
            Rc::get_mut(&mut self.0)
                .expect(
                    "during legacy init there should be only one Client instance",
                )
                .legacy = true;
        }

        // Create a new session for this client
        // https://www.w3.org/TR/webdriver/#dfn-new-session
        Box::new(self.issue_wd_cmd(WebDriverCommand::NewSession(params))
            .then(move |r| match r {
                Ok((this, Json::Object(mut v))) => {
                    // TODO: not all impls are w3c compatible
                    // See https://github.com/SeleniumHQ/selenium/blob/242d64ca4cd3523489ac1e58703fd7acd4f10c5a/py/selenium/webdriver/remote/webdriver.py#L189
                    // and https://github.com/SeleniumHQ/selenium/blob/242d64ca4cd3523489ac1e58703fd7acd4f10c5a/py/selenium/webdriver/remote/webdriver.py#L200
                    if let Some(session_id) = v.remove("sessionId") {
                        if let Some(session_id) = session_id.as_string() {
                            *this.0.session.borrow_mut() = Some(session_id.to_string());
                            return Ok(this);
                        }
                        v.insert("sessionId".to_string(), session_id);
                        Err(error::NewSessionError::NotW3C(Json::Object(v)))
                    } else {
                        Err(error::NewSessionError::NotW3C(Json::Object(v)))
                    }
                }
                Ok((_, v)) | Err(error::CmdError::NotW3C(v)) => {
                    Err(error::NewSessionError::NotW3C(v))
                },
                Err(error::CmdError::Failed(e)) => Err(error::NewSessionError::Failed(e)),
                Err(error::CmdError::Lost(e)) => Err(error::NewSessionError::Lost(e)),
                Err(error::CmdError::NotJson(v)) => {
                    Err(error::NewSessionError::NotW3C(Json::String(v)))
                }
                Err(
                    error::CmdError::Standard(
                        e @ WebDriverError { error: ErrorStatus::SessionNotCreated, .. },
                    ),
                ) => Err(error::NewSessionError::SessionNotCreated(e)),
                Err(e) => {
                    panic!("unexpected webdriver error; {}", e);
                }
            }))
    }

    /// Create a new `Client` associated with a new WebDriver session on the server at the given
    /// URL.
    ///
    /// Returns two futures -- one that resolves to a handle for issuing additional WebDriver
    /// tasks, and one that resolves once all tasks have been completed and the WebDriver session
    /// has been destroyed. Note that it is important to eventually wait for this future, as
    /// otherwise the WebDriver browser session may not be closed. For some drivers, such as
    /// geckodriver, this is particularly important, as multiple simulatenous sessions are not
    /// supported.
    ///
    /// Note that the second future will *not* resolve until the `Client` has been dropped.
    #[cfg_attr(feature = "cargo-clippy", allow(new_ret_no_self))]
    pub fn new(
        webdriver: &str,
        handle: &tokio_core::reactor::Handle,
    ) -> (
        Box<Future<Item = Self, Error = error::NewSessionError>>,
        Box<Future<Item = (), Error = ()>>,
    ) {
        // Where is the WebDriver server?
        let wdb = match webdriver.parse::<url::Url>() {
            Ok(wdb) => wdb,
            Err(e) => {
                return (
                    Box::new(future::err(error::NewSessionError::BadWebdriverUrl(e))),
                    Box::new(future::err(())),
                );
            }
        };

        // We want a tls-enabled client
        let client = hyper::Client::configure()
            .connector(hyper_tls::HttpsConnector::new(4, handle).unwrap())
            .build(handle);

        // Keep a channel for tracking when all outstanding references to the client has gone away
        // (i.e., when all futures have resolved, no more commands can be issued, and the session
        // has been closed assuming the user remembered to call end()).
        let (tx, rx) = futures::unsync::mpsc::channel(0);

        // Set up our WebDriver client
        let c = Client(Rc::new(Inner {
            c: client.clone(),
            handle: handle.clone(),
            wdb: wdb.clone(),
            session: RefCell::new(None),
            legacy: false,
            ua: RefCell::new(None),
            tx: tx,
        }));

        // Required capabilities
        // https://www.w3.org/TR/webdriver/#capabilities
        let mut cap = webdriver::capabilities::Capabilities::new();
        //  - we want the browser to wait for the page to load
        cap.insert(
            "pageLoadStrategy".to_string(),
            Json::String("normal".to_string()),
        );

        let session_config = webdriver::capabilities::SpecNewSessionParameters {
            alwaysMatch: cap.clone(),
            firstMatch: vec![],
        };
        let spec = webdriver::command::NewSessionParameters::Spec(session_config);

        let f = c.dup().init(spec).or_else(move |e| {
            match e {
                error::NewSessionError::NotW3C(json) => {
                    let mut legacy = false;
                    match json {
                        Json::String(ref err) if err.starts_with("Missing Command Parameter") => {
                            // ghostdriver
                            legacy = true;
                        }
                        Json::Object(ref err) => {
                            if err.contains_key("message") &&
                                err["message"]
                                    .as_string()
                                    .map(|s| {
                                        // chromedriver < 2.29 || chromedriver == 2.29
                                        s.contains("cannot find dict 'desiredCapabilities'") ||
                                            s.contains("Missing or invalid capabilities")
                                    })
                                    .unwrap_or(false)
                            {
                                legacy = true;
                            }
                        }
                        _ => {}
                    }

                    if legacy {
                        // we're dealing with an implementation that only supports the legacy
                        // WebDriver protocol:
                        // https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
                        let session_config = webdriver::capabilities::LegacyNewSessionParameters {
                            required: cap,
                            desired: webdriver::capabilities::Capabilities::new(),
                        };
                        let spec = webdriver::command::NewSessionParameters::Legacy(session_config);

                        // try a new client
                        future::Either::A(c.init(spec))
                    } else {
                        future::Either::B(future::err(error::NewSessionError::NotW3C(json)))
                    }
                }
                e => future::Either::B(future::err(e.into())),
            }
        });
        (
            Box::new(Box::new(f) as Box<Future<Item = _, Error = _>>),
            Box::new(rx.into_future().then(|_| Ok(()))),
        )
    }

    fn dup(&self) -> Self {
        Client(self.0.clone())
    }

    /// Set the User Agent string to use for all subsequent requests.
    pub fn set_ua<S: Into<String>>(&mut self, ua: S) {
        *self.0.ua.borrow_mut() = Some(ua.into());
    }

    /// Helper for determining what URL endpoint to use for various requests.
    ///
    /// This mapping is essentially that of https://www.w3.org/TR/webdriver/#list-of-endpoints.
    fn endpoint_for(&self, cmd: &Cmd) -> Result<url::Url, url::ParseError> {
        if let WebDriverCommand::NewSession(..) = *cmd {
            return self.0.wdb.join("/session");
        }

        let base = {
            let session = self.0.session.borrow();
            self.0
                .wdb
                .join(&format!("/session/{}/", session.as_ref().unwrap()))?
        };
        match *cmd {
            WebDriverCommand::NewSession(..) => unreachable!(),
            WebDriverCommand::DeleteSession => unreachable!(),
            WebDriverCommand::Get(..) | WebDriverCommand::GetCurrentUrl => base.join("url"),
            WebDriverCommand::GoBack => base.join("back"),
            WebDriverCommand::Refresh => base.join("refresh"),
            WebDriverCommand::GetPageSource => base.join("source"),
            WebDriverCommand::FindElement(..) => base.join("element"),
            WebDriverCommand::GetCookies => base.join("cookie"),
            WebDriverCommand::ExecuteScript(..) if self.0.legacy => base.join("execute"),
            WebDriverCommand::ExecuteScript(..) => base.join("execute/sync"),
            WebDriverCommand::GetElementProperty(ref we, ref prop) => {
                base.join(&format!("element/{}/property/{}", we.id, prop))
            }
            WebDriverCommand::GetElementAttribute(ref we, ref attr) => {
                base.join(&format!("element/{}/attribute/{}", we.id, attr))
            }
            WebDriverCommand::FindElementElement(ref p, _) => {
                base.join(&format!("element/{}/element", p.id))
            }
            WebDriverCommand::ElementClick(ref we) => {
                base.join(&format!("element/{}/click", we.id))
            }
            WebDriverCommand::GetElementText(ref we) => {
                base.join(&format!("element/{}/text", we.id))
            }
            WebDriverCommand::ElementSendKeys(ref we, _) => {
                base.join(&format!("element/{}/value", we.id))
            }
            _ => unimplemented!(),
        }
    }

    /// Helper for issuing a WebDriver command, and then reading and parsing the response.
    ///
    /// Since most `WebDriverCommand` arguments already implement `ToJson`, this is mostly a matter
    /// of picking the right URL and method from [the spec], and stuffing the JSON encoded
    /// arguments (if any) into the body.
    ///
    /// [the spec]: https://www.w3.org/TR/webdriver/#list-of-endpoints
    fn issue_wd_cmd(
        self,
        cmd: WebDriverCommand<webdriver::command::VoidWebDriverExtensionCommand>,
    ) -> Box<Future<Item = (Self, Json), Error = error::CmdError>> {
        use rustc_serialize::json::ToJson;
        use webdriver::command;

        // most actions are just get requests with not parameters
        let url = match self.endpoint_for(&cmd) {
            Ok(url) => url,
            Err(e) => return Box::new(future::err(error::CmdError::from(e))),
        };
        let mut method = Method::Get;
        let mut body = None;

        // but some are special
        match cmd {
            WebDriverCommand::NewSession(command::NewSessionParameters::Spec(ref conf)) => {
                body = Some(format!("{}", conf.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::NewSession(command::NewSessionParameters::Legacy(ref conf)) => {
                body = Some(format!("{}", conf.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::Get(ref params) => {
                body = Some(format!("{}", params.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::FindElement(ref loc) |
            WebDriverCommand::FindElementElement(_, ref loc) => {
                body = Some(format!("{}", loc.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::ExecuteScript(ref script) => {
                body = Some(format!("{}", script.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::ElementSendKeys(_, ref keys) => {
                body = Some(format!("{}", keys.to_json()));
                method = Method::Post;
            }
            WebDriverCommand::ElementClick(..) => {
                body = Some("{}".to_string());
                method = Method::Post;
            }
            WebDriverCommand::GoBack | WebDriverCommand::Refresh => {
                method = Method::Post;
            }
            _ => {}
        }

        // issue the command to the webdriver server
        let mut req = hyper::client::Request::new(method, url.as_ref().parse().unwrap());
        if let Some(ref s) = *self.0.ua.borrow() {
            req.headers_mut()
                .set(hyper::header::UserAgent::new(s.to_owned()));
        }
        if let Some(ref body) = body {
            req.headers_mut().set(hyper::header::ContentType::json());
            req.headers_mut()
                .set(hyper::header::ContentLength(body.len() as u64));
            req.set_body(body.clone());
        }

        let req = self.0.c.request(req);
        let f = req.map_err(error::CmdError::from).and_then(move |res| {
            // keep track of result status (.body() consumes self -- ugh)
            let status = res.status();

            // check that the server sent us json
            let ctype = {
                let ctype = res.headers()
                    .get::<hyper::header::ContentType>()
                    .expect("webdriver response did not have a content type");
                (**ctype).clone()
            };

            // What did the server send us?
            res.body()
                .concat2()
                .map(move |body| {
                    (self, body, ctype, status)
                })
                .map_err(|e| -> error::CmdError { e.into() })
        }).and_then(|(this, body, ctype, status)| {
            // Too bad we can't stream into a String :(
            let body = String::from_utf8(body.to_vec()).expect("non utf-8 response from webdriver");
            if ctype.type_() == hyper::mime::APPLICATION && ctype.subtype() == hyper::mime::JSON {
                Ok((this, body, status))
            } else {
                // nope, something else...
                Err(error::CmdError::NotJson(body))
            }
        }).and_then(move |(this, body, status)| {
            let is_new_session = if let WebDriverCommand::NewSession(..) = cmd {
                true
            } else {
                false
            };

            let mut is_success = status.is_success();
            let mut legacy_status = 0;

            // https://www.w3.org/TR/webdriver/#dfn-send-a-response
            // NOTE: the standard specifies that even errors use the "Send a Reponse" steps
            let body = match Json::from_str(&*body)? {
                Json::Object(mut v) => {
                    if this.0.legacy {
                        legacy_status = v["status"].as_u64().unwrap();
                        is_success = legacy_status == 0;
                    }

                    if this.0.legacy && is_new_session {
                        // legacy implementations do not wrap sessionId inside "value"
                        Ok(Json::Object(v))
                    } else {
                        v.remove("value")
                            .ok_or_else(|| error::CmdError::NotW3C(Json::Object(v)))
                    }
                }
                v => Err(error::CmdError::NotW3C(v)),
            }?;

            if is_success {
                return Ok((this, body));
            }

            // https://www.w3.org/TR/webdriver/#dfn-send-an-error
            // https://www.w3.org/TR/webdriver/#handling-errors
            if !body.is_object() {
                return Err(error::CmdError::NotW3C(body));
            }
            let mut body = body.into_object().unwrap();

            // phantomjs injects a *huge* field with the entire screen contents -- remove that
            body.remove("screen");

            let es = if this.0.legacy {
                // old clients use status codes instead of "error", and we now have to map them
                // https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#response-status-codes
                if !body.contains_key("message") || !body["message"].is_string() {
                    return Err(error::CmdError::NotW3C(Json::Object(body)));
                }
                match legacy_status {
                    6 | 33 => ErrorStatus::SessionNotCreated,
                    7 => ErrorStatus::NoSuchElement,
                    8 => ErrorStatus::NoSuchFrame,
                    9 => ErrorStatus::UnknownCommand,
                    10 => ErrorStatus::StaleElementReference,
                    11 => ErrorStatus::ElementNotInteractable,
                    12 => ErrorStatus::InvalidElementState,
                    13 => ErrorStatus::UnknownError,
                    15 => ErrorStatus::ElementNotSelectable,
                    17 => ErrorStatus::JavascriptError,
                    19 | 32 => ErrorStatus::InvalidSelector,
                    21 => ErrorStatus::Timeout,
                    23 => ErrorStatus::NoSuchWindow,
                    24 => ErrorStatus::InvalidCookieDomain,
                    25 => ErrorStatus::UnableToSetCookie,
                    26 => ErrorStatus::UnexpectedAlertOpen,
                    27 => ErrorStatus::NoSuchAlert,
                    28 => ErrorStatus::ScriptTimeout,
                    29 => ErrorStatus::InvalidCoordinates,
                    34 => ErrorStatus::MoveTargetOutOfBounds,
                    _ => return Err(error::CmdError::NotW3C(Json::Object(body))),
                }
            } else {
                if !body.contains_key("error") || !body.contains_key("message") ||
                    !body["error"].is_string() || !body["message"].is_string()
                {
                    return Err(error::CmdError::NotW3C(Json::Object(body)));
                }

                use hyper::StatusCode;
                let error = body["error"].as_string().unwrap();
                match status {
                    StatusCode::BadRequest => {
                        match error {
                            "element click intercepted" => ErrorStatus::ElementClickIntercepted,
                            "element not selectable" => ErrorStatus::ElementNotSelectable,
                            "element not interactable" => ErrorStatus::ElementNotInteractable,
                            "insecure certificate" => ErrorStatus::InsecureCertificate,
                            "invalid argument" => ErrorStatus::InvalidArgument,
                            "invalid cookie domain" => ErrorStatus::InvalidCookieDomain,
                            "invalid coordinates" => ErrorStatus::InvalidCoordinates,
                            "invalid element state" => ErrorStatus::InvalidElementState,
                            "invalid selector" => ErrorStatus::InvalidSelector,
                            "no such alert" => ErrorStatus::NoSuchAlert,
                            "no such frame" => ErrorStatus::NoSuchFrame,
                            "no such window" => ErrorStatus::NoSuchWindow,
                            "stale element reference" => ErrorStatus::StaleElementReference,
                            _ => unreachable!(),
                        }
                    }
                    StatusCode::NotFound => {
                        match error {
                            "unknown command" => ErrorStatus::UnknownCommand,
                            "no such cookie" => ErrorStatus::NoSuchCookie,
                            "invalid session id" => ErrorStatus::InvalidSessionId,
                            "no such element" => ErrorStatus::NoSuchElement,
                            _ => unreachable!(),
                        }
                    }
                    StatusCode::InternalServerError => {
                        match error {
                            "javascript error" => ErrorStatus::JavascriptError,
                            "move target out of bounds" => ErrorStatus::MoveTargetOutOfBounds,
                            "session not created" => ErrorStatus::SessionNotCreated,
                            "unable to set cookie" => ErrorStatus::UnableToSetCookie,
                            "unable to capture screen" => ErrorStatus::UnableToCaptureScreen,
                            "unexpected alert open" => ErrorStatus::UnexpectedAlertOpen,
                            "unknown error" => ErrorStatus::UnknownError,
                            "unsupported operation" => ErrorStatus::UnsupportedOperation,
                            _ => unreachable!(),
                        }
                    }
                    StatusCode::RequestTimeout => {
                        match error {
                            "timeout" => ErrorStatus::Timeout,
                            "script timeout" => ErrorStatus::ScriptTimeout,
                            _ => unreachable!(),
                        }
                    }
                    StatusCode::MethodNotAllowed => {
                        match error {
                            "unknown method" => ErrorStatus::UnknownMethod,
                            _ => unreachable!(),
                        }
                    }
                    _ => unreachable!(),
                }
            };

            let message = body["message"].as_string().unwrap().to_string();
            Err(error::CmdError::from(WebDriverError::new(es, message)))
        });

        Box::new(f)
    }

    /// Terminate the connection to the webservice.
    ///
    /// This function may be useful in conjunction with `raw_client_for`, as it allows you to close
    /// the automated browser window while doing e.g., a large download.
    ///
    /// Note that this call only takes effect if there are no other copies of this `Client`. This
    /// function is safe to call multiple times. Returns true if the shutdown took effect.
    /// Returns true if the shutd
    pub fn shutdown(&mut self) -> bool {
        Rc::get_mut(&mut self.0)
            .map(|i| i.shutdown())
            .unwrap_or(false)
    }

    /// Navigate directly to the given URL.
    pub fn goto(&self, url: &str) -> Box<Future<Item = Self, Error = error::CmdError>> {
        let url = url.to_owned();
        Box::new(
            self.current_url_()
                .and_then(move |(this, base)| Ok((this, base.join(&url)?)))
                .and_then(move |(this, url)| {
                    this.issue_wd_cmd(WebDriverCommand::Get(webdriver::command::GetParameters {
                        url: url.into_string(),
                    }))
                })
                .map(|(this, _)| this),
        ) as Box<Future<Item = _, Error = _>>
    }

    fn current_url_(
        &self,
    ) -> Box<Future<Item = (Self, url::Url), Error = error::CmdError>> {
        Box::new(self.dup()
            .issue_wd_cmd(WebDriverCommand::GetCurrentUrl)
            .and_then(|(this, url)| {
                if let Some(url) = url.as_string() {
                    return Ok((this, url.parse()?));
                }

                Err(error::CmdError::NotW3C(url))
            }))
    }

    /// Retrieve the currently active URL for this session.
    pub fn current_url(&self) -> Box<Future<Item = url::Url, Error = error::CmdError>> {
        Box::new(self.current_url_().map(|(_, u)| u))
    }

    /// Get the HTML source for the current page.
    pub fn source(&self) -> Box<Future<Item = String, Error = error::CmdError>> {
        Box::new(self.dup()
            .issue_wd_cmd(WebDriverCommand::GetPageSource)
            .and_then(|(_, src)| {
                if let Some(src) = src.as_string() {
                    return Ok(src.to_string());
                }

                Err(error::CmdError::NotW3C(src))
            }))
    }

    /// Go back to the previous page.
    pub fn back(&self) -> Box<Future<Item = Self, Error = error::CmdError>> {
        Box::new(
            self.dup()
                .issue_wd_cmd(WebDriverCommand::GoBack)
                .map(|(this, _)| this),
        ) as Box<Future<Item = _, Error = _>>
    }

    /// Refresh the current previous page.
    pub fn refresh(&self) -> Box<Future<Item = Self, Error = error::CmdError>> {
        Box::new(
            self.dup()
                .issue_wd_cmd(WebDriverCommand::Refresh)
                .map(|(this, _)| this),
        ) as Box<Future<Item = _, Error = _>>
    }

    /// Execute the given JavaScript `script` in the current browser session.
    ///
    /// `args` is available to the script inside the `arguments` array. Since `Element` implements
    /// `ToJson`, you can also provide serialized `Element`s as arguments, and they will correctly
    /// serialize to DOM elements on the other side.
    pub fn execute(
        &self,
        script: &str,
        mut args: Vec<Json>,
    ) -> Box<Future<Item = Json, Error = error::CmdError>> {
        self.fixup_elements(&mut args);
        let cmd = webdriver::command::JavascriptCommandParameters {
            script: script.to_string(),
            args: webdriver::common::Nullable::Value(args),
        };

        Box::new(self.dup()
            .issue_wd_cmd(WebDriverCommand::ExecuteScript(cmd))
            .map(|(_, v)| v))
    }

    /// Issue an HTTP request to the given `url` with all the same cookies as the current session.
    ///
    /// Calling this method is equivalent to calling `with_raw_client_for` with an empty closure.
    pub fn raw_client_for(
        &self,
        method: Method,
        url: &str,
    ) -> Box<Future<Item = hyper::Response, Error = error::CmdError>> {
        self.with_raw_client_for(method, url, |_| {})
    }

    /// Build and issue an HTTP request to the given `url` with all the same cookies as the current
    /// session.
    ///
    /// Before the HTTP request is issued, the given `before` closure will be called with a handle
    /// to the `Request` about to be sent.
    pub fn with_raw_client_for<F>(
        &self,
        method: Method,
        url: &str,
        before: F,
    ) -> Box<Future<Item = hyper::Response, Error = error::CmdError>>
    where
        F: FnOnce(&mut hyper::Request) + 'static,
    {
        let url = url.to_owned();
        // We need to do some trickiness here. GetCookies will only give us the cookies for the
        // *current* domain, whereas we want the cookies for `url`'s domain. So, we navigate to the
        // URL in question, fetch its cookies, and then navigate back. *Except* that we can't do
        // that either (what if `url` is some huge file?). So we *actually* navigate to some weird
        // url that's unlikely to exist on the target doamin, and which won't resolve into the
        // actual content, but will still give the same cookies.
        //
        // The fact that cookies can have /path and security constraints makes this even more of a
        // pain. /path in particular is tricky, because you could have a URL like:
        //
        //    example.com/download/some_identifier/ignored_filename_just_for_show
        //
        // Imagine if a cookie is set with path=/download/some_identifier. How do we get that
        // cookie without triggering a request for the (large) file? I don't know. Hence: TODO.
        let res = self.current_url_()
            .and_then(move |(this, old_url)| {
                old_url
                    .clone()
                    .join(&url)
                    .map(move |url| (this, url))
                    .map_err(|e| e.into())
            })
            .and_then(|(this, url)| {
                url.clone()
                    .join("/please_give_me_your_cookies")
                    .map(move |cookie_url| (this, url, cookie_url))
                    .map_err(|e| e.into())
            })
            .and_then(|(this, url, cookie_url)| {
                this.goto(cookie_url.as_str()).map(|this| (this, url))
            })
            .and_then(|(this, url)| {
                this.issue_wd_cmd(WebDriverCommand::GetCookies)
                    .then(|cookies| {
                        match cookies {
                            Ok((this, cookies)) => if cookies.is_array() {
                                future::ok((this, url, cookies))
                            } else {
                                future::err(error::CmdError::NotW3C(cookies))
                            },
                            Err(e) => {
                                // TODO: go back before we return
                                // can't get a handle to this here though :(
                                //future::Either::B(
                                //    this.goto(&format!("{}", old_url))
                                //        .and_then(move |_| future::err(e)),
                                //)
                                future::err(e)
                            }
                        }
                    })
            })
            .and_then(|(this, url, cookies)| {
                this.back().map(|this| (this, url, cookies))
            })
            .and_then(|(this, url, cookies)| {
                let cookies = cookies.into_array().unwrap();

                // now add all the cookies
                let mut all_ok = true;
                let mut jar = hyper::header::Cookie::new();
                for cookie in &cookies {
                    if !cookie.is_object() {
                        all_ok = false;
                        break;
                    }

                    // https://w3c.github.io/webdriver/webdriver-spec.html#cookies
                    let cookie = cookie.as_object().unwrap();
                    if !cookie.contains_key("name") || !cookie.contains_key("value") {
                        all_ok = false;
                        break;
                    }

                    if !cookie["name"].is_string() || !cookie["value"].is_string() {
                        all_ok = false;
                        break;
                    }

                    // Note that since we're sending these cookies, all that matters is the mapping
                    // from name to value. The other fields only matter when deciding whether to
                    // include a cookie or not, and the driver has already decided that for us
                    // (GetCookies is for a particular URL).
                    jar.append(
                        cookie["name"].as_string().unwrap().to_owned(),
                        cookie["value"].as_string().unwrap().to_owned(),
                    );
                }

                if all_ok {
                    let mut req =
                        hyper::client::Request::new(method, url.as_ref().parse().unwrap());
                    req.headers_mut().set(jar);
                    if let Some(ref s) = *this.0.ua.borrow() {
                        req.headers_mut()
                            .set(hyper::header::UserAgent::new(s.to_owned()));
                    }
                    before(&mut req);
                    future::Either::A(this.0.c.request(req).map_err(|e| e.into()))
                } else {
                    future::Either::B(future::err(error::CmdError::NotW3C(Json::Array(cookies))))
                }
            });
        Box::new(res)
    }

    /// Find an element by CSS selector.
    pub fn by_selector(
        &self,
        selector: &str,
    ) -> Box<Future<Item = Element, Error = error::CmdError>> {
        let locator = Self::mklocator(selector);
        self.by(locator)
    }

    /// Find an element by its link text.
    ///
    /// The text matching is exact.
    pub fn by_link_text(
        &self,
        text: &str,
    ) -> Box<Future<Item = Element, Error = error::CmdError>> {
        let locator = webdriver::command::LocatorParameters {
            using: webdriver::common::LocatorStrategy::LinkText,
            value: text.to_string(),
        };
        self.by(locator)
    }

    /// Find an element using an XPath expression.
    pub fn by_xpath(
        &self,
        xpath: &str,
    ) -> Box<Future<Item = Element, Error = error::CmdError>> {
        let locator = webdriver::command::LocatorParameters {
            using: webdriver::common::LocatorStrategy::XPath,
            value: xpath.to_string(),
        };
        self.by(locator)
    }

    /// Wait for the given function to return `true` before proceeding.
    ///
    /// This can be useful to wait for something to appear on the page before interacting with it.
    /// While this currently just spins and yields, it may be more efficient than this in the
    /// future. In particular, in time, it may only run `is_ready` again when an event occurs on
    /// the page.
    pub fn wait_for<F>(&mut self, mut is_ready: F) -> &mut Self
    where
        F: FnMut(Client) -> bool,
    {
        while !is_ready(self.dup()) {
            use std::thread;
            thread::yield_now();
        }
        self
    }

    /// Wait for the page to navigate to a new URL before proceeding.
    ///
    /// If the `current` URL is not provided, `self.current_url()` will be used. Note however that
    /// this introduces a race condition: the browser could finish navigating *before* we call
    /// `current_url()`, which would lead to an eternal wait.
    pub fn wait_for_navigation(
        self,
        current: Option<url::Url>,
    ) -> Box<Future<Item = Self, Error = error::CmdError>> {
        Box::new(match current {
            Some(current) => future::Either::A(future::ok((self, current))),
            None => future::Either::B(self.current_url_()),
        }.and_then(|(mut this, current)| {
            let mut err = None;

            this.wait_for(|c| match c.current_url().wait() {
                Err(e) => {
                    err = Some(e);
                    true
                }
                Ok(ref url) if url == &current => false,
                Ok(_) => true,
            });

            if let Some(e) = err {
                Err(e)
            } else {
                Ok(this)
            }
        }))
    }

    /// Locate a form on the page.
    ///
    /// Through the returned `Form`, HTML forms can be filled out and submitted.
    pub fn form(
        &self,
        selector: &str,
    ) -> Box<Future<Item = Form, Error = error::CmdError>> {
        let locator = Self::mklocator(selector);
        Box::new(
            self.dup()
                .issue_wd_cmd(WebDriverCommand::FindElement(locator))
                .map_err(|e| e.into())
                .and_then(|(this, res)| {
                    let f = this.parse_lookup(res);
                    f.map(move |f| Form { c: this, f: f })
                }),
        ) as Box<Future<Item = _, Error = _>>
    }

    // helpers

    fn by(
        &self,
        locator: webdriver::command::LocatorParameters,
    ) -> Box<Future<Item = Element, Error = error::CmdError>> {
        Box::new(
            self.dup()
                .issue_wd_cmd(WebDriverCommand::FindElement(locator))
                .map_err(|e| e.into())
                .and_then(|(this, res)| {
                    let e = this.parse_lookup(res);
                    e.map(move |e| Element { c: this, e: e })
                }),
        ) as Box<Future<Item = _, Error = _>>
    }

    /// Extract the `WebElement` from a `FindElement` or `FindElementElement` command.
    fn parse_lookup(&self, res: Json) -> Result<webdriver::common::WebElement, error::CmdError> {
        if !res.is_object() {
            return Err(error::CmdError::NotW3C(res));
        }

        // legacy protocol uses "ELEMENT" as identifier
        let key = if self.0.legacy {
            "ELEMENT"
        } else {
            ELEMENT_KEY
        };

        let mut res = res.into_object().unwrap();
        if !res.contains_key(key) {
            return Err(error::CmdError::NotW3C(Json::Object(res)));
        }

        match res.remove(key) {
            Some(Json::String(wei)) => {
                return Ok(webdriver::common::WebElement::new(wei));
            }
            Some(v) => {
                res.insert(key.to_string(), v);
            }
            None => {}
        }

        Err(error::CmdError::NotW3C(Json::Object(res)))
    }

    fn fixup_elements(&self, args: &mut [Json]) {
        if self.0.legacy {
            for arg in args {
                // the serialization of WebElement uses the W3C index,
                // but legacy implementations need us to use the "ELEMENT" index
                if let Json::Object(ref mut o) = *arg {
                    if let Some(wei) = o.remove(ELEMENT_KEY) {
                        o.insert("ELEMENT".to_string(), wei);
                    }
                }
            }
        }
    }

    /// Make a WebDriver locator for the given CSS selector.
    ///
    /// See https://www.w3.org/TR/webdriver/#element-retrieval.
    fn mklocator(selector: &str) -> webdriver::command::LocatorParameters {
        webdriver::command::LocatorParameters {
            using: webdriver::common::LocatorStrategy::CSSSelector,
            value: selector.to_string(),
        }
    }
}

impl Element {
    /// Look up an [attribute] value for this element by name.
    ///
    /// `Ok(None)` is returned if the element does not have the given attribute.
    ///
    /// [attribute]: https://dom.spec.whatwg.org/#concept-attribute
    pub fn attr(
        self,
        attribute: &str,
    ) -> Box<Future<Item = Option<String>, Error = error::CmdError>> {
        let cmd = WebDriverCommand::GetElementAttribute(self.e.clone(), attribute.to_string());
        Box::new(self.c.issue_wd_cmd(cmd).and_then(|(_, v)| match v {
            Json::String(v) => Ok(Some(v)),
            Json::Null => Ok(None),
            v => Err(error::CmdError::NotW3C(v)),
        }))
    }

    /// Look up a DOM [property] for this element by name.
    ///
    /// `Ok(None)` is returned if the element does not have the given property.
    ///
    /// [property]: https://www.ecma-international.org/ecma-262/5.1/#sec-8.12.1
    pub fn prop(
        self,
        prop: &str,
    ) -> Box<Future<Item = Option<String>, Error = error::CmdError>> {
        let cmd = WebDriverCommand::GetElementProperty(self.e.clone(), prop.to_string());
        Box::new(self.c.issue_wd_cmd(cmd).and_then(|(_, v)| match v {
            Json::String(v) => Ok(Some(v)),
            Json::Null => Ok(None),
            v => Err(error::CmdError::NotW3C(v)),
        }))
    }

    /// Retrieve the text contents of this elment.
    pub fn text(self) -> Box<Future<Item = String, Error = error::CmdError>> {
        let cmd = WebDriverCommand::GetElementText(self.e.clone());
        Box::new(self.c.issue_wd_cmd(cmd).and_then(|(_, v)| match v {
            Json::String(v) => Ok(v),
            v => Err(error::CmdError::NotW3C(v)),
        }))
    }

    /// Retrieve the HTML contents of this element.
    ///
    /// `inner` dictates whether the wrapping node's HTML is excluded or not. For example, take the
    /// HTML:
    ///
    /// ```html
    /// <div id="foo"><hr /></div>
    /// ```
    ///
    /// With `inner = true`, `<hr />` would be returned. With `inner = false`,
    /// `<div id="foo"><hr /></div>` would be returned instead.
    pub fn html(
        self,
        inner: bool,
    ) -> Box<Future<Item = String, Error = error::CmdError>> {
        let prop = if inner { "innerHTML" } else { "outerHTML" };
        Box::new(self.prop(prop).map(|v| v.unwrap()))
    }

    /// Simulate the user clicking on this element.
    ///
    /// Note that since this *may* result in navigation, we give up the handle to the element.
    pub fn click(self) -> Box<Future<Item = Client, Error = error::CmdError>> {
        let cmd = WebDriverCommand::ElementClick(self.e);
        Box::new(self.c.issue_wd_cmd(cmd).and_then(move |(c, r)| {
            if r.is_null() || r.as_object().map(|o| o.is_empty()).unwrap_or(false) {
                // geckodriver returns {} :(
                Ok(c)
            } else {
                Err(error::CmdError::NotW3C(r))
            }
        }))
    }

    /// Follow the `href` target of the element matching the given CSS selector *without* causing a
    /// click interaction.
    ///
    /// Note that since this *may* result in navigation, we give up the handle to the element.
    pub fn follow(self) -> Box<Future<Item = Client, Error = error::CmdError>> {
        let cmd = WebDriverCommand::GetElementAttribute(self.e, "href".to_string());
        Box::new(
            self.c
                .issue_wd_cmd(cmd)
                .and_then(|(this, href)| match href {
                    Json::String(v) => Ok((this, v)),
                    Json::Null => {
                        let e = WebDriverError::new(
                            webdriver::error::ErrorStatus::InvalidArgument,
                            "cannot follow element without href attribute",
                        );
                        Err(error::CmdError::Standard(e))
                    }
                    v => Err(error::CmdError::NotW3C(v)),
                })
                .and_then(|(this, href)| {
                    this.current_url_()
                        .and_then(move |(this, url)| Ok((this, url.join(&href)?)))
                })
                .and_then(|(this, href)| this.goto(href.as_str()).map(|this| this)),
        ) as Box<Future<Item = _, Error = _>>
    }
}

impl rustc_serialize::json::ToJson for Element {
    fn to_json(&self) -> Json {
        self.e.to_json()
    }
}


impl Form {
    /// Set the `value` of the given `field` in this form.
    pub fn set_by_name<'s>(
        &self,
        field: &str,
        value: &'s str,
    ) -> Box<Future<Item = Self, Error = error::CmdError>> {
        let locator = Client::mklocator(&format!("input[name='{}']", field));
        let locator = WebDriverCommand::FindElementElement(self.f.clone(), locator);
        let f = Form {
            c: self.c.dup(),
            f: self.f.clone(),
        };
        let value_string = value.to_string();
        Box::new(self.c
            .dup()
            .issue_wd_cmd(locator)
            .map_err(|e| e.into())
            .and_then(|(this, res)| {
                let f = this.parse_lookup(res);
                f.map(move |f| (this, f))
            })
            .and_then(move |(this, field)| {
                use rustc_serialize::json::ToJson;
                let mut args = vec![field.to_json(), Json::String(value_string)];
                this.fixup_elements(&mut args);
                let cmd = webdriver::command::JavascriptCommandParameters {
                    script: "arguments[0].value = arguments[1]".to_string(),
                    args: webdriver::common::Nullable::Value(args),
                };

                this.issue_wd_cmd(WebDriverCommand::ExecuteScript(cmd))
            })
            .and_then(|(_, res)| if res.is_null() {
                Ok(f)
            } else {
                Err(error::CmdError::NotW3C(res))
            }))
    }

    /// Submit this form using the first available submit button.
    ///
    /// `false` is returned if no submit button was not found.
    pub fn submit(self) -> Box<Future<Item = Client, Error = error::CmdError>> {
        self.submit_with("input[type=submit],button[type=submit]")
    }

    /// Submit this form using the button matched by the given CSS selector.
    ///
    /// `false` is returned if a matching button was not found.
    pub fn submit_with(
        self,
        button: &str,
    ) -> Box<Future<Item = Client, Error = error::CmdError>> {
        let locator = Client::mklocator(button);
        let locator = WebDriverCommand::FindElementElement(self.f, locator);
        Box::new(
            self.c
                .issue_wd_cmd(locator)
                .map_err(|e| e.into())
                .and_then(|(this, res)| {
                    let s = this.parse_lookup(res);
                    s.map(move |s| (this, s))
                })
                .and_then(move |(this, submit)| {
                    this.issue_wd_cmd(WebDriverCommand::ElementClick(submit))
                })
                .and_then(move |(this, res)| {
                    if res.is_null() || res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
                        // geckodriver returns {} :(
                        Ok(this)
                    } else {
                        Err(error::CmdError::NotW3C(res))
                    }
                }),
        ) as Box<Future<Item = _, Error = _>>
    }

    /// Submit this form using the form submit button with the given label (case-insensitive).
    ///
    /// `false` is returned if a matching button was not found.
    pub fn submit_using(
        self,
        button_label: &str,
    ) -> Box<Future<Item = Client, Error = error::CmdError>> {
        let escaped = button_label.replace('\\', "\\\\").replace('"', "\\\"");
        Box::new(self.submit_with(&format!(
            "input[type=submit][value=\"{}\" i],\
             button[type=submit][value=\"{}\" i]",
            escaped,
            escaped
        ))) as Box<Future<Item = _, Error = _>>
    }

    /// Submit this form directly, without clicking any buttons.
    ///
    /// This can be useful to bypass forms that perform various magic when the submit button is
    /// clicked, or that hijack click events altogether (yes, I'm looking at you online
    /// advertisement code).
    ///
    /// Note that since no button is actually clicked, the `name=value` pair for the submit button
    /// will not be submitted. This can be circumvented by using `submit_sneaky` instead.
    pub fn submit_direct(self) -> Box<Future<Item = Client, Error = error::CmdError>> {
        use rustc_serialize::json::ToJson;

        let mut args = vec![self.f.clone().to_json()];
        self.c.fixup_elements(&mut args);
        // some sites are silly, and name their submit button "submit". this ends up overwriting
        // the "submit" function of the form with a reference to the submit button itself, so we
        // can't call .submit(). we get around this by creating a *new* form, and using *its*
        // submit() handler but with this pointed to the real form. solution from here:
        // https://stackoverflow.com/q/833032/472927#comment23038712_834197
        let cmd = webdriver::command::JavascriptCommandParameters {
            script: "document.createElement('form').submit.call(arguments[0])".to_string(),
            args: webdriver::common::Nullable::Value(args),
        };

        Box::new(
            self.c
                .issue_wd_cmd(WebDriverCommand::ExecuteScript(cmd))
                .and_then(move |(this, res)| {
                    if res.is_null() || res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
                        // geckodriver returns {} :(
                        Ok(this)
                    } else {
                        Err(error::CmdError::NotW3C(res))
                    }
                }),
        ) as Box<Future<Item = _, Error = _>>
    }

    /// Submit this form directly, without clicking any buttons, and with an extra field.
    ///
    /// Like `submit_direct`, this method will submit this form without clicking a submit button.
    /// However, it will *also* inject a hidden input element on the page that carries the given
    /// `field=value` mapping. This allows you to emulate the form data as it would have been *if*
    /// the submit button was indeed clicked.
    pub fn submit_sneaky(
        self,
        field: &str,
        value: &str,
    ) -> Box<Future<Item = Client, Error = error::CmdError>> {
        use rustc_serialize::json::ToJson;
        let mut args = vec![
            self.f.clone().to_json(),
            Json::String(field.to_string()),
            Json::String(value.to_string()),
        ];
        self.c.fixup_elements(&mut args);
        let cmd = webdriver::command::JavascriptCommandParameters {
            script: "\
                     var h = document.createElement('input');\
                     h.setAttribute('type', 'hidden');\
                     h.setAttribute('name', arguments[1]);\
                     h.value = arguments[2];\
                     arguments[0].appendChild(h)"
                .to_string(),
            args: webdriver::common::Nullable::Value(args),
        };

        let f = self.f;
        Box::new(
            self.c
                .issue_wd_cmd(WebDriverCommand::ExecuteScript(cmd))
                .and_then(move |(this, res)| {
                    if res.is_null() | res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
                        // geckodriver returns {} :(
                        future::Either::A(Form { f: f, c: this }.submit_direct())
                    } else {
                        future::Either::B(future::err(error::CmdError::NotW3C(res)))
                    }
                }),
        ) as Box<Future<Item = _, Error = _>>
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio_core::reactor::Core;

    macro_rules! tester {
        ($f:ident) => {{
            let mut core = Core::new().unwrap();
            let h = core.handle();
            let (c, fin) = Client::new("http://localhost:4444", &h);
            let c = core.run(c)
                .expect("failed to construct test client");
            core.run($f(&c))
                .expect("test produced unexpected error response");
            drop(c);
            core.run(fin).expect("failed to close test session");
        }}
    }

    fn works_inner<'a>(c: &'a Client) -> Box<Future<Item = (), Error = error::CmdError>> {
        // go to the Wikipedia page for Foobar
        c.goto("https://en.wikipedia.org/wiki/Foobar")
            .and_then(move |_| c.current_url())
            .and_then(move |url| {
                assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foobar");
                // click "Foo (disambiguation)"
                c.by_selector(".mw-disambig")
            })
            .and_then(|e| e.click())
            .and_then(move |_| {
                // click "Foo Lake"
                c.by_link_text("Foo Lake")
            })
            .and_then(|e| e.click())
            .and_then(move |_| c.current_url())
            .and_then(|url| {
                assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foo_Lake");
                Ok(())
            })
    }

    #[test]
    #[ignore]
    fn it_works() {
        tester!(works_inner)
    }

    fn clicks_inner<'a>(c: &'a Client) -> Box<Future<Item = (), Error = error::CmdError>> {
        // go to the Wikipedia frontpage this time
        c.goto("https://www.wikipedia.org/")
            .and_then(move |_| {
                // find, fill out, and submit the search form
                c.form("#search-form")
            })
            .and_then(|f| f.set_by_name("search", "foobar"))
            .and_then(|f| f.submit())
            .and_then(move |_| c.current_url())
            .and_then(|url| {
                // we should now have ended up in the rigth place
                assert_eq!(url.as_ref(), "https://en.wikipedia.org/wiki/Foobar");
                Ok(())
            })
    }

    #[test]
    #[ignore]
    fn it_clicks() {
        tester!(clicks_inner)
    }

    fn raw_inner<'a>(c: &'a Client) -> Box<Future<Item = (), Error = error::CmdError>> {
        // go back to the frontpage
        c.goto("https://www.wikipedia.org/")
            .and_then(move |_| {
                // find the source for the Wikipedia globe
                c.by_selector("img.central-featured-logo")
            })
            .and_then(|img| {
                img.attr("src")
                    .map(|src| src.expect("image should have a src"))
            })
            .and_then(move |src| {
                // now build a raw HTTP client request (which also has all current cookies)
                c.raw_client_for(Method::Get, &src)
            })
            .and_then(|raw| {
                // we then read out the image bytes
                raw.body()
                    .map_err(error::CmdError::from)
                    .fold(Vec::new(), |mut pixels, chunk| {
                        pixels.extend(&*chunk);
                        future::ok::<Vec<u8>, error::CmdError>(pixels)
                    })
            })
            .and_then(|pixels| {
                // and voilla, we now have the bytes for the Wikipedia logo!
                assert!(pixels.len() > 0);
                println!("Wikipedia logo is {}b", pixels.len());
                Ok(())
            })
    }

    #[test]
    #[ignore]
    fn it_can_be_raw() {
        tester!(raw_inner)
    }
}