├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── bin ├── .gitignore ├── build_docs ├── build_local ├── build_tc ├── download_chromedriver ├── download_geckodriver ├── publish └── travis │ └── before ├── doc └── release.md ├── src ├── bin │ └── www.rs ├── chrome.rs ├── firefox.rs ├── lib.rs ├── messages.rs └── util.rs └── tests ├── integration.rs ├── integration_test.html └── www ├── favicon.ico ├── inner_frame.html ├── page1.html ├── page2.html └── page3.html /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | env: 7 | - RUST_BACKTRACE=1 8 | addons: 9 | # Force the latest firefox, otherwise we get an old ESR version 10 | # that we don't even support 11 | firefox: latest 12 | chrome: stable 13 | before_script: 14 | - source bin/travis/before 15 | script: 16 | - TC="${TRAVIS_RUST_VERSION}" bin/build_tc 17 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webdriver_client" 3 | version = "0.2.5" 4 | 5 | description = "WebDriver client library" 6 | license = "ISC" 7 | repository = "https://github.com/fluffysquirrels/webdriver_client_rust" 8 | keywords = ["webdriver", "testing", "browser"] 9 | categories = ["api-bindings", "development-tools", "web-programming"] 10 | readme="README.md" 11 | authors = ["equalsraf ", 12 | "Alex Helfet "] 13 | 14 | [badges.travis-ci] 15 | repository = "fluffysquirrels/webdriver_client_rust" 16 | branch = "master" 17 | 18 | [dependencies] 19 | base64 = "^0.12.0" 20 | derive_builder = "^0.5.1" 21 | hyper = "^0.10" 22 | serde = "^1.0" 23 | serde_json = "^1.0" 24 | serde_derive = "^1.0" 25 | log = "^0.3" 26 | rand = "^0.3" 27 | rustyline = { version = "^1.0", optional = true } 28 | stderrlog = "^0.2" 29 | clap = "^2.0" 30 | 31 | [dev-dependencies] 32 | env_logger = "^0.4" 33 | 34 | [features] 35 | default = ["shell"] 36 | shell = ["rustyline"] 37 | 38 | [[bin]] 39 | name = "www" 40 | path = "src/bin/www.rs" 41 | required-features = ["shell"] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 11 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 12 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 13 | PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebDriver client library in Rust 2 | 3 | `webdriver_client` on crates.io 4 | 5 | [![crates.io](https://img.shields.io/crates/v/webdriver_client.svg)](https://crates.io/crates/webdriver_client) 6 | 7 | [![docs.rs](https://docs.rs/webdriver_client/badge.svg)](https://docs.rs/webdriver_client) 8 | 9 | Source code and issues on GitHub: 10 | [![GitHub last commit](https://img.shields.io/github/last-commit/fluffysquirrels/webdriver_client_rust.svg)][github] 11 | 12 | [github]: https://github.com/fluffysquirrels/webdriver_client_rust 13 | 14 | CI build on Travis CI: [![Build Status](https://travis-ci.org/fluffysquirrels/webdriver_client_rust.svg)](https://travis-ci.org/fluffysquirrels/webdriver_client_rust) 15 | 16 | Pull requests welcome. 17 | 18 | ## Getting started 19 | 20 | [GeckoDriver] and [ChromeDriver] are fully supported as WebDriver backends by the `webdriver_client::firefox::GeckoDriver` and `webdriver_client::chrome::ChromeDriver` structs. This crate expects the driver to be on your path. 21 | 22 | However HttpDriver will accept any WebDriver server's HTTP URL, so 23 | [Microsoft WebDriver for Edge][ms-wd], `safaridriver` for Apple 24 | Safari, and [OperaDriver] for Opera should all work if you start the 25 | server yourself. 26 | 27 | [GeckoDriver]: https://github.com/mozilla/geckodriver 28 | [ChromeDriver]: https://sites.google.com/a/chromium.org/chromedriver/getting-started 29 | [ms-wd]: https://docs.microsoft.com/en-us/microsoft-edge/webdriver 30 | [OperaDriver]: https://github.com/operasoftware/operachromiumdriver 31 | 32 | ### On Linux 33 | 34 | The scripts `bin/download_geckodriver` and `bin/download_chromedriver` download the Linux x64 binary releases for geckodriver and chromedriver. 35 | 36 | This snippet will download the drivers and place it on your current shell's path: 37 | ```sh 38 | bin/download_geckodriver 39 | bin/download_chromedriver 40 | export PATH=$PATH:$PWD/bin 41 | ``` 42 | 43 | ## Tests 44 | 45 | `cargo test` runs a few tests. Integration tests require geckodriver and chromedriver to be installed. 46 | 47 | ## Changelog 48 | 49 | ### v0.2.6 50 | 51 | * Update tests, Chrome now returns relative URLs on links. 52 | 53 | ### v0.2.5 54 | 55 | * `Error` implements `std::error::Error`. 56 | 57 | ### v0.2.4 58 | 59 | * Added screenshot support: `DriverSession::screenshot()` and `Element::screenshot()`. 60 | * Add `Element::click()`. 61 | * Add `Element::send_keys()`. 62 | * Add alert functionality to `DriverSession`: `dismiss_alert`, `accept_alert`, `get_alert_text`, `send_alert_text`. 63 | 64 | ### v0.2.0 65 | 66 | * Added ChromeDriver. 67 | * `www` bin has new commands: `frames`, `switchframe`. 68 | * Breaking change: `Driver::session and DriverSession::create_session` take 69 | a `NewSessionCmd` argument that specifies the session capabilities. 70 | * New method on `DriverSession`: `browser_name` 71 | * New methods on `Element`: `property`, `clear`, `find_element`, `find_elements` 72 | `raw_reference`. 73 | * Integration tests: many more of them, more assertions and a built-in HTTP server. 74 | 75 | --------------- 76 | 77 | This fork is based on equalsraf's excellent work from . 78 | -------------------------------------------------------------------------------- /bin/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore driver binaries downloaded by download_{geckodriver,chromedriver}. 2 | geckodriver* 3 | chromedriver* -------------------------------------------------------------------------------- /bin/build_docs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | CRATE_DIR=$( cd $(dirname ${BASH_SOURCE[0]})/..; pwd ) 5 | 6 | TC=nightly 7 | 8 | rustup toolchain update ${TC}; 9 | 10 | export CARGO_TARGET_DIR="${CRATE_DIR}/target/doc"; 11 | cargo +${TC} doc -p webdriver_client --verbose --no-deps --open; 12 | -------------------------------------------------------------------------------- /bin/build_local: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex; 3 | 4 | # A simple build script to run locally before pushing. 5 | 6 | CRATE_DIR=$( cd $(dirname ${BASH_SOURCE[0]})/..; pwd ) 7 | 8 | for TC in nightly stable beta; do 9 | TC="${TC}" ${CRATE_DIR}/bin/build_tc 10 | done 11 | -------------------------------------------------------------------------------- /bin/build_tc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e; 3 | readonly crate_dir="$( cd $(dirname ${BASH_SOURCE})/..; pwd )"; 4 | 5 | # Run a build with a given toolchain. 6 | TC=${TC?Supply a toolchain in this environment variable} 7 | 8 | echo "Rust toolchain: ${TC}" 9 | 10 | CRATE_DIR=$( cd $(dirname ${BASH_SOURCE[0]})/..; pwd ) 11 | export CARGO_TARGET_DIR="${CRATE_DIR}/target/build_local/${TC}" 12 | 13 | set -x; 14 | 15 | rustup toolchain update ${TC}; 16 | 17 | cd ${CRATE_DIR}; 18 | 19 | cargo +${TC} build -p webdriver_client --verbose; 20 | export PATH="${PATH}:${crate_dir}/bin"; 21 | RUST_LOG="webdriver=trace" \ 22 | cargo +${TC} test -p webdriver_client --verbose -- \ 23 | --test-threads=1; 24 | -------------------------------------------------------------------------------- /bin/download_chromedriver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | script_dir=$( cd $(dirname ${BASH_SOURCE[0]}); pwd ) 5 | 6 | version=${version:-$(wget -q -O - http://chromedriver.storage.googleapis.com/LATEST_RELEASE)}; 7 | 8 | curl -sSL \ 9 | http://chromedriver.storage.googleapis.com/$version/chromedriver_linux64.zip \ 10 | -o ${script_dir}/chromedriver-linux64.zip 11 | unzip -o -d ${script_dir} ${script_dir}/chromedriver-linux64.zip 12 | chmod +x ${script_dir}/chromedriver 13 | -------------------------------------------------------------------------------- /bin/download_geckodriver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | script_dir=$( cd $(dirname ${BASH_SOURCE[0]}); pwd ) 5 | 6 | version="v0.26.0" 7 | 8 | curl -sSL \ 9 | https://github.com/mozilla/geckodriver/releases/download/${version}/geckodriver-${version}-linux64.tar.gz \ 10 | -o ${script_dir}/geckodriver-${version}-linux64.tar.gz 11 | tar -C ${script_dir} --overwrite -xzf \ 12 | ${script_dir}/geckodriver-${version}-linux64.tar.gz 13 | -------------------------------------------------------------------------------- /bin/publish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | CRATE_DIR=$( cd $(dirname ${BASH_SOURCE[0]})/..; pwd ) 5 | 6 | TC=stable 7 | 8 | rustup toolchain update ${TC}; 9 | cargo +${TC} publish --verbose --manifest-path ${CRATE_DIR}/Cargo.toml; 10 | -------------------------------------------------------------------------------- /bin/travis/before: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script is sourced as the travis before-script. 4 | # If testing locally, source it from the crate root directory. 5 | 6 | # Store old shell options 7 | shopt_old=$-; 8 | 9 | set -x; 10 | 11 | # Install geckodriver 12 | bin/download_geckodriver; 13 | 14 | # Install chromedriver 15 | version="92.0.4515.107" bin/download_chromedriver; 16 | export CHROME_BIN=/usr/bin/google-chrome-stable 17 | export CHROME_HEADLESS=1 18 | 19 | export PATH=$PATH:$PWD/bin; 20 | 21 | # Show bin locations 22 | which firefox; 23 | which geckodriver; 24 | which chromedriver; 25 | 26 | # Restore old shell options 27 | if [[ ${shopt_old} != *"x"* ]]; then 28 | set +x; 29 | fi 30 | -------------------------------------------------------------------------------- /doc/release.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | ## `webdriver_client` crate 4 | 5 | 1. Push changes to [GitHub][github]. 6 | 1. Check build locally with `bin/build_local`. 7 | 1. Check Travis build: [![Build Status](https://travis-ci.org/fluffysquirrels/webdriver_client_rust.svg)][travis] 8 | 9 | 1. Increment version number in Cargo.toml (major version if breaking changes). 10 | 1. Commit to update the version number: 11 | 12 | `git commit -m "Update version number"` 13 | 14 | 1. Add a git tag for the new version number: 15 | 16 | `git tag v1.2.3` 17 | 18 | 1. Push to [GitHub][github]: 19 | 20 | `git push origin master && git push --tags` 21 | 22 | 1. Publish to [crates.io][crates] with `bin/publish`. 23 | 1. Check new version appears on 24 | [![Crate](https://img.shields.io/crates/v/webdriver_client.svg)][crates] 25 | and 26 | [![Documentation](https://docs.rs/webdriver_client/badge.svg)][docs] 27 | 28 | [travis]: https://travis-ci.org/fluffysquirrels/webdriver_client_rust 29 | [github]: https://github.com/fluffysquirrels/webdriver_client_rust 30 | [crates]: https://crates.io/crates/webdriver_client 31 | [docs]: https://docs.rs/webdriver_client 32 | -------------------------------------------------------------------------------- /src/bin/www.rs: -------------------------------------------------------------------------------- 1 | extern crate webdriver_client; 2 | use webdriver_client::*; 3 | use webdriver_client::messages::{LocationStrategy, ExecuteCmd}; 4 | use webdriver_client::firefox::GeckoDriver; 5 | use webdriver_client::chrome::ChromeDriver; 6 | 7 | extern crate rustyline; 8 | use rustyline::error::ReadlineError; 9 | use rustyline::Editor; 10 | 11 | extern crate clap; 12 | use clap::{App, Arg}; 13 | 14 | extern crate stderrlog; 15 | 16 | fn execute_function(name: &str, args: &str, sess: &DriverSession) -> Result<(), Error> { 17 | match name { 18 | "back" => sess.back()?, 19 | "go" => sess.go(args)?, 20 | "refresh" => sess.refresh()?, 21 | "source" => println!("{}", sess.get_page_source()?), 22 | "url" => println!("{}", sess.get_current_url()?), 23 | "innerhtml" => { 24 | for (idx, elem) in sess.find_elements(args, LocationStrategy::Css)?.iter().enumerate() { 25 | println!("#{} {}", idx, elem.inner_html()?); 26 | } 27 | } 28 | "outerhtml" => { 29 | for (idx, elem) in sess.find_elements(args, LocationStrategy::Css)?.iter().enumerate() { 30 | println!("#{} {}", idx, elem.outer_html()?); 31 | } 32 | } 33 | "frames" => { 34 | for (idx, elem) in sess.find_elements("iframe", LocationStrategy::Css)?.iter().enumerate() { 35 | println!("#{} {}", idx, elem.raw_reference()); 36 | } 37 | } 38 | "windows" => { 39 | for (idx, handle) in sess.get_window_handles()?.iter().enumerate() { 40 | println!("#{} {}", idx, handle) 41 | } 42 | } 43 | "execute" => { 44 | let script = ExecuteCmd { 45 | script: args.to_owned(), 46 | args: vec![], 47 | }; 48 | match sess.execute(script)? { 49 | JsonValue::String(ref s) => println!("{}", s), 50 | other => println!("{}", other), 51 | } 52 | } 53 | "switchframe" => { 54 | let arg = args.trim(); 55 | if arg.is_empty() { 56 | sess.switch_to_frame(JsonValue::Null)?; 57 | } else { 58 | sess.switch_to_frame(Element::new(sess, arg.to_string()).reference()?)?; 59 | } 60 | } 61 | _ => println!("Unknown function: \"{}\"", name), 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn execute(line: &str, sess: &DriverSession) -> Result<(), Error>{ 67 | let (cmd, args) = line.find(' ') 68 | .map_or((line, "".as_ref()), |idx| line.split_at(idx)); 69 | execute_function(cmd, args, sess) 70 | } 71 | 72 | fn main() { 73 | let matches = App::new("www") 74 | .arg(Arg::with_name("attach-to") 75 | .help("Attach to a running webdriver") 76 | .value_name("URL") 77 | .takes_value(true)) 78 | .arg(Arg::with_name("driver") 79 | .short("D") 80 | .long("driver") 81 | .possible_values(&["geckodriver", "chromedriver"]) 82 | .default_value("geckodriver") 83 | .takes_value(true)) 84 | .arg(Arg::with_name("verbose") 85 | .short("v") 86 | .multiple(true) 87 | .help("Increases verbose")) 88 | .get_matches(); 89 | 90 | stderrlog::new() 91 | .module("webdriver_client") 92 | .verbosity(matches.occurrences_of("verbose") as usize) 93 | .init() 94 | .expect("Unable to initialize logging in stderr"); 95 | 96 | let sess = match matches.value_of("attach-to") { 97 | Some(url) => HttpDriverBuilder::default() 98 | .url(url) 99 | .build().unwrap() 100 | .session(&Default::default()) 101 | .expect("Unable to attach to WebDriver session"), 102 | None => match matches.value_of("driver").unwrap() { 103 | "geckodriver" => { 104 | GeckoDriver::spawn() 105 | .expect("Unable to start geckodriver") 106 | .session(&Default::default()) 107 | .expect("Unable to start Geckodriver session") 108 | } 109 | "chromedriver" => { 110 | ChromeDriver::spawn() 111 | .expect("Unable to start chromedriver") 112 | .session(&Default::default()) 113 | .expect("Unable to start chromedriver session") 114 | } 115 | unsupported => { 116 | // should be unreachable see Arg::possible_values() 117 | panic!("Unsupported driver: {}", unsupported); 118 | } 119 | } 120 | }; 121 | 122 | let mut rl = Editor::<()>::new(); 123 | loop { 124 | let readline = rl.readline(">> "); 125 | match readline { 126 | Ok(line) => { 127 | rl.add_history_entry(&line); 128 | if let Err(err) = execute(line.trim_matches('\n'), &sess) { 129 | println!("{}", err); 130 | } 131 | }, 132 | Err(ReadlineError::Interrupted) => { 133 | break 134 | }, 135 | Err(ReadlineError::Eof) => { 136 | break 137 | }, 138 | Err(err) => { 139 | println!("Error: {:?}", err); 140 | break 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/chrome.rs: -------------------------------------------------------------------------------- 1 | //! Support for the Chrome browser. 2 | 3 | use super::*; 4 | 5 | use std::process::{Command, Child, Stdio}; 6 | use std::thread; 7 | use std::time::Duration; 8 | use std::ffi::OsString; 9 | 10 | use super::util; 11 | 12 | pub struct ChromeDriverBuilder { 13 | driver_binary: OsString, 14 | port: Option, 15 | kill_on_drop: bool, 16 | } 17 | 18 | impl ChromeDriverBuilder { 19 | pub fn new() -> Self { 20 | ChromeDriverBuilder { 21 | driver_binary: "chromedriver".into(), 22 | port: None, 23 | kill_on_drop: true, 24 | } 25 | } 26 | pub fn driver_path>(mut self, path: S) -> Self { 27 | self.driver_binary = path.into(); 28 | self 29 | } 30 | pub fn port(mut self, port: u16) -> Self { 31 | self.port = Some(port); 32 | self 33 | } 34 | pub fn kill_on_drop(mut self, kill: bool) -> Self { 35 | self.kill_on_drop = kill; 36 | self 37 | } 38 | pub fn spawn(self) -> Result { 39 | let port = util::check_tcp_port(self.port)?; 40 | 41 | let child = Command::new(self.driver_binary) 42 | .arg(format!("--port={}", port)) 43 | .stdin(Stdio::null()) 44 | .stderr(Stdio::null()) 45 | .stdout(Stdio::null()) 46 | .spawn()?; 47 | 48 | // TODO: parameterize this 49 | thread::sleep(Duration::new(1, 500)); 50 | Ok(ChromeDriver { 51 | child: child, 52 | url: format!("http://localhost:{}", port), 53 | kill_on_drop: self.kill_on_drop, 54 | }) 55 | } 56 | } 57 | 58 | 59 | /// A chromedriver process 60 | pub struct ChromeDriver { 61 | child: Child, 62 | url: String, 63 | kill_on_drop: bool, 64 | } 65 | 66 | impl ChromeDriver { 67 | pub fn spawn() -> Result { 68 | ChromeDriverBuilder::new().spawn() 69 | } 70 | pub fn build() -> ChromeDriverBuilder { 71 | ChromeDriverBuilder::new() 72 | } 73 | } 74 | 75 | impl Drop for ChromeDriver { 76 | fn drop(&mut self) { 77 | if self.kill_on_drop { 78 | let _ = self.child.kill(); 79 | } 80 | } 81 | } 82 | 83 | impl Driver for ChromeDriver { 84 | fn url(&self) -> &str { 85 | &self.url 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/firefox.rs: -------------------------------------------------------------------------------- 1 | //! Support for the Firefox browser. 2 | 3 | use super::*; 4 | 5 | use std::process::{Command, Child, Stdio}; 6 | use std::thread; 7 | use std::time::Duration; 8 | use std::ffi::OsString; 9 | 10 | use super::util; 11 | 12 | pub struct GeckoDriverBuilder { 13 | driver_binary: OsString, 14 | port: Option, 15 | ff_binary: String, 16 | kill_on_drop: bool, 17 | } 18 | 19 | impl GeckoDriverBuilder { 20 | pub fn new() -> Self { 21 | GeckoDriverBuilder { 22 | driver_binary: "geckodriver".into(), 23 | port: None, 24 | ff_binary: "firefox".to_owned(), 25 | kill_on_drop: true, 26 | } 27 | } 28 | pub fn driver_path>(mut self, path: S) -> Self { 29 | self.driver_binary = path.into(); 30 | self 31 | } 32 | pub fn port(mut self, port: u16) -> Self { 33 | self.port = Some(port); 34 | self 35 | } 36 | pub fn firefox_binary(mut self, binary: &str) -> Self { 37 | self.ff_binary = binary.to_owned(); 38 | self 39 | } 40 | pub fn kill_on_drop(mut self, kill: bool) -> Self { 41 | self.kill_on_drop = kill; 42 | self 43 | } 44 | pub fn spawn(self) -> Result { 45 | let port = util::check_tcp_port(self.port)?; 46 | 47 | let child = Command::new(self.driver_binary) 48 | .arg("-b") 49 | .arg(self.ff_binary) 50 | .arg("--port") 51 | .arg(format!("{}", port)) 52 | .stdin(Stdio::null()) 53 | .stderr(Stdio::null()) 54 | .stdout(Stdio::null()) 55 | .spawn()?; 56 | 57 | // TODO: parameterize this 58 | thread::sleep(Duration::new(1, 500)); 59 | Ok(GeckoDriver { 60 | child: child, 61 | url: format!("http://localhost:{}", port), 62 | kill_on_drop: self.kill_on_drop, 63 | }) 64 | } 65 | } 66 | 67 | 68 | /// A geckodriver process 69 | pub struct GeckoDriver { 70 | child: Child, 71 | url: String, 72 | kill_on_drop: bool, 73 | } 74 | 75 | impl GeckoDriver { 76 | pub fn spawn() -> Result { 77 | GeckoDriverBuilder::new().spawn() 78 | } 79 | pub fn build() -> GeckoDriverBuilder { 80 | GeckoDriverBuilder::new() 81 | } 82 | } 83 | 84 | impl Drop for GeckoDriver { 85 | fn drop(&mut self) { 86 | if self.kill_on_drop { 87 | let _ = self.child.kill(); 88 | } 89 | } 90 | } 91 | 92 | impl Driver for GeckoDriver { 93 | fn url(&self) -> &str { 94 | &self.url 95 | } 96 | } 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library to drive web browsers using the webdriver 2 | //! interface. 3 | 4 | // extern crates 5 | extern crate hyper; 6 | extern crate serde; 7 | #[macro_use] 8 | extern crate serde_json; 9 | #[macro_use] 10 | extern crate serde_derive; 11 | #[macro_use] 12 | extern crate log; 13 | #[macro_use] 14 | extern crate derive_builder; 15 | extern crate rand; 16 | 17 | // Sub-modules 18 | pub mod chrome; 19 | pub mod firefox; 20 | pub mod messages; 21 | pub mod util; 22 | 23 | // pub use statements 24 | pub use messages::LocationStrategy; 25 | pub use serde_json::Value as JsonValue; 26 | 27 | // use statements 28 | use hyper::client::*; 29 | use hyper::Url; 30 | use messages::*; 31 | use serde::Serialize; 32 | use serde::de::DeserializeOwned; 33 | use std::collections::BTreeMap; 34 | use std::convert::From; 35 | use std::fmt::{self, Debug}; 36 | use std::io::Read; 37 | use std::io; 38 | use std::error::Error as StdError; 39 | // -------- 40 | 41 | /// Error conditions returned by this crate. 42 | #[derive(Debug)] 43 | pub enum Error { 44 | FailedToLaunchDriver, 45 | InvalidUrl, 46 | ConnectionError, 47 | Io(io::Error), 48 | JsonDecodeError(serde_json::Error), 49 | WebDriverError(WebDriverError), 50 | Base64DecodeError(base64::DecodeError), 51 | } 52 | 53 | impl StdError for Error { 54 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 55 | match *self { 56 | Error::Io(ref err) => Some(err), 57 | Error::JsonDecodeError(ref err) => Some(err), 58 | Error::Base64DecodeError(ref err) => Some(err), 59 | _ => None 60 | } 61 | } 62 | } 63 | 64 | impl fmt::Display for Error { 65 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 66 | match *self { 67 | Error::FailedToLaunchDriver => write!(f, "Unable to start browser driver"), 68 | Error::InvalidUrl => write!(f, "Invalid URL"), 69 | Error::ConnectionError => write!(f, "Error connecting to browser"), 70 | Error::Io(ref err) => write!(f, "{}", err), 71 | Error::JsonDecodeError(ref s) => write!(f, "Received invalid response from browser: {}", s), 72 | Error::WebDriverError(ref err) => write!(f, "Error: {}", err.message), 73 | Error::Base64DecodeError(ref err) => write!(f, "Base64DecodeError: {}", err), 74 | } 75 | } 76 | } 77 | 78 | impl From for Error { 79 | fn from(_: hyper::Error) -> Error { 80 | Error::ConnectionError 81 | } 82 | } 83 | 84 | impl From for Error { 85 | fn from(err: io::Error) -> Error { 86 | Error::Io(err) 87 | } 88 | } 89 | 90 | impl From for Error { 91 | fn from(e: serde_json::Error) -> Error { 92 | Error::JsonDecodeError(e) 93 | } 94 | } 95 | 96 | impl From for Error { 97 | fn from(e: base64::DecodeError) -> Error { 98 | Error::Base64DecodeError(e) 99 | } 100 | } 101 | 102 | /// WebDriver server that can create a session. 103 | pub trait Driver { 104 | /// The url used to connect to this driver 105 | fn url(&self) -> &str; 106 | 107 | /// Start a session for this driver 108 | fn session(self, params: &NewSessionCmd) -> Result where Self : Sized + 'static { 109 | DriverSession::create_session(Box::new(self), params) 110 | } 111 | } 112 | 113 | /// A driver using a pre-existing WebDriver HTTP URL. 114 | #[derive(Builder)] 115 | #[builder(field(private))] 116 | pub struct HttpDriver { 117 | #[builder(setter(into))] 118 | url: String, 119 | } 120 | 121 | impl Driver for HttpDriver { 122 | fn url(&self) -> &str { &self.url } 123 | } 124 | 125 | /// Wrapper around the hyper Client, that handles Json encoding and URL construction 126 | struct HttpClient { 127 | baseurl: Url, 128 | http: Client, 129 | } 130 | 131 | impl HttpClient { 132 | pub fn new(baseurl: Url) -> Self { 133 | HttpClient { 134 | baseurl: baseurl, 135 | http: Client::new(), 136 | } 137 | } 138 | 139 | pub fn decode(res: &mut Response) -> Result { 140 | let mut data = String::new(); 141 | res.read_to_string(&mut data)?; 142 | debug!("result status: {}\n\ 143 | body: '{}'", res.status, data); 144 | 145 | if !res.status.is_success() { 146 | let err: Value = serde_json::from_str(&data)?; 147 | trace!("deserialize error result: {:#?}", err); 148 | return Err(Error::WebDriverError(err.value)); 149 | } 150 | let response = serde_json::from_str(&data); 151 | trace!("deserialize result: {:#?}", response); 152 | Ok(response?) 153 | } 154 | 155 | pub fn get(&self, path: &str) -> Result { 156 | let url = self.baseurl.join(path) 157 | .map_err(|_| Error::InvalidUrl)?; 158 | debug!("GET {}", url); 159 | let mut res = self.http.get(url) 160 | .send()?; 161 | Self::decode(&mut res) 162 | } 163 | 164 | pub fn delete(&self, path: &str) -> Result { 165 | let url = self.baseurl.join(path) 166 | .map_err(|_| Error::InvalidUrl)?; 167 | debug!("DELETE {}", url); 168 | let mut res = self.http.delete(url) 169 | .send()?; 170 | Self::decode(&mut res) 171 | } 172 | 173 | pub fn post(&self, path: &str, body: &E) -> Result { 174 | let url = self.baseurl.join(path) 175 | .map_err(|_| Error::InvalidUrl)?; 176 | let body_str = serde_json::to_string(body)?; 177 | debug!("POST url: {}\n\ 178 | body: {}", url, body_str); 179 | let mut res = self.http.post(url) 180 | .body(&body_str) 181 | .send()?; 182 | Self::decode(&mut res) 183 | } 184 | } 185 | 186 | /// A WebDriver session. 187 | /// 188 | /// By default the session is removed on `Drop` 189 | pub struct DriverSession { 190 | /// driver is kept so it is dropped when DriverSession is dropped. 191 | _driver: Box, 192 | client: HttpClient, 193 | session_id: String, 194 | drop_session: bool, 195 | capabilities: BTreeMap, 196 | } 197 | 198 | impl DriverSession { 199 | /// Create a new session with the driver. 200 | pub fn create_session(driver: Box, params: &NewSessionCmd) 201 | -> Result 202 | { 203 | let baseurl = Url::parse(driver.url()) 204 | .map_err(|_| Error::InvalidUrl)?; 205 | let client = HttpClient::new(baseurl); 206 | info!("Creating session at {}", client.baseurl); 207 | let sess = Self::new_session(&client, params)?; 208 | info!("Session {} created", sess.sessionId); 209 | Ok(DriverSession { 210 | _driver: driver, 211 | client: client, 212 | session_id: sess.sessionId, 213 | drop_session: true, 214 | capabilities: sess.capabilities, 215 | }) 216 | } 217 | 218 | /// Use an existing session 219 | pub fn attach(url: &str, session_id: &str) -> Result { 220 | let driver = Box::new(HttpDriver { 221 | url: url.to_owned(), 222 | }); 223 | let baseurl = Url::parse(url).map_err(|_| Error::InvalidUrl)?; 224 | let mut s = DriverSession { 225 | _driver: driver, 226 | client: HttpClient::new(baseurl), 227 | session_id: session_id.to_owned(), 228 | // This starts as false to avoid triggering the deletion call in Drop 229 | // if an error occurs 230 | drop_session: false, 231 | capabilities: Default::default(), 232 | }; 233 | info!("Connecting to session at {} with id {}", url, session_id); 234 | 235 | // FIXME /status would be preferable here to test the connection, but 236 | // it does not seem to work for the current geckodriver 237 | 238 | // We can fetch any value for the session to verify it exists. 239 | // The page URL will work. 240 | let _ = s.get_current_url()?; 241 | 242 | info!("Connected to existing session {}", s.session_id); 243 | // The session exists, enable session deletion on Drop 244 | s.drop_session = true; 245 | Ok(s) 246 | } 247 | 248 | pub fn browser_name(&self) -> Option<&str> { 249 | if let Some(&JsonValue::String(ref val)) = self.capabilities.get("browserName") { 250 | Some(val) 251 | } else { 252 | None 253 | } 254 | } 255 | 256 | pub fn session_id(&self) -> &str { 257 | &self.session_id 258 | } 259 | 260 | /// Whether to remove the session on Drop, the default is true 261 | pub fn drop_session(&mut self, drop: bool) { 262 | self.drop_session = drop; 263 | } 264 | 265 | /// Create a new webdriver session 266 | fn new_session(client: &HttpClient, params: &NewSessionCmd) -> Result { 267 | let resp: Value = client.post("/session", ¶ms)?; 268 | Ok(resp.value) 269 | } 270 | 271 | /// Navigate to the given URL 272 | pub fn go(&self, url: &str) -> Result<(), Error> { 273 | let params = GoCmd { url: url.to_string() }; 274 | let _: Empty = self.client.post(&format!("/session/{}/url", &self.session_id), ¶ms)?; 275 | Ok(()) 276 | } 277 | 278 | pub fn get_current_url(&self) -> Result { 279 | let v: Value<_> = self.client.get(&format!("/session/{}/url", self.session_id))?; 280 | Ok(v.value) 281 | } 282 | 283 | pub fn back(&self) -> Result<(), Error> { 284 | let _: Empty = self.client.post(&format!("/session/{}/back", self.session_id), &Empty {})?; 285 | Ok(()) 286 | } 287 | 288 | pub fn forward(&self) -> Result<(), Error> { 289 | let _: Empty = self.client.post(&format!("/session/{}/forward", self.session_id), &Empty {})?; 290 | Ok(()) 291 | } 292 | 293 | pub fn refresh(&self) -> Result<(), Error> { 294 | let _: Empty = self.client.post(&format!("/session/{}/refresh", self.session_id), &Empty {})?; 295 | Ok(()) 296 | } 297 | 298 | pub fn get_page_source(&self) -> Result { 299 | let v: Value<_> = self.client.get(&format!("/session/{}/source", self.session_id))?; 300 | Ok(v.value) 301 | } 302 | 303 | pub fn get_title(&self) -> Result { 304 | let v: Value<_> = self.client.get(&format!("/session/{}/title", self.session_id))?; 305 | Ok(v.value) 306 | } 307 | 308 | /// Get all cookies 309 | pub fn get_cookies(&self) -> Result, Error> { 310 | let v: Value<_> = self.client.get(&format!("/session/{}/cookie", self.session_id))?; 311 | Ok(v.value) 312 | } 313 | 314 | pub fn get_window_handle(&self) -> Result { 315 | let v: Value<_> = self.client.get(&format!("/session/{}/window", self.session_id))?; 316 | Ok(v.value) 317 | } 318 | 319 | pub fn switch_window(&mut self, handle: &str) -> Result<(), Error> { 320 | let _: Empty = self.client.post(&format!("/session/{}/window", self.session_id), &SwitchWindowCmd::from(handle))?; 321 | Ok(()) 322 | } 323 | 324 | pub fn close_window(&mut self) -> Result<(), Error> { 325 | let _: Empty = self.client.delete(&format!("/session/{}/window", self.session_id))?; 326 | Ok(()) 327 | } 328 | 329 | pub fn get_window_handles(&self) -> Result, Error> { 330 | let v: Value<_> = self.client.get(&format!("/session/{}/window/handles", self.session_id))?; 331 | Ok(v.value) 332 | } 333 | 334 | /// Dismiss an active dialog, if present. 335 | /// 336 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#dismiss-alert 337 | pub fn dismiss_alert(&self) -> Result<(), Error> { 338 | let _: Empty = self.client.post(&format!("/session/{}/alert/dismiss", self.session_id), &Empty {})?; 339 | Ok(()) 340 | } 341 | 342 | /// Accept an active dialog, if present. 343 | /// 344 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#accept-alert 345 | pub fn accept_alert(&self) -> Result<(), Error> { 346 | let _: Empty = self.client.post(&format!("/session/{}/alert/accept", self.session_id), &Empty {})?; 347 | Ok(()) 348 | } 349 | 350 | /// Get the message of an active dialog, if present. 351 | /// 352 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#get-alert-text 353 | pub fn get_alert_text(&self) -> Result { 354 | let v: Value<_> = self.client.get(&format!("/session/{}/alert/text", self.session_id))?; 355 | Ok(v.value) 356 | } 357 | 358 | /// Set the text field of a user prompt. 359 | /// 360 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#send-alert-text 361 | pub fn send_alert_text(&self, text :&str) -> Result<(), Error> { 362 | let _: Empty = self.client.post(&format!("/session/{}/alert/text", self.session_id), 363 | &SendAlertTextCmd { text: text.to_owned() })?; 364 | Ok(()) 365 | } 366 | 367 | pub fn find_element(&self, selector: &str, strategy: LocationStrategy) -> Result { 368 | let cmd = FindElementCmd { using: strategy, value: selector}; 369 | let v: Value = self.client.post(&format!("/session/{}/element", self.session_id), &cmd)?; 370 | Ok(Element::new(self, v.value.reference)) 371 | } 372 | 373 | pub fn find_elements(&self, selector: &str, strategy: LocationStrategy) -> Result, Error> { 374 | let cmd = FindElementCmd { using: strategy, value: selector}; 375 | let v: Value> = self.client.post(&format!("/session/{}/elements", self.session_id), &cmd)?; 376 | 377 | Ok(v.value.into_iter().map(|er| Element::new(self, er.reference)).collect()) 378 | } 379 | 380 | pub fn execute(&self, script: ExecuteCmd) -> Result { 381 | let v: Value = self.client.post(&format!("/session/{}/execute/sync", self.session_id), &script)?; 382 | Ok(v.value) 383 | } 384 | 385 | pub fn execute_async(&self, script: ExecuteCmd) -> Result { 386 | let v: Value = self.client.post(&format!("/session/{}/execute/async", self.session_id), &script)?; 387 | Ok(v.value) 388 | } 389 | 390 | /// Valid values are element references as returned by Element::reference() or null to switch 391 | /// to the top level frame 392 | pub fn switch_to_frame(&self, handle: JsonValue) -> Result<(), Error> { 393 | let _: Empty = self.client.post(&format!("/session/{}/frame", self.session_id), &SwitchFrameCmd::from(handle))?; 394 | Ok(()) 395 | } 396 | 397 | pub fn switch_to_parent_frame(&self) -> Result<(), Error> { 398 | let _: Empty = self.client.post(&format!("/session/{}/frame/parent", self.session_id), &Empty {})?; 399 | Ok(()) 400 | } 401 | 402 | /// Take a screenshot of the current frame. 403 | /// 404 | /// WebDriver specification: https://www.w3.org/TR/webdriver/#take-screenshot 405 | pub fn screenshot(&self) -> Result { 406 | let v: Value = self.client.get(&format!("/session/{}/screenshot", 407 | self.session_id))?; 408 | Screenshot::from_string(v.value) 409 | } 410 | } 411 | 412 | impl Drop for DriverSession { 413 | fn drop(&mut self) { 414 | if self.drop_session { 415 | let _: Result = self.client.delete(&format!("/session/{}", self.session_id)); 416 | } 417 | } 418 | } 419 | 420 | /// An HTML element within a WebDriver session. 421 | pub struct Element<'a> { 422 | session: &'a DriverSession, 423 | reference: String, 424 | } 425 | 426 | impl<'a> Element<'a> { 427 | pub fn new(s: &'a DriverSession, reference: String) -> Self { 428 | Element { session: s, reference: reference } 429 | } 430 | 431 | pub fn attribute(&self, name: &str) -> Result { 432 | let v: Value<_> = self.session.client.get(&format!("/session/{}/element/{}/attribute/{}", self.session.session_id(), self.reference, name))?; 433 | Ok(v.value) 434 | } 435 | 436 | /// Return this element's property value. 437 | /// 438 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#get-element-property 439 | pub fn property(&self, name: &str) -> Result { 440 | let v: Value<_> = self.session.client.get(&format!("/session/{}/element/{}/property/{}", self.session.session_id(), self.reference, name))?; 441 | Ok(v.value) 442 | } 443 | 444 | /// Click this element. 445 | /// 446 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#element-click 447 | pub fn click(&self) -> Result<(), Error> { 448 | let _: Value = self.session.client.post( 449 | &format!("/session/{}/element/{}/click", self.session.session_id(), self.reference), 450 | &Empty {})?; 451 | Ok(()) 452 | } 453 | 454 | /// Clear the text of this element. 455 | /// 456 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#element-clear 457 | pub fn clear(&self) -> Result<(), Error> { 458 | let _: Value = self.session.client.post(&format!("/session/{}/element/{}/clear", self.session.session_id(), self.reference), &Empty {})?; 459 | Ok(()) 460 | } 461 | 462 | /// Send key presses to this element. 463 | /// 464 | /// WebDriver spec: https://www.w3.org/TR/webdriver/#element-send-keys 465 | pub fn send_keys(&self, s: &str) -> Result<(), Error> { 466 | let _: Value = 467 | self.session.client.post(&format!("/session/{}/element/{}/value", 468 | self.session.session_id(), self.reference), 469 | &json!({ "text": s }))?; 470 | Ok(()) 471 | } 472 | 473 | pub fn css_value(&self, name: &str) -> Result { 474 | let v: Value<_> = self.session.client.get(&format!("/session/{}/element/{}/css/{}", self.session.session_id(), self.reference, name))?; 475 | Ok(v.value) 476 | } 477 | 478 | pub fn text(&self) -> Result { 479 | let v: Value<_> = self.session.client.get(&format!("/session/{}/element/{}/text", self.session.session_id(), self.reference))?; 480 | Ok(v.value) 481 | } 482 | 483 | /// Returns the tag name for this element 484 | pub fn name(&self) -> Result { 485 | let v: Value<_> = self.session.client.get(&format!("/session/{}/element/{}/name", self.session.session_id(), self.reference))?; 486 | Ok(v.value) 487 | } 488 | 489 | pub fn find_element(&self, selector: &str, strategy: LocationStrategy) -> Result { 490 | let cmd = FindElementCmd { using: strategy, value: selector }; 491 | let v: Value = self.session.client.post(&format!("/session/{}/element/{}/element", self.session.session_id, self.reference), &cmd)?; 492 | Ok(Element::new(self.session, v.value.reference)) 493 | } 494 | 495 | pub fn find_elements(&self, selector: &str, strategy: LocationStrategy) -> Result, Error> { 496 | let cmd = FindElementCmd { using: strategy, value: selector }; 497 | let v: Value> = self.session.client.post(&format!("/session/{}/element/{}/elements", self.session.session_id, self.reference), &cmd)?; 498 | 499 | Ok(v.value.into_iter().map(|er| Element::new(self.session, er.reference)).collect()) 500 | } 501 | 502 | /// Returns a reference that can be passed on to the API 503 | pub fn reference(&self) -> Result { 504 | serde_json::to_value(&ElementReference::from_str(&self.reference)) 505 | .map_err(|err| Error::from(err)) 506 | } 507 | 508 | /// The raw reference id that identifies this element, this can be used 509 | /// with Element::new() 510 | pub fn raw_reference(&self) -> &str { &self.reference } 511 | 512 | /// Gets the `innerHTML` javascript attribute for this element. Some drivers can get 513 | /// this using regular attributes, in others it does not work. This method gets it 514 | /// executing a bit of javascript. 515 | pub fn inner_html(&self) -> Result { 516 | let script = ExecuteCmd { 517 | script: "return arguments[0].innerHTML;".to_owned(), 518 | args: vec![self.reference()?], 519 | }; 520 | self.session.execute(script) 521 | } 522 | 523 | pub fn outer_html(&self) -> Result { 524 | let script = ExecuteCmd { 525 | script: "return arguments[0].outerHTML;".to_owned(), 526 | args: vec![self.reference()?], 527 | }; 528 | self.session.execute(script) 529 | } 530 | 531 | /// Take a screenshot of this element 532 | /// 533 | /// WebDriver specification: https://www.w3.org/TR/webdriver/#take-element-screenshot 534 | pub fn screenshot(&self) -> Result { 535 | let v: Value = self.session.client.get( 536 | &format!("/session/{}/element/{}/screenshot", 537 | self.session.session_id, 538 | self.reference))?; 539 | Screenshot::from_string(v.value) 540 | } 541 | } 542 | 543 | impl<'a> fmt::Debug for Element<'a> { 544 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 545 | write!(f, "WebDriver Element with remote reference {}", self.reference) 546 | } 547 | } 548 | 549 | /// Switch the context of the current session to the given frame reference. 550 | /// 551 | /// This structure implements Drop, and restores the session context 552 | /// to the current top level window. 553 | pub struct FrameContext<'a> { 554 | session: &'a DriverSession, 555 | } 556 | 557 | impl<'a> FrameContext<'a> { 558 | pub fn new(session: &'a DriverSession, frameref: JsonValue) -> Result, Error> { 559 | session.switch_to_frame(frameref)?; 560 | Ok(FrameContext { session: session }) 561 | } 562 | } 563 | 564 | impl<'a> Drop for FrameContext<'a> { 565 | fn drop(&mut self) { 566 | let _ = self.session.switch_to_frame(JsonValue::Null); 567 | } 568 | } 569 | 570 | pub struct Screenshot { 571 | base64: String, 572 | } 573 | 574 | impl Screenshot { 575 | fn from_string(s: String) -> Result { 576 | Ok(Screenshot { 577 | base64: s, 578 | }) 579 | } 580 | 581 | pub fn bytes(&self) -> Result, Error> { 582 | Ok(base64::decode(&self.base64)?) 583 | } 584 | 585 | pub fn save_file(&self, path: &str) -> Result<(), Error> { 586 | Ok(std::fs::write(path, self.bytes()?)?) 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/messages.rs: -------------------------------------------------------------------------------- 1 | //! Messages sent and received in the WebDriver protocol. 2 | 3 | #![allow(non_snake_case)] 4 | 5 | use ::util::merge_json_mut; 6 | use serde::{Serialize, Deserialize, Serializer, Deserializer}; 7 | use serde::de::{Visitor, MapAccess}; 8 | use serde::de::Error as DeError; 9 | use serde::ser::SerializeStruct; 10 | use serde_json::Value as JsonValue; 11 | use std::fmt; 12 | use std::collections::BTreeMap; 13 | 14 | #[derive(Debug)] 15 | pub enum LocationStrategy { 16 | Css, 17 | LinkText, 18 | PartialLinkText, 19 | XPath, 20 | } 21 | 22 | impl Serialize for LocationStrategy { 23 | fn serialize(&self, s: S) -> Result { 24 | match self { 25 | &LocationStrategy::Css => s.serialize_str("css selector"), 26 | &LocationStrategy::LinkText => s.serialize_str("link text"), 27 | &LocationStrategy::PartialLinkText => s.serialize_str("partial link text"), 28 | &LocationStrategy::XPath => s.serialize_str("xpath"), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Debug, Deserialize)] 34 | pub struct WebDriverError { 35 | pub error: String, 36 | pub message: String, 37 | pub stacktrace: Option, 38 | } 39 | 40 | #[derive(Serialize, Default)] 41 | struct Capabilities { 42 | alwaysMatch: JsonValue, 43 | } 44 | 45 | /// The arguments to create a new session, including the capabilities 46 | /// as defined by the [WebDriver specification][cap-spec]. 47 | /// 48 | /// [cap-spec]: https://www.w3.org/TR/webdriver/#capabilities 49 | #[derive(Serialize)] 50 | pub struct NewSessionCmd { 51 | capabilities: Capabilities, 52 | } 53 | 54 | impl NewSessionCmd { 55 | /// Merges a new `alwaysMatch` capability with the given `key` and 56 | /// `value` into the new session's capabilities. 57 | /// 58 | /// For the merging rules, see the documentation of 59 | /// `webdriver_client::util::merge_json`. 60 | pub fn always_match(&mut self, key: &str, value: JsonValue) -> &mut Self { 61 | merge_json_mut(&mut self.capabilities.alwaysMatch, 62 | &json!({ key: value })); 63 | self 64 | } 65 | 66 | /// Resets the `alwaysMatch` capabilities to an empty JSON object. 67 | pub fn reset_always_match(&mut self) -> &mut Self { 68 | self.capabilities.alwaysMatch = json!({}); 69 | self 70 | } 71 | } 72 | 73 | impl Default for NewSessionCmd { 74 | fn default() -> Self { 75 | NewSessionCmd { 76 | capabilities: Capabilities { 77 | alwaysMatch: json!({ 78 | 79 | // By default chromedriver is NOT compliant with the w3c 80 | // spec. But one can request w3c compliance with a capability 81 | // extension included in the new session command payload. 82 | "goog:chromeOptions": { 83 | "w3c": true 84 | } 85 | }) 86 | }, 87 | } 88 | } 89 | } 90 | 91 | #[derive(Debug, Deserialize)] 92 | pub struct Session { 93 | pub sessionId: String, 94 | pub capabilities: BTreeMap, 95 | } 96 | 97 | #[derive(Serialize)] 98 | pub struct GoCmd { 99 | pub url: String, 100 | } 101 | 102 | #[derive(Debug, Deserialize)] 103 | pub struct Value { 104 | pub value: T, 105 | } 106 | 107 | #[derive(Debug, Deserialize)] 108 | pub struct CurrentTitle { 109 | pub title: String, 110 | } 111 | 112 | #[derive(Serialize)] 113 | pub struct SwitchFrameCmd { 114 | pub id: JsonValue, 115 | } 116 | 117 | impl SwitchFrameCmd { 118 | pub fn from(id: JsonValue) -> Self { 119 | SwitchFrameCmd { id: id } 120 | } 121 | } 122 | 123 | #[derive(Serialize)] 124 | pub struct SwitchWindowCmd { 125 | handle: String, 126 | } 127 | 128 | impl SwitchWindowCmd { 129 | pub fn from(handle: &str) -> Self { 130 | SwitchWindowCmd { handle: handle.to_string() } 131 | } 132 | } 133 | 134 | #[derive(Debug, Deserialize, Serialize)] 135 | pub struct Empty {} 136 | 137 | #[derive(Serialize)] 138 | pub struct FindElementCmd<'a> { 139 | pub using: LocationStrategy, 140 | pub value: &'a str, 141 | } 142 | 143 | #[derive(PartialEq, Debug)] 144 | pub struct ElementReference { 145 | pub reference: String, 146 | } 147 | 148 | impl ElementReference { 149 | pub fn from_str(handle: &str) -> ElementReference { 150 | ElementReference { reference: handle.to_string() } 151 | } 152 | } 153 | 154 | impl Serialize for ElementReference { 155 | fn serialize(&self, s: S) -> Result { 156 | let mut ss = s.serialize_struct("ElementReference", 1)?; 157 | ss.serialize_field("element-6066-11e4-a52e-4f735466cecf", &self.reference)?; 158 | // even in w3c compliance mode chromedriver only accepts a reference name ELEMENT 159 | ss.serialize_field("ELEMENT", &self.reference)?; 160 | ss.end() 161 | } 162 | } 163 | 164 | impl<'de> Deserialize<'de> for ElementReference { 165 | fn deserialize>(d: D) -> Result { 166 | enum Field { Reference } 167 | 168 | impl<'de> Deserialize<'de> for Field { 169 | fn deserialize>(d: D) -> Result { 170 | struct FieldVisitor; 171 | impl<'de> Visitor<'de> for FieldVisitor { 172 | type Value = Field; 173 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 174 | formatter.write_str("element-6066-11e4-a52e-4f735466cecf") 175 | } 176 | 177 | fn visit_str(self, value: &str) -> Result 178 | { 179 | match value { 180 | "element-6066-11e4-a52e-4f735466cecf" => Ok(Field::Reference), 181 | _ => Err(DeError::unknown_field(value, FIELDS)), 182 | } 183 | } 184 | } 185 | 186 | d.deserialize_identifier(FieldVisitor) 187 | } 188 | } 189 | 190 | struct ElementReferenceVisitor; 191 | impl<'de> Visitor<'de> for ElementReferenceVisitor { 192 | type Value = ElementReference; 193 | 194 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 195 | formatter.write_str("struct ElementReference") 196 | } 197 | 198 | fn visit_map(self, mut visitor: V) -> Result 199 | where V: MapAccess<'de> 200 | { 201 | let mut reference = None; 202 | while let Some(key) = visitor.next_key()? { 203 | match key { 204 | Field::Reference => { 205 | if reference.is_some() { 206 | return Err(DeError::duplicate_field("element-6066-11e4-a52e-4f735466cecf")); 207 | } 208 | reference = Some(visitor.next_value()?); 209 | } 210 | } 211 | } 212 | match reference { 213 | Some(r) => Ok(ElementReference { reference: r }), 214 | None => return Err(DeError::missing_field("element-6066-11e4-a52e-4f735466cecf")), 215 | } 216 | } 217 | } 218 | 219 | const FIELDS: &'static [&'static str] = &["element-6066-11e4-a52e-4f735466cecf"]; 220 | d.deserialize_struct("ElementReference", FIELDS, ElementReferenceVisitor) 221 | } 222 | } 223 | 224 | #[derive(Debug, Deserialize)] 225 | pub struct Cookie { 226 | pub name: String, 227 | pub value: String, 228 | pub path: String, 229 | pub domain: String, 230 | pub secure: bool, 231 | pub httpOnly: bool, 232 | // TODO: expiry: 233 | } 234 | 235 | #[derive(Serialize)] 236 | pub struct ExecuteCmd { 237 | pub script: String, 238 | pub args: Vec, 239 | } 240 | 241 | #[derive(Serialize)] 242 | pub struct SendAlertTextCmd { 243 | pub text: String, 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::NewSessionCmd; 249 | #[test] 250 | fn capability_extend() { 251 | let mut session = NewSessionCmd::default(); 252 | session.always_match("cap", json!({"a": true})); 253 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!({"a": true})); 254 | 255 | session.always_match("cap", json!({"b": false})); 256 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!({"a": true, "b": false})); 257 | 258 | session.always_match("cap", json!({"a": false})); 259 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!({"a": false, "b": false})); 260 | } 261 | #[test] 262 | fn capability_extend_replaces_non_obj() { 263 | let mut session = NewSessionCmd::default(); 264 | session.always_match("cap", json!("value")); 265 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!("value")); 266 | 267 | session.always_match("cap", json!({"a": false})); 268 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!({"a": false})); 269 | } 270 | #[test] 271 | fn capability_extend_replaces_obj_with_non_obj() { 272 | let mut session = NewSessionCmd::default(); 273 | session.always_match("cap", json!({"value": true})) 274 | .always_match("cap", json!("new")); 275 | assert_eq!(session.capabilities.alwaysMatch.get("cap").unwrap(), &json!("new")); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Small utilities. 2 | 3 | use serde_json::Value as JsonValue; 4 | use std::io; 5 | use std::net::TcpListener; 6 | 7 | /// Find a TCP port number to use. This is racy but see 8 | /// https://bugzilla.mozilla.org/show_bug.cgi?id=1240830 9 | /// 10 | /// If port is Some, check if we can bind to the given port. Otherwise 11 | /// pick a random port. 12 | pub (crate) fn check_tcp_port(port: Option) -> io::Result { 13 | TcpListener::bind(&("localhost", port.unwrap_or(0))) 14 | .and_then(|stream| stream.local_addr()) 15 | .map(|x| x.port()) 16 | } 17 | 18 | /// Recursively merge serde_json::Value's from a then b into a new 19 | /// returned value. 20 | /// 21 | /// # Example 22 | /// 23 | /// ``` 24 | /// # #[macro_use] extern crate serde_json; 25 | /// # extern crate webdriver_client; 26 | /// # 27 | /// # use webdriver_client::util::merge_json; 28 | /// # fn main() { 29 | /// # 30 | /// let a = json!({ 31 | /// "a": "only in a", 32 | /// "overwritten": "value in a", 33 | /// "child_object": { "x": "value in a" }, 34 | /// "array": ["value 1 in a", "value 2 in a"], 35 | /// "different_types": 5 36 | /// }); 37 | /// let b = json!({ 38 | /// "b": "only in b", 39 | /// "overwritten": "value in b", 40 | /// "child_object": { "x": "value in b" }, 41 | /// "array": ["value in b"], 42 | /// "different_types": true 43 | /// }); 44 | /// let merged = merge_json(&a, &b); 45 | /// 46 | /// assert_eq!(merged, json!({ 47 | /// // When only one input contains the key, the value is cloned. 48 | /// "a": "only in a", 49 | /// "b": "only in b", 50 | /// 51 | /// // When both inputs contain a key, the value from b is cloned. 52 | /// "overwritten": "value in b", 53 | /// 54 | /// // When a child object is present in both values, it is recursively 55 | /// // merged. 56 | /// "child_object": { "x": "value in b" }, 57 | /// 58 | /// // If both values are an array, the value from b is cloned. 59 | /// "array": ["value in b"], 60 | /// 61 | /// // If the two values have different types, the value from b is cloned. 62 | /// "different_types": true 63 | /// })); 64 | /// # 65 | /// # } // Close main. 66 | pub fn merge_json(a: &JsonValue, b: &JsonValue) -> JsonValue { 67 | let mut out = a.clone(); 68 | merge_json_mut(&mut out, b); 69 | out 70 | } 71 | 72 | /// Recursively merge serde_json::Value's from b into a. 73 | pub fn merge_json_mut(a: &mut JsonValue, b: &JsonValue) { 74 | match (a, b) { 75 | // Recurse when a and b are both objects. 76 | (&mut JsonValue::Object(ref mut a), &JsonValue::Object(ref b)) => { 77 | for (k, v) in b { 78 | let a_entry = a.entry(k.clone()).or_insert(JsonValue::Null); 79 | merge_json_mut(a_entry, v); 80 | } 81 | } 82 | // When a and b aren't both objects, overwrite a. 83 | (a, b) => { 84 | *a = b.clone(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | extern crate env_logger; 2 | extern crate hyper; 3 | extern crate log; 4 | #[macro_use] 5 | extern crate serde_json; 6 | extern crate webdriver_client; 7 | 8 | use env_logger::{LogBuilder, LogTarget}; 9 | use log::LogLevelFilter; 10 | use std::io::Read; 11 | use std::env; 12 | use std::path::PathBuf; 13 | use std::sync::Once; 14 | use std::thread::sleep; 15 | use std::time::Duration; 16 | use webdriver_client::{Driver, DriverSession, HttpDriverBuilder, LocationStrategy}; 17 | use webdriver_client::firefox::GeckoDriver; 18 | use webdriver_client::chrome::ChromeDriver; 19 | use webdriver_client::messages::{ExecuteCmd, NewSessionCmd}; 20 | 21 | /// The different browsers supported in tests 22 | #[derive(Debug)] 23 | enum TestBrowser { 24 | Firefox, 25 | Chrome, 26 | } 27 | 28 | impl TestBrowser { 29 | fn session(&self) -> DriverSession { 30 | match *self { 31 | TestBrowser::Firefox => { 32 | GeckoDriver::build() 33 | .spawn() 34 | .expect("Error starting geckodriver") 35 | .session(&self.new_session_cmd()) 36 | .expect("Error starting session") 37 | } 38 | TestBrowser::Chrome => { 39 | ChromeDriver::build() 40 | .spawn() 41 | .expect("Error starting chromedriver") 42 | .session(&self.new_session_cmd()) 43 | .expect("Error starting session") 44 | } 45 | } 46 | } 47 | 48 | fn driver(&self) -> Box { 49 | match *self { 50 | TestBrowser::Firefox => { 51 | Box::new(GeckoDriver::build() 52 | .spawn().expect("Error starting geckodriver")) 53 | } 54 | TestBrowser::Chrome => { 55 | Box::new(ChromeDriver::build() 56 | .spawn().expect("Error starting chromedriver")) 57 | } 58 | } 59 | } 60 | 61 | fn new_session_cmd(&self) -> NewSessionCmd { 62 | let mut new = NewSessionCmd::default(); 63 | 64 | match *self { 65 | TestBrowser::Firefox => { 66 | new.always_match( 67 | "moz:firefoxOptions", json!({ 68 | "args": ["-headless"] 69 | })) 70 | } 71 | TestBrowser::Chrome => { 72 | // Tests must run in headless mode without a 73 | // sandbox (required for Travis CI). 74 | new.always_match( 75 | "goog:chromeOptions", json!({ 76 | "args": ["--no-sandbox", "--headless"], 77 | })) 78 | } 79 | }; 80 | 81 | new 82 | } 83 | } 84 | 85 | /// Tests defined in this macro are run once per browser. See the macro invocations below. 86 | macro_rules! browser_tests { 87 | ($mod_name:ident, $browser_type:expr) => { 88 | mod $mod_name { 89 | use super::*; 90 | fn test_browser() -> TestBrowser { $browser_type } 91 | 92 | #[test] 93 | fn navigation() { 94 | let (server, sess) = setup(); 95 | let page1 = server.url("/page1.html"); 96 | sess.go(&page1).expect("Error going to page1"); 97 | assert_eq!(&sess.get_current_url().expect("Error getting url [1]"), &page1, "Wrong URL [1]"); 98 | 99 | let page2 = server.url("/page2.html"); 100 | sess.go(&page2).expect("Error going to page2"); 101 | assert_eq!(&sess.get_current_url().expect("Error getting url [2]"), &page2, "Wrong URL [2]"); 102 | 103 | sess.back().expect("Error going back"); 104 | assert_eq!(&sess.get_current_url().expect("Error getting url [3]"), &page1, "Wrong URL [3]"); 105 | 106 | sess.forward().expect("Error going forward"); 107 | assert_eq!(&sess.get_current_url().expect("Error getting url [4]"), &page2, "Wrong URL [4]"); 108 | } 109 | 110 | #[test] 111 | fn title() { 112 | let (server, sess) = setup(); 113 | let page1 = server.url("/page1.html"); 114 | sess.go(&page1).expect("Error going to page1"); 115 | assert_eq!(&sess.get_title().expect("Error getting title"), "Test page 1 title", "Wrong title"); 116 | } 117 | 118 | #[test] 119 | fn get_page_source() { 120 | let (server, sess) = setup(); 121 | let page1 = server.url("/page1.html"); 122 | sess.go(&page1).expect("Error going to page1"); 123 | let page_source = sess.get_page_source().expect("Error getting page source"); 124 | 125 | if sess.browser_name() == Some("chrome") { 126 | // chrome sets the xmlns attribute in the html element 127 | assert!(page_source.contains(r#""#), "Want page_source to contain but was {}", page_source); 128 | } else { 129 | assert!(page_source.contains(""), "Want page_source to contain but was {}", page_source); 130 | } 131 | assert!(page_source.contains("Test page 1 title"), "Want page_source to contain Test page 1 title but was {}", page_source); 132 | } 133 | 134 | #[test] 135 | fn find_element_by_css() { 136 | let (server, sess) = setup(); 137 | let page1 = server.url("/page1.html"); 138 | sess.go(&page1).expect("Error going to page1"); 139 | let element = sess.find_element("span.red", LocationStrategy::Css).expect("Error finding element"); 140 | assert_eq!(element.text().expect("Error getting text"), "Red text", "Wrong element found"); 141 | 142 | sess.find_element("body.red", LocationStrategy::Css).expect_err("Want error"); 143 | } 144 | 145 | #[test] 146 | fn find_element_by_link_text() { 147 | let (server, sess) = setup(); 148 | let page1 = server.url("/page1.html"); 149 | sess.go(&page1).expect("Error going to page1"); 150 | let element = sess.find_element("A really handy WebDriver crate", LocationStrategy::LinkText).expect("Error finding element"); 151 | assert_eq!(element.text().expect("Error getting text"), "A really handy WebDriver crate", "Wrong element found"); 152 | 153 | sess.find_element("A link with this text does not appear on the page", LocationStrategy::LinkText).expect_err("Want error"); 154 | } 155 | 156 | #[test] 157 | fn find_element_by_partial_link_text() { 158 | let (server, sess) = setup(); 159 | let page1 = server.url("/page1.html"); 160 | sess.go(&page1).expect("Error going to page1"); 161 | let element = sess.find_element("crate", LocationStrategy::PartialLinkText).expect("Error finding element"); 162 | assert_eq!(element.text().expect("Error getting text"), "A really handy WebDriver crate", "Wrong element found"); 163 | 164 | sess.find_element("A link with this text does not appear on the page", LocationStrategy::PartialLinkText).expect_err("Want error"); 165 | } 166 | 167 | #[test] 168 | fn find_element_by_xpath() { 169 | let (server, sess) = setup(); 170 | let page1 = server.url("/page1.html"); 171 | sess.go(&page1).expect("Error going to page1"); 172 | let element = sess.find_element("//a", LocationStrategy::XPath).expect("Error finding element"); 173 | assert_eq!(element.text().expect("Error getting text"), "A really handy WebDriver crate", "Wrong element found"); 174 | 175 | sess.find_element("//video", LocationStrategy::XPath).expect_err("Want error"); 176 | } 177 | 178 | #[test] 179 | fn find_elements_by_css() { 180 | let (server, sess) = setup(); 181 | let page1 = server.url("/page1.html"); 182 | sess.go(&page1).expect("Error going to page1"); 183 | let elements = sess.find_elements("span.red", LocationStrategy::Css).expect("Error finding elements"); 184 | let element_texts: Vec = elements.into_iter().map(|elem| elem.text().expect("Error getting text")).collect(); 185 | assert_eq!(element_texts, vec!["Red text".to_owned(), "More red text".to_owned()], "Wrong element texts"); 186 | 187 | let found_elements = sess.find_elements("body.red", LocationStrategy::Css).expect("Error finding absent elements"); 188 | assert!(found_elements.is_empty(), "Want to find no elements, found {:?}", found_elements); 189 | } 190 | 191 | #[test] 192 | fn find_elements_by_link_text() { 193 | let (server, sess) = setup(); 194 | let page1 = server.url("/page1.html"); 195 | sess.go(&page1).expect("Error going to page1"); 196 | let elements = sess.find_elements("A really handy WebDriver crate", LocationStrategy::LinkText).expect("Error finding elements"); 197 | let element_texts: Vec = elements.into_iter().map(|elem| elem.text().expect("Error getting text")).collect(); 198 | assert_eq!(element_texts, vec!["A really handy WebDriver crate".to_owned()], "Wrong element texts"); 199 | 200 | let found_elements = sess.find_elements("A really bad WebDriver crate", LocationStrategy::LinkText).expect("Error finding absent elements"); 201 | assert!(found_elements.is_empty(), "Want to find no elements, found {:?}", found_elements); 202 | } 203 | 204 | #[test] 205 | fn find_elements_by_partial_link_text() { 206 | let (server, sess) = setup(); 207 | let page1 = server.url("/page1.html"); 208 | sess.go(&page1).expect("Error going to page1"); 209 | let elements = sess.find_elements("crate", LocationStrategy::PartialLinkText).expect("Error finding elements"); 210 | let element_texts: Vec = elements.into_iter().map(|elem| elem.text().expect("Error getting text")).collect(); 211 | assert_eq!(element_texts, vec!["A really handy WebDriver crate".to_owned(), "A WebDriver crate with just the server-side".to_owned()], "Wrong element texts"); 212 | 213 | let found_elements = sess.find_elements("A really bad WebDriver crate", LocationStrategy::PartialLinkText).expect("Error finding absent elements"); 214 | assert!(found_elements.is_empty(), "Want to find no elements, found {:?}", found_elements); 215 | } 216 | 217 | #[test] 218 | fn find_elements_by_xpath() { 219 | let (server, sess) = setup(); 220 | let page1 = server.url("/page1.html"); 221 | sess.go(&page1).expect("Error going to page1"); 222 | let elements = sess.find_elements("//body/span", LocationStrategy::XPath).expect("Error finding elements"); 223 | let element_texts: Vec = elements.into_iter().map(|elem| elem.text().expect("Error getting text")).collect(); 224 | assert_eq!(element_texts, vec!["Red text".to_owned(), "More red text".to_owned()], "Wrong element texts"); 225 | 226 | let found_elements = sess.find_elements("//video", LocationStrategy::XPath).expect("Error finding absent elements"); 227 | assert!(found_elements.is_empty(), "Want to find no elements, found {:?}", found_elements); 228 | } 229 | 230 | #[test] 231 | fn element_attribute_and_property() { 232 | let (server, sess) = setup(); 233 | let page1 = server.url("/page1.html"); 234 | let page2 = server.url("/page2.html"); 235 | 236 | sess.go(&page1).expect("Error going to page1"); 237 | let link = sess.find_element("#link_to_page_2", LocationStrategy::Css).expect("Error finding element"); 238 | 239 | assert_eq!(&link.attribute("href").expect("Error getting attribute"), 240 | "/page2.html"); 241 | 242 | if sess.browser_name() == Some("chrome") { 243 | // FIXME: chrome does not implement the property endpoint 244 | } else { 245 | assert_eq!(&link.property("href").expect("Error getting property"), &page2); 246 | } 247 | } 248 | 249 | #[test] 250 | fn element_css_value() { 251 | let (server, sess) = setup(); 252 | let page1 = server.url("/page1.html"); 253 | sess.go(&page1).expect("Error going to page1"); 254 | let element = sess.find_element("span.red", LocationStrategy::Css).expect("Error finding element"); 255 | if sess.browser_name() == Some("chrome") { 256 | assert_eq!(&element.css_value("color").expect("Error getting css value"), "rgba(255, 0, 0, 1)"); 257 | } else { 258 | assert_eq!(&element.css_value("color").expect("Error getting css value"), "rgb(255, 0, 0)"); 259 | } 260 | } 261 | 262 | #[test] 263 | fn element_click() { 264 | let (server, sess) = setup(); 265 | let page1 = server.url("/page1.html"); 266 | sess.go(&page1).expect("Error going to page1"); 267 | let output = sess.find_element("#set-text-output", LocationStrategy::Css) 268 | .expect("Finding output element"); 269 | assert_eq!(&output.text().expect("Getting output text"), "Unset"); 270 | let button = sess.find_element("#set-text-btn", LocationStrategy::Css) 271 | .expect("Finding button element"); 272 | button.click().expect("Click button"); 273 | assert_eq!(&output.text().expect("Getting output text"), "Set"); 274 | } 275 | 276 | #[test] 277 | fn element_clear() { 278 | let (server, sess) = setup(); 279 | let page1 = server.url("/page1.html"); 280 | sess.go(&page1).expect("Error going to page1"); 281 | let element = sess.find_element("#textfield", LocationStrategy::Css).expect("Error finding element"); 282 | assert_eq!(&element.property("value").expect("Error getting value [1]"), "Pre-filled"); 283 | element.clear().expect("Error clearing element"); 284 | assert_eq!(&element.property("value").expect("Error getting value [2]"), ""); 285 | } 286 | 287 | #[test] 288 | fn element_send_keys() { 289 | let (server, sess) = setup(); 290 | let page1 = server.url("/page1.html"); 291 | sess.go(&page1).expect("Error going to page1"); 292 | let element = sess.find_element("#textfield", LocationStrategy::Css).expect("Error finding element"); 293 | assert_eq!(&element.property("value").expect("Error getting value [1]"), 294 | "Pre-filled"); 295 | element.send_keys(" hello").expect("Error sending keys to element"); 296 | assert_eq!(&element.property("value").expect("Error getting value [2]"), "Pre-filled hello"); 297 | } 298 | 299 | #[test] 300 | fn element_text() { 301 | let (server, sess) = setup(); 302 | let page1 = server.url("/page1.html"); 303 | sess.go(&page1).expect("Error going to page1"); 304 | let element = sess.find_element("span.red", LocationStrategy::Css).expect("Error finding element"); 305 | assert_eq!(&element.text().expect("Error getting text"), "Red text"); 306 | } 307 | 308 | #[test] 309 | fn element_name() { 310 | let (server, sess) = setup(); 311 | let page1 = server.url("/page1.html"); 312 | sess.go(&page1).expect("Error going to page1"); 313 | let element = sess.find_element("span.red", LocationStrategy::Css).expect("Error finding element"); 314 | assert_eq!(&element.name().expect("Error getting name"), "span"); 315 | } 316 | 317 | #[test] 318 | fn element_child() { 319 | let (server, sess) = setup(); 320 | let page1 = server.url("/page1.html"); 321 | sess.go(&page1).expect("Error going to page1"); 322 | let element = sess.find_element("#parent", LocationStrategy::Css).expect("Error finding parent element"); 323 | 324 | let child_by_css = element.find_element("span", LocationStrategy::Css).expect("Error finding child by CSS"); 325 | assert_eq!(&child_by_css.attribute("id").expect("Error getting id [1]"), "child1"); 326 | 327 | let child_by_xpath = element.find_element(".//span", LocationStrategy::XPath).expect("Error finding child by XPath"); 328 | assert_eq!(&child_by_xpath.attribute("id").expect("Error getting id [2]"), "child1"); 329 | } 330 | 331 | #[test] 332 | fn element_children() { 333 | let (server, sess) = setup(); 334 | let page1 = server.url("/page1.html"); 335 | sess.go(&page1).expect("Error going to page1"); 336 | let element = sess.find_element("#parent", LocationStrategy::Css).expect("Error finding parent element"); 337 | 338 | let children = element.find_elements("span", LocationStrategy::Css).expect("Error finding children by CSS"); 339 | assert_eq!(children.iter().map(|e| e.attribute("id").expect("Error getting id")).collect::>(), vec!["child1".to_owned(), "child2".to_owned()]); 340 | } 341 | 342 | #[test] 343 | fn refresh() { 344 | let (server, sess) = setup(); 345 | let page1 = server.url("/page1.html"); 346 | sess.go(&page1).expect("Error going to page1"); 347 | let elem = sess.find_element("#textfield", LocationStrategy::Css).expect("Error finding element [1]"); 348 | assert_eq!(elem.property("value").expect("Error getting value [1]"), "Pre-filled".to_owned()); 349 | 350 | elem.clear().expect("Error clearing"); 351 | assert_eq!(elem.property("value").expect("Error getting value [1]"), "".to_owned()); 352 | 353 | sess.refresh().expect("Error refreshing"); 354 | elem.text().expect_err("Want stale element error"); 355 | let elem2 = sess.find_element("#textfield", LocationStrategy::Css).expect("Error finding element [1]"); 356 | assert_eq!(elem2.property("value").expect("Error getting value [2]"), "Pre-filled".to_owned()); 357 | } 358 | 359 | #[test] 360 | fn execute() { 361 | let (server, sess) = setup(); 362 | let page1 = server.url("/page1.html"); 363 | sess.go(&page1).expect("Error going to page1"); 364 | let exec_json = sess.execute(ExecuteCmd { 365 | script: "return arguments[0] + arguments[1];".to_owned(), 366 | args: vec![json!(1), json!(2)], 367 | }).expect("Error executing script"); 368 | assert_eq!(serde_json::from_value::(exec_json).expect("Error converting result to i64"), 3); 369 | 370 | let exec_error = sess.execute(ExecuteCmd { 371 | script: "throw 'foo';".to_owned(), 372 | args: vec![], 373 | }).expect_err("Want error"); 374 | 375 | // FIXME: Chrome represents errors differently 376 | if sess.browser_name() != Some("chrome") { 377 | match exec_error { 378 | webdriver_client::Error::WebDriverError(err) => assert!(format!("{:?}", err).contains("foo"), "Bad error message: {:?}", err), 379 | other => panic!("Wrong error type: {:?}", other), 380 | }; 381 | } 382 | 383 | } 384 | 385 | #[test] 386 | fn browser_name() { 387 | let sess = test_browser().session(); 388 | match test_browser() { 389 | TestBrowser::Firefox => assert_eq!(sess.browser_name(), Some("firefox")), 390 | TestBrowser::Chrome => assert_eq!(sess.browser_name(), Some("chrome")), 391 | } 392 | } 393 | 394 | #[test] 395 | fn execute_async() { 396 | let (server, sess) = setup(); 397 | let page1 = server.url("/page1.html"); 398 | sess.go(&page1).expect("Error going to page1"); 399 | let exec_json = sess.execute_async(ExecuteCmd { 400 | script: "setTimeout(() => arguments[1](arguments[0]), 1000);".to_owned(), 401 | args: vec![json!(1)], 402 | }).unwrap(); 403 | let exec_int = serde_json::from_value::(exec_json).unwrap(); 404 | assert_eq!(exec_int, 1); 405 | } 406 | 407 | // TODO: Test cookies 408 | 409 | // TODO: Test window handles 410 | 411 | #[test] 412 | fn frame_switch() { 413 | let (server, sess) = setup(); 414 | let page3 = server.url("/page3.html"); 415 | sess.go(&page3).expect("Error going to page3"); 416 | 417 | // switching to parent from parent is harmless 418 | sess.switch_to_parent_frame().unwrap(); 419 | 420 | let frames = sess.find_elements("iframe", LocationStrategy::Css).unwrap(); 421 | assert_eq!(frames.len(), 1); 422 | 423 | sess.switch_to_frame(frames[0].reference().unwrap()).unwrap(); 424 | let frames = sess.find_elements("iframe", LocationStrategy::Css).unwrap(); 425 | assert_eq!(frames.len(), 2); 426 | 427 | for f in &frames { 428 | sess.switch_to_frame(f.reference().unwrap()).unwrap(); 429 | let childframes = sess.find_elements("iframe", LocationStrategy::Css).unwrap(); 430 | assert_eq!(childframes.len(), 0); 431 | sess.switch_to_parent_frame().unwrap(); 432 | } 433 | 434 | sess.switch_to_parent_frame().unwrap(); 435 | let frames = sess.find_elements("iframe", LocationStrategy::Css).unwrap(); 436 | assert_eq!(frames.len(), 1); 437 | } 438 | 439 | #[test] 440 | fn http_driver() { 441 | ensure_logging_init(); 442 | 443 | let driver = test_browser().driver(); 444 | 445 | // Hackily sleep a bit until geckodriver is ready, otherwise our session 446 | // will fail to connect. 447 | // If this is unreliable, we could try: 448 | // * Polling for the TCP port to become unavailable. 449 | // * Wait for geckodriver to log "Listening on 127.0.0.1:4444". 450 | sleep(Duration::from_millis(1000)); 451 | 452 | let http_driver = HttpDriverBuilder::default() 453 | .url(driver.url()) 454 | .build().unwrap(); 455 | 456 | let sess = http_driver.session(&test_browser().new_session_cmd()) 457 | .unwrap(); 458 | 459 | let server = FileServer::new(); 460 | let test_url = server.url("/page1.html"); 461 | sess.go(&test_url).unwrap(); 462 | let url = sess.get_current_url().unwrap(); 463 | assert_eq!(url, test_url); 464 | } 465 | 466 | #[test] 467 | fn screenshot_frame() { 468 | let (server, sess) = setup(); 469 | let page1 = server.url("/page1.html"); 470 | sess.go(&page1).expect("Error going to page1"); 471 | let ss = sess.screenshot().expect("Screenshot"); 472 | std::fs::create_dir_all("target/screenshots").expect("Create screenshot dir"); 473 | ss.save_file(&format!("target/screenshots/{:?}_frame.png", 474 | test_browser())) 475 | .expect("Save screenshot"); 476 | } 477 | 478 | #[test] 479 | fn screenshot_element() { 480 | let (server, sess) = setup(); 481 | let page1 = server.url("/page1.html"); 482 | sess.go(&page1).expect("Error going to page1"); 483 | let ss = sess.find_element("#parent", LocationStrategy::Css).expect("element") 484 | .screenshot().expect("Screenshot"); 485 | std::fs::create_dir_all("target/screenshots").expect("Create screenshot dir"); 486 | ss.save_file(&format!("target/screenshots/{:?}_element.png", 487 | test_browser())) 488 | .expect("Save screenshot"); 489 | } 490 | 491 | #[test] 492 | fn dismiss_alert() { 493 | let (server, sess) = setup(); 494 | let page1 = server.url("/page1.html"); 495 | sess.go(&page1).expect("Error going to page1"); 496 | let btn = sess.find_element("#alert-btn", LocationStrategy::Css).expect("btn"); 497 | btn.click().expect("click"); 498 | sess.dismiss_alert().expect("dismiss alert"); 499 | } 500 | 501 | #[test] 502 | fn accept_confirm_alert() { 503 | let (server, sess) = setup(); 504 | let page1 = server.url("/page1.html"); 505 | sess.go(&page1).expect("Error going to page1"); 506 | let btn = sess.find_element("#confirm-btn", LocationStrategy::Css) 507 | .expect("find btn"); 508 | btn.click().expect("click"); 509 | sess.accept_alert().expect("accept alert"); 510 | let out = sess.find_element("#alerts-out", LocationStrategy::Css) 511 | .expect("find output"); 512 | assert_eq!("true", out.text().expect("output text")); 513 | } 514 | 515 | #[test] 516 | fn dismiss_confirm_alert() { 517 | let (server, sess) = setup(); 518 | let page1 = server.url("/page1.html"); 519 | sess.go(&page1).expect("Error going to page1"); 520 | let btn = sess.find_element("#confirm-btn", LocationStrategy::Css) 521 | .expect("find btn"); 522 | btn.click().expect("click"); 523 | sess.dismiss_alert().expect("accept alert"); 524 | let out = sess.find_element("#alerts-out", LocationStrategy::Css) 525 | .expect("find output"); 526 | assert_eq!("false", out.text().expect("output text")); 527 | } 528 | 529 | #[test] 530 | fn get_alert_text() { 531 | let (server, sess) = setup(); 532 | let page1 = server.url("/page1.html"); 533 | sess.go(&page1).expect("Error going to page1"); 534 | let btn = sess.find_element("#alert-btn", LocationStrategy::Css) 535 | .expect("find btn"); 536 | btn.click().expect("click"); 537 | assert_eq!("Alert", sess.get_alert_text().expect("get_alert_text")); 538 | sess.dismiss_alert().expect("accept alert"); 539 | } 540 | 541 | #[test] 542 | fn send_alert_text() { 543 | let (server, sess) = setup(); 544 | let page1 = server.url("/page1.html"); 545 | sess.go(&page1).expect("Error going to page1"); 546 | let btn = sess.find_element("#prompt-btn", LocationStrategy::Css) 547 | .expect("find btn"); 548 | btn.click().expect("click"); 549 | sess.send_alert_text("foobar").expect("send_alert_text"); 550 | sess.accept_alert().expect("accept alert"); 551 | let out = sess.find_element("#alerts-out", LocationStrategy::Css) 552 | .expect("find output"); 553 | assert_eq!("foobar", out.text().expect("output text")); 554 | } 555 | 556 | fn setup() -> (FileServer, DriverSession) { 557 | ensure_logging_init(); 558 | 559 | let session = test_browser().session(); 560 | 561 | let server = FileServer::new(); 562 | 563 | (server, session) 564 | } 565 | 566 | // End of browser_tests tests 567 | } 568 | } 569 | } 570 | 571 | browser_tests!(firefox, TestBrowser::Firefox); 572 | browser_tests!(chrome, TestBrowser::Chrome); 573 | 574 | fn ensure_logging_init() { 575 | static DONE: Once = Once::new(); 576 | DONE.call_once(|| init_logging()); 577 | } 578 | 579 | fn init_logging() { 580 | let mut builder = LogBuilder::new(); 581 | builder.filter(None, LogLevelFilter::Info); 582 | builder.target(LogTarget::Stdout); 583 | 584 | if let Ok(ev) = env::var("RUST_LOG") { 585 | builder.parse(&ev); 586 | } 587 | 588 | builder.init().unwrap(); 589 | } 590 | 591 | struct FileServer { 592 | listening: hyper::server::Listening, 593 | base_url: String, 594 | } 595 | 596 | impl FileServer { 597 | pub fn new() -> FileServer { 598 | for i in 0..2000 { 599 | let port = 8000 + i; 600 | let base_url = format!("http://localhost:{}", port); 601 | let server = match hyper::Server::http(("localhost", port)) { 602 | Ok(server) => server, 603 | Err(_) => { 604 | continue; 605 | }, 606 | }; 607 | match server.handle_threads(FileServer::handle, 10) { 608 | Ok(listening) => { 609 | return FileServer { 610 | listening, 611 | base_url, 612 | }; 613 | }, 614 | Err(err) => panic!("Error listening: {:?}", err), 615 | } 616 | } 617 | panic!("Could not find free port to serve test pages") 618 | } 619 | 620 | pub fn url(&self, path: &str) -> String { 621 | format!("{base_url}{path}", base_url = self.base_url, path = path) 622 | } 623 | 624 | fn handle(req: hyper::server::Request, mut resp: hyper::server::Response) { 625 | match FileServer::handle_impl(&req) { 626 | Ok(bytes) => { 627 | *resp.status_mut() = hyper::status::StatusCode::Ok; 628 | resp.send(&bytes).expect("Failed to send HTTP response"); 629 | }, 630 | Err(err) => { 631 | eprintln!("{}", err); 632 | *resp.status_mut() = hyper::status::StatusCode::BadRequest; 633 | }, 634 | }; 635 | } 636 | 637 | fn handle_impl(req: &hyper::server::Request) -> Result, String> { 638 | let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 639 | let www_root = crate_root.join("tests").join("www"); 640 | 641 | match req.uri { 642 | hyper::uri::RequestUri::AbsolutePath(ref path) => { 643 | if path.starts_with("/") { 644 | let abs_path = www_root.join(&path[1..]); 645 | let file_path = std::fs::canonicalize(&abs_path); 646 | match file_path { 647 | Ok(realpath) => { 648 | if realpath.starts_with(&www_root) { 649 | let mut contents = Vec::new(); 650 | std::fs::File::open(&realpath) 651 | .and_then(|mut f| f.read_to_end(&mut contents)) 652 | .map_err(|err| format!("Error reading file {:?}: {:?}", realpath, err))?; 653 | return Ok(contents); 654 | } else { 655 | return Err(format!("Rejecting request for path outside of www: {:?}", realpath)); 656 | } 657 | }, 658 | Err(err) => { 659 | return Err(format!("Error canonicalizing file {:?}: {:?}", abs_path, err)); 660 | }, 661 | 662 | } 663 | } else { 664 | return Err(format!("Received bad request for path {:?}", path)); 665 | } 666 | }, 667 | ref path => { 668 | return Err(format!("Received request for non-AbsolutePath: {:?}", path)); 669 | }, 670 | } 671 | } 672 | } 673 | 674 | impl Drop for FileServer { 675 | fn drop(&mut self) { 676 | self.listening.close().expect("FileServer failed to stop listening"); 677 | } 678 | } 679 | -------------------------------------------------------------------------------- /tests/integration_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page title 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/www/favicon.ico: -------------------------------------------------------------------------------- 1 | # A dummy file to prevent a 404 during tests that makes test output noisier. -------------------------------------------------------------------------------- /tests/www/inner_frame.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /tests/www/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page 1 title 5 | 10 | 11 | 12 | Red text 13 | A really handy WebDriver crate 14 | More red text 15 | A WebDriver crate with just the server-side 16 | 17 | Page 2 18 |
19 | Outer: 20 | Inner 21 |
22 | Other inner 23 |
24 |

25 | 26 | Unset 27 |

28 |

29 |

Alerts

30 | 31 | 32 | 33 |
34 |

35 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/www/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page 2 title 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/www/page3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test page title 5 | 6 | 7 | 8 | 9 | 10 | --------------------------------------------------------------------------------