├── .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 | [](https://crates.io/crates/webdriver_client)
6 |
7 | [](https://docs.rs/webdriver_client)
8 |
9 | Source code and issues on GitHub:
10 | [][github]
11 |
12 | [github]: https://github.com/fluffysquirrels/webdriver_client_rust
13 |
14 | CI build on Travis CI: [](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: [][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 | [][crates]
25 | and
26 | [][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 |
--------------------------------------------------------------------------------