Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mozilla/geckodriver.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjgraham <james@hoppipolla.co.uk>2020-07-29 11:38:21 +0300
committerGitHub <noreply@github.com>2020-07-29 11:38:21 +0300
commit917daaccc78bb8cac3ab324861ada48d68746d13 (patch)
treefccb6b8f51b4eb59c820ad96156ff5e21d2ab434
parent92ed2d8b2cdb6788dc56f5688b506b6467525f41 (diff)
Import of v0.27.0 (#1752)v0.27.0
Co-authored-by: Maja Frydrychowicz <mjzffr@gmail.com>
-rw-r--r--CHANGES.md265
-rw-r--r--Cargo.toml26
-rw-r--r--README.md33
-rw-r--r--build.rs10
-rw-r--r--doc/ARM.md39
-rw-r--r--doc/Building.md4
-rw-r--r--doc/Capabilities.md170
-rw-r--r--doc/Notarization.md41
-rw-r--r--doc/Profiles.md108
-rw-r--r--doc/Releasing.md146
-rw-r--r--doc/Support.md43
-rw-r--r--doc/Testing.md4
-rw-r--r--doc/TraceLogs.md2
-rw-r--r--doc/Usage.md2
-rw-r--r--doc/index.rst14
-rw-r--r--mach_commands.py30
-rw-r--r--marionette/Cargo.toml10
-rw-r--r--marionette/src/common.rs246
-rw-r--r--marionette/src/error.rs180
-rw-r--r--marionette/src/lib.rs10
-rw-r--r--marionette/src/marionette.rs65
-rw-r--r--marionette/src/message.rs332
-rw-r--r--marionette/src/result.rs219
-rw-r--r--marionette/src/test.rs31
-rw-r--r--marionette/src/webdriver.rs452
-rw-r--r--moz.build8
-rw-r--r--src/android.rs376
-rw-r--r--src/build.rs5
-rw-r--r--src/capabilities.rs566
-rw-r--r--src/command.rs216
-rw-r--r--src/logging.rs7
-rw-r--r--src/main.rs325
-rw-r--r--src/marionette.rs1228
-rw-r--r--src/prefs.rs16
-rw-r--r--src/test.rs15
35 files changed, 4103 insertions, 1141 deletions
diff --git a/CHANGES.md b/CHANGES.md
index 0dd7731..b1f76ce 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,14 +4,231 @@ Change log
All notable changes to this program is documented in this file.
-0.24.0 [917474f3473e] (2018-01-28)
+0.27.0 (2020-07-27, `90ec81285ff6`)
+--------------------
+
+### Known problems
+
+- _macOS 10.15 (Catalina):_
+
+ Due to the requirement from Apple that all programs must be
+ notarized, geckodriver will not work on Catalina if you manually
+ download it through another notarized program, such as Firefox.
+
+ Whilst we are working on a repackaging fix for this problem, you
+ can find more details on how to work around this issue in the
+ [macOS notarization] section of the documentation.
+
+### Added
+
+- To set environment variables for the launched Firefox for Android,
+ it is now possible to add an `env` object on `moz:firefoxOptions`
+ (note: this is not supported for Firefox Desktop)
+
+- Support for print-to-PDF
+
+ The newly standardised WebDriver [Print] endpoint provides a way to
+ render pages to a paginated PDF representation. This endpoint is
+ supported by geckodriver when using Firefox version ≥78.
+
+- Support for same-site cookies
+
+ Cookies can now be set with a `same-site` parameter, and the value
+ of that parameter will be returned when cookies are
+ retrieved. Requires Firefox version ≥79. Thanks to [Peter Major] for
+ the patch.
+
+### Fixed
+
+- _Android:_
+
+ * Firefox running on Android devices can now be controlled from a
+ Windows host.
+
+ * Setups with multiple connected Android devices are now supported.
+
+ * Improved cleanup of configuration files. This prevents crashes if
+ the application is started manually after launching it through
+ geckodriver.
+
+- Windows and Linux binaries are again statically linked.
+
+0.26.0 (2019-10-12, `e9783a644016'`)
------------------------------------
+Note that with this release the minimum recommended Firefox version
+has changed to Firefox ≥60.
+
+### Known problems
+
+- _macOS 10.15 (Catalina):_
+
+ Due to the recent requirement from Apple that all programs must
+ be notarized, geckodriver will not work on Catalina if you manually
+ download it through another notarized program, such as Firefox.
+
+ Whilst we are working on a repackaging fix for this problem, you
+ can find more details on how to work around this issue in the
+ [macOS notarization] section of the documentation.
+
+- _Windows:_
+
+ You must still have the [Microsoft Visual Studio redistributable
+ runtime] installed on your system for the binary to run. This
+ is a known bug which we weren't able fix for this release.
+
+### Added
+
+- Support for Firefox on Android
+
+ Starting with this release geckodriver is able to connect to
+ Firefox on Android systems, and to control packages based on
+ [GeckoView].
+
+ Support for Android works by the geckodriver process running on
+ a host system and Firefox running within either an emulator or
+ on a physical device connected to the host system. This requires
+ you to first [enable remote debugging on the Android device].
+
+ The WebDriver client must set the [`platformName` capability] to
+ "`android`" and the `androidPackage` capability within
+ [`moz:firefoxOptions`] to the Android package name of the Firefox
+ application.
+
+ The full list of new capabilities specific to Android, instructions
+ how to use them, and examples can be found in the [`moz:firefoxOptions`]
+ documentation on MDN.
+
+ When the session is created, the `platformName` capability will
+ return "`android`" instead of reporting the platform of the host
+ system.
+
+### Changed
+
+- Continued Marionette refactoring changes
+
+ 0.25.0 came with a series of internal changes for how geckodriver
+ communicates with Firefox over the Marionette protocol. This
+ release contains the second half of the refactoring work.
+
+### Fixed
+
+- Connection attempts to Firefox made more reliable
+
+ geckodriver now waits for the Marionette handshake before assuming
+ the session has been established. This should improve reliability
+ in creating new WebDriver sessions.
+
+- Corrected error codes used during session creation
+
+ When a new session was being configured with invalid input data,
+ the error codes returned was not always consistent. Attempting
+ to start a session with a malformed capabilities configuration
+ will now return the [`invalid argument`] error consistently.
+
+
+0.25.0 (2019-09-09, `bdb64cf16b68`)
+-----------------------------------
+
+__Note to Windows users!__
+With this release you must have the [Microsoft Visual Studio redistributable runtime]
+installed on your system for the binary to run.
+This is a [known bug](https://github.com/mozilla/geckodriver/issues/1617)
+with this particular release that we intend to release a fix for soon.
+
+### Added
+
+- Added support for HTTP `HEAD` requests to the HTTPD
+
+ geckodriver now responds correctly to HTTP `HEAD` requests,
+ which can be used for probing whether it supports a particular API.
+
+ Thanks to [Bastien Orivel] for this patch.
+
+- Added support for searching for Nightly’s default path on macOS
+
+ If the location of the Firefox binary is not given, geckodriver
+ will from now also look for the location of Firefox Nightly in
+ the default locations. The ordered list of search paths on macOS
+ is as follows:
+
+ 1. `/Applications/Firefox.app/Contents/MacOS/firefox-bin`
+ 2. `$HOME/Applications/Firefox.app/Contents/MacOS/firefox-bin`
+ 3. `/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin`
+ 4. `$HOME/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin`
+
+ Thanks to [Kriti Singh] for this patch.
+
+- Support for application bundle paths on macOS
+
+ It is now possible to pass an application bundle path, such as
+ `/Applications/Firefox.app` as argument to the `binary` field in
+ [`moz:firefoxOptions`]. This will be automatically resolved to
+ the absolute path of the binary when Firefox is started.
+
+ Thanks to [Nupur Baghel] for this patch.
+
+- macOS and Windows builds are signed
+
+ With this release of geckodriver, executables for macOS and Windows
+ are signed using the same certificate key as Firefox. This should
+ help in cases where geckodriver previously got misidentified as
+ a virus by antivirus software.
+
+### Removed
+
+- Dropped support for legacy Selenium web element references
+
+ The legacy way of serialising web elements, using `{"ELEMENT": <UUID>}`,
+ has been removed in this release. This may break older Selenium
+ clients and clients which are otherwise not compatible with the
+ WebDriver standard.
+
+ Thanks to [Shivam Singhal] for this patch.
+
+- Removed `--webdriver-port` command-line option
+
+ `--webdriver-port <PORT>` was an undocumented alias for `--port`,
+ initially used for backwards compatibility with clients
+ prior to Selenium 3.0.0.
+
+### Changed
+
+- Refactored Marionette serialisation
+
+ Much of geckodriver’s internal plumbing for serialising WebDriver
+ requests to Marionette messages has been refactored to decrease
+ the amount of manual lifting.
+
+ This work should have no visible side-effects for users.
+
+ Thanks to [Nupur Baghel] for working on this throughout her
+ Outreachy internship at Mozilla.
+
+- Improved error messages for incorrect command-line usage
+
+### Fixed
+
+- Errors related to incorrect command-line usage no longer hidden
+
+ By mistake, earlier versions of geckodriver failed to print incorrect
+ flag use. With this release problems are again written to stderr.
+
+- Search system path for Firefox binary on BSDs
+
+ geckodriver would previously only search the system path for the
+ `firefox` binary on Linux. Now it supports different BSD flavours
+ as well.
+
+
+0.24.0 (2019-01-28, `917474f3473e`)
+-----------------------------------
+
### Added
- Introduces `strictFileInteractability` capability
- The new capabilitiy indicates if strict interactability checks
+ The new capability indicates if strict interactability checks
should be applied to `<input type=file>` elements. As strict
interactability checks are off by default, there is a change
in behaviour when using [Element Send Keys] with hidden file
@@ -20,12 +237,13 @@ All notable changes to this program is documented in this file.
- Added new endpoint `GET /session/{session id}/moz/screenshot/full`
for taking full document screenshots, thanks to Greg Fraley.
-- Added new `--marionette-host <HOSTNAME>` flag for binding to a
+- Added new `--marionette-host <hostname>` flag for binding to a
particular interface/IP layer on the system.
- Added new endpoint `POST /session/{session_id}/window/new`
for the [New Window] command to create a new top-level browsing
- context, which can be either a window or a tab.
+ context, which can be either a window or a tab. The first version
+ of Firefox supporting this command is Firefox 66.0.
- When using the preference `devtools.console.stdout.content` set to
`true` logging of console API calls like `info()`, `warn()`, and
@@ -37,8 +255,15 @@ All notable changes to this program is documented in this file.
### Removed
-- Turned off builds for arm7hf, which will no longer be released but
- can still be built from the source.
+- ARMv7 HF builds have been discontinued
+
+ We [announced](https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html)
+ back in September 2018 that we would stop building for ARM,
+ but builds can be self-serviced by building from source.
+
+ To cross-compile from another host system, you can use this command:
+
+ % cargo build --target armv7-unknown-linux-gnueabihf
### Changed
@@ -199,7 +424,7 @@ to the standard.
[Jeremy Lempereur].
- Many documentation improvements, now published on
- https://firefox-source-docs.mozilla.org/testing/geckodriver/geckodriver/.
+ https://firefox-source-docs.mozilla.org/testing/geckodriver/.
0.21.0 (2018-06-15)
@@ -555,7 +780,7 @@ and greater.
- Disable Flash and the plugin container in Firefox by
default, which should help mitigate the “Plugin Container
- for Firefox has stopped wroking” problems [many users were
+ for Firefox has stopped working” problems [many users were
reporting](https://github.com/mozilla/geckodriver/issues/225) when
deleting a session
@@ -564,7 +789,7 @@ and greater.
[Marc Fisher](https://github.com/DrMarcII))
- The exceptions are the `marionette.port` and `marionette.log.level`
preferences and their fallbacks, which are set unconditionally and
- cannot be overriden
+ cannot be overridden
- Remove default preference that disables unsafe CPOW checks
@@ -573,7 +798,7 @@ and greater.
### Fixed
- Fix for the “corrupt deflate stream” exception that
- sometimes occured when trying to write an empty profile by
+ sometimes occurred when trying to write an empty profile by
[@kirhgoph](https://github.com/kirhgoph)
- Recognise `sslProxy` and `sslProxyPort` entries in the proxy
@@ -617,7 +842,7 @@ and greater.
- Now uses about:blank as the new tab document; this was previously
disabled due to [bug 1333736](https://bugzil.la/1333736) in Marionette
-- WebDriver libary updated to 0.23.0
+- WebDriver library updated to 0.23.0
### Fixed
@@ -726,7 +951,7 @@ and greater.
### Added
-- Introduced continous integration builds for Linux- and Windows 32-bit
+- Introduced continuous integration builds for Linux- and Windows 32-bit
binaries
- Added commands for setting- and getting the window position
@@ -1024,7 +1249,7 @@ and greater.
- Make failing to communicate with Firefox a fatal error that closes
the session
-- Shut down session only when loosing connection
+- Shut down session only when losing connection
- Better handling of missing command line flags
@@ -1096,6 +1321,14 @@ and greater.
[README]: https://github.com/mozilla/geckodriver/blob/master/README.md
[Browser Toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox
[WebDriver conformance]: https://wpt.fyi/results/webdriver/tests?label=experimental
+[`moz:firefoxOptions`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
+[Microsoft Visual Studio redistributable runtime]: https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads
+[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView
+[Firefox Preview]: https://play.google.com/store/apps/details?id=org.mozilla.fenix
+[Firefox Reality]: https://play.google.com/store/apps/details?id=org.mozilla.vrbrowser
+[Capabilities]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
+[enable remote debugging on the Android device]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging
+[macOS notarization]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Notarization.html
[`CloseWindowResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CloseWindowResponse.html
[`CookieResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CookieResponse.html
@@ -1127,6 +1360,7 @@ and greater.
[script timeout]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/ScriptTimeout
[timeout]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/Timeout
[timeout object]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Timeouts
+[`platformName` capability]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities#platformName
[hyper]: https://hyper.rs/
[mozrunner crate]: https://crates.io/crates/mozrunner
@@ -1142,6 +1376,7 @@ and greater.
[Minimize Window]: https://w3c.github.io/webdriver/webdriver-spec.html#minimize-window
[New Session]: https://w3c.github.io/webdriver/webdriver-spec.html#new-session
[New Window]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/New_Window
+[Print]: https://w3c.github.io/webdriver/webdriver-spec.html#print
[Send Alert Text]: https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text
[Set Timeouts]: https://w3c.github.io/webdriver/webdriver-spec.html#set-timeouts
[Set Window Rect]: https://w3c.github.io/webdriver/webdriver-spec.html#set-window-rect
@@ -1154,6 +1389,10 @@ and greater.
[Jeremy Lempereur]: https://github.com/o0Ignition0o
[Joshua Bruning]: https://github.com/joshbruning
[Kalpesh Krishna]: https://github.com/martiansideofthemoon
+[Kriti Singh]: https://github.com/kritisingh1
[Mike Pennisi]: https://github.com/jugglinmike
+[Nupur Baghel]: https://github.com/nupurbaghel
+[Peter Major]: https://github.com/aldaris
+[Shivam Singhal]: https://github.com/championshuttler
[Sven Jost]: https://github/mythsunwind
[Vlad Filippov]: https://github.com/vladikoff
diff --git a/Cargo.toml b/Cargo.toml
index b5232c0..7a1ebdb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,30 +1,34 @@
[package]
name = "geckodriver"
-version = "0.24.0"
+version = "0.27.0"
description = "Proxy for using WebDriver clients to interact with Gecko-based browsers."
keywords = ["webdriver", "w3c", "httpd", "mozilla", "firefox"]
repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver"
readme = "README.md"
license = "MPL-2.0"
publish = false
+edition = "2018"
[dependencies]
-base64 = "0.10"
+base64 = "0.12"
chrono = "0.4.6"
clap = { version = "^2.19", default-features = false, features = ["suggestions", "wrap_help"] }
-hyper = "0.12"
+hyper = "0.13"
lazy_static = "1.0"
log = { version = "0.4", features = ["std"] }
-mozprofile = "0.5.0"
-mozrunner = "0.9.0"
-mozversion = "0.2.0"
-regex = "1.0"
+marionette = { path = "./marionette" }
+mozdevice = "0.2.0"
+mozprofile = "0.7.0"
+mozrunner = "0.11"
+mozversion = "0.3"
+regex = { version="1.0", default-features = false, features = ["perf", "std"] }
serde = "1.0"
-serde_json = "1.0"
serde_derive = "1.0"
-uuid = { version = "0.6", features = ["v4"] }
-webdriver = "0.39.0"
-zip = "0.4"
+serde_json = "1.0"
+serde_yaml = "0.8"
+uuid = { version = "0.8", features = ["v4"] }
+webdriver = "0.41"
+zip = { version = "0.4", default-features = false, features = ["deflate"] }
[[bin]]
name = "geckodriver"
diff --git a/README.md b/README.md
index 078fac0..78e4414 100644
--- a/README.md
+++ b/README.md
@@ -4,18 +4,13 @@ geckodriver
Proxy for using W3C [WebDriver] compatible clients to interact with
Gecko-based browsers.
-This program provides the HTTP API described by the [WebDriver protocol]
-to communicate with Gecko browsers, such as Firefox. It translates calls
-into the [Firefox remote protocol] by acting as a proxy between the local-
-and remote ends.
-
-geckodriver’s [source code] is made available under the [Mozilla
-Public License].
+This program provides the HTTP API described by the [WebDriver
+protocol] to communicate with Gecko browsers, such as Firefox. It
+translates calls into the [Marionette remote protocol] by acting
+as a proxy between the local- and remote ends.
[WebDriver protocol]: https://w3c.github.io/webdriver/#protocol
-[Firefox remote protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html
-[source code]: https://hg.mozilla.org/mozilla-unified/file/tip/testing/geckodriver
-[Mozilla Public License]: https://www.mozilla.org/en-US/MPL/2.0/
+[Marionette remote protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/
[WebDriver]: https://developer.mozilla.org/en-US/docs/Web/WebDriver
@@ -52,17 +47,23 @@ Documentation
* [Analyzing crash data from Firefox](https://firefox-source-docs.mozilla.org/testing/geckodriver/CrashReports.html)
* [Contributing](https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers)
+ * [Building](https://firefox-source-docs.mozilla.org/testing/geckodriver/Building.html)
+ * [Testing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Testing.html)
+ * [Releasing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Releasing.html)
+ * [Self-serving an ARM build](https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html)
Source code
-----------
-geckodriver’s canonical source code can be found in [mozilla-central].
-We only use this GitHub repository for issue tracking and making releases.
-See our [contribution documentation] for more information.
+geckodriver is made available under the [Mozilla Public License].
+Its source code can be found in [mozilla-central] under testing/geckodriver.
+This GitHub repository is only used for issue tracking and making releases.
+
+[source code]: https://hg.mozilla.org/mozilla-unified/file/tip/testing/geckodriver
+[Mozilla Public License]: https://www.mozilla.org/en-US/MPL/2.0/
[mozilla-central]: https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver
-[contribution documentation]: https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers
Contact
@@ -71,8 +72,8 @@ Contact
The mailing list for geckodriver discussion is
tools-marionette@lists.mozilla.org ([subscribe], [archive]).
-There is also an IRC channel to talk about using and developing
-geckodriver in #interop on irc.mozilla.org.
+There is also a Matrix channel to talk about using and developing
+geckodriver on `#interop:mozilla.org <https://chat.mozilla.org/#/room/#interop:mozilla.org>`__
[subscribe]: https://lists.mozilla.org/listinfo/tools-marionette
[archive]: https://lists.mozilla.org/pipermail/tools-marionette/
diff --git a/build.rs b/build.rs
index 8e2e8b0..58d2726 100644
--- a/build.rs
+++ b/build.rs
@@ -37,7 +37,7 @@ fn main() -> io::Result<()> {
Ok(())
}
-fn get_build_info(dir: &Path) -> Box<BuildInfo> {
+fn get_build_info(dir: &Path) -> Box<dyn BuildInfo> {
if Path::exists(&dir.join(".hg")) {
Box::new(Hg {})
} else if Path::exists(&dir.join(".git")) {
@@ -71,7 +71,7 @@ impl Hg {
.output()
.ok()
.and_then(|r| String::from_utf8(r.stdout).ok())
- .map(|s| s.trim_right().into())
+ .map(|s| s.trim_end().into())
}
}
@@ -99,7 +99,7 @@ impl Git {
.output()
.ok()
.and_then(|r| String::from_utf8(r.stdout).ok())
- .map(|s| s.trim_right().into())
+ .map(|s| s.trim_end().into())
}
fn to_hg_sha(&self, git_sha: String) -> Option<String> {
@@ -112,8 +112,8 @@ impl BuildInfo for Git {
self.exec(&["rev-parse", "HEAD"])
.and_then(|sha| self.to_hg_sha(sha))
.map(|mut s| {
- s.truncate(12);
- s
+ s.truncate(12);
+ s
})
}
diff --git a/doc/ARM.md b/doc/ARM.md
new file mode 100644
index 0000000..5627cbd
--- /dev/null
+++ b/doc/ARM.md
@@ -0,0 +1,39 @@
+Self-serving an ARM build
+=========================
+
+Mozilla [announced the intent] to deprecate ARMv7 HF builds of
+geckodriver in September 2018. This does not mean you can no longer
+use geckodriver on ARM systems, and this document explains how you
+can self-service a build for ARMv7 HF.
+
+Assuming you have already checked out [central], the steps to
+cross-compile ARMv7 from a Linux host system is as follows:
+
+ 1. If you don’t have Rust installed:
+
+ # curl https://sh.rustup.rs -sSf | sh
+
+ 2. Install cross-compiler toolchain:
+
+ # apt install gcc-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross
+
+ 3. Create a new shell, or to reuse the existing shell:
+
+ source $HOME/.cargo/env
+
+ 4. Install rustc target toolchain:
+
+ % rustup target install armv7-unknown-linux-gnueabihf
+
+ 5. Put this in testing/geckodriver/.cargo/config:
+
+ [target.armv7-unknown-linux-gnueabihf]
+ linker = "arm-linux-gnueabihf-gcc"
+
+ 6. Build geckodriver from testing/geckodriver:
+
+ % cd testing/geckodriver
+ % cargo build --release --target armv7-unknown-linux-gnueabihf
+
+[announced the intent]: https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html
+[central]: https://hg.mozilla.org/mozilla-central/
diff --git a/doc/Building.md b/doc/Building.md
index 0f8fc28..50e2f96 100644
--- a/doc/Building.md
+++ b/doc/Building.md
@@ -18,7 +18,7 @@ since mach in this case does not have a compile environment:
% cd testing/geckodriver
% cargo build
- Compiling geckodriver v0.21.0 (file:///home/ato/src/gecko/testing/geckodriver)
+ Compiling geckodriver v0.21.0 (file:///code/gecko/testing/geckodriver)
Finished dev [optimized + debuginfo] target(s) in 7.83s
@@ -29,7 +29,7 @@ You can run your freshly built geckodriver this way:
% ./mach geckodriver -- --other --flags
-See <Testing.md> for how to run tests.
+See [Testing](Testing.html) for how to run tests.
[Rust]: https://www.rust-lang.org/
[webdriver crate]: https://crates.io/crates/webdriver
diff --git a/doc/Capabilities.md b/doc/Capabilities.md
index dd1354d..383a362 100644
--- a/doc/Capabilities.md
+++ b/doc/Capabilities.md
@@ -2,85 +2,10 @@ Firefox capabilities
====================
geckodriver has a few capabilities that are specific to Firefox.
+Most of these [are documented on MDN](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions).
-
-`moz:firefoxOptions`
---------------------
-
-A dictionary used to define options which control how Firefox gets
-started and run. It may contain any of the following fields:
-
-<table>
- <thead>
- <tr>
- <th>Name
- <th>Type
- <th>Description
- </tr>
- </thead>
-
- <tr id=capability-binary>
- <td><code>binary</code>
- <td>string
- <td>Absolute path of the Firefox binary,
- e.g. <code>/usr/bin/firefox</code>
- or <code>/Applications/Firefox.app/Contents/MacOS/firefox</code>,
- to select which custom browser binary to use.
- If left undefined geckodriver will attempt
- to deduce the default location of Firefox
- on the current system.
- </tr>
-
- <tr id=capability-args>
- <td><code>args</code>
- <td>array&nbsp;of&nbsp;strings
- <td><p>Command line arguments to pass to the Firefox binary.
- These must include the leading dash (<code>-</code>) where required,
- e.g. <code>["-devtools"]</code>.
-
- <p>To have geckodriver pick up an existing profile on the filesystem,
- you may pass <code>["-profile", "/path/to/profile"]</code>.
- </tr>
-
- <tr id=capability-profile>
- <td><code>profile</code>
- <td>string
- <td><p>Base64-encoded ZIP of a profile directory to use for the Firefox instance.
- This may be used to e.g. install extensions or custom certificates,
- but for setting custom preferences
- we recommend using the <a href=#capability-prefs><code>prefs</code></a> entry
- instead of passing a profile.
-
- <p>Profiles are created in the system’s temporary folder.
- This is also where the encoded profile is extracted
- when <code>profile</code> is provided.
- By default, geckodriver will create a new profile in this location.
-
- <p>The effective profile in use by the WebDriver session
- is returned to the user in the <code>moz:profile</code> capability
- in the new session response.
-
- <p>To have geckodriver pick up an <em>existing profile</em> on the filesystem,
- please set the <a href=#capability-args><code>args</code></a> field
- to <code>{"args": ["-profile", "/path/to/your/profile"]}</code>.
- </tr>
-
- <tr id=capability-log>
- <td><code>log</code>
- <td><a href=#log-object><code>log</code></a>&nbsp;object
- <td>To increase the logging verbosity of geckodriver and Firefox,
- you may pass a <a href=#log-object><code>log</code> object</a>
- that may look like <code>{"log": {"level": "trace"}}</code>
- to include all trace-level logs and above.
- </tr>
-
- <tr id=capability-prefs>
- <td><code>prefs</code>
- <td><a href=#prefs-object><code>prefs</code></a>&nbsp;object
- <td>Map of preference name to preference value, which can be a
- string, a boolean or an integer.
- </tr>
-</table>
+We additionally have some capabilities that largely are implementation
+concerns that normal users should not care about:
`moz:useNonSpecCompliantPointerOrigin`
@@ -89,8 +14,8 @@ started and run. It may contain any of the following fields:
A boolean value to indicate how the pointer origin for an action
command will be calculated.
-With Firefox 59 the calculation will be based on the requirements by
-the [WebDriver] specification. This means that the pointer origin
+With Firefox 59 the calculation will be based on the requirements
+by the [WebDriver] specification. This means that the pointer origin
is no longer computed based on the top and left position of the
referenced element, but on the in-view center point.
@@ -98,7 +23,8 @@ To temporarily disable the WebDriver conformant behavior use `false`
as value for this capability.
Please note that this capability exists only temporarily, and that
-it will be removed once all Selenium bindings can handle the new behavior.
+it will be removed once all Selenium bindings can handle the new
+behavior.
`moz:webdriverClick`
@@ -112,88 +38,18 @@ an older version of FirefoxDriver was in use.
With Firefox 58 the interactability checks as required by the
[WebDriver] specification are enabled by default. This means
geckodriver will additionally check if an element is obscured by
-another when clicking, and if an element is focusable for sending keys.
+another when clicking, and if an element is focusable for sending
+keys.
Because of this change in behaviour, we are aware that some extra
errors could be returned. In most cases the test in question might
have to be updated so it's conform with the new checks. But if the
-problem is located in geckodriver, then please raise an issue in the
-[issue tracker].
+problem is located in geckodriver, then please raise an issue in
+the [issue tracker].
To temporarily disable the WebDriver conformant checks use `false`
as value for this capability.
Please note that this capability exists only temporarily, and that
-it will be removed once the interactability checks have been stabilized.
-
-
-`log` object
-------------
-
-<table>
- <thead>
- <tr>
- <th>Name
- <th>Type
- <th>Description
- </tr>
- </thead>
-
- <tr>
- <td><code>level</code>
- <td>string
- <td>Set the level of verbosity of geckodriver and Firefox.
- Available levels are <code>trace</code>,
- <code>debug</code>, <code>config</code>,
- <code>info</code>, <code>warn</code>,
- <code>error</code>, and <code>fatal</code>.
- If left undefined the default is <code>info</code>.
- The value is treated case-insensitively.
- </tr>
-</table>
-
-
-`prefs` object
---------------
-
-<table>
- <thead>
- <tr>
- <th>Name
- <th>Type
- <th>Description
- </tr>
- </thead>
-
- <tr>
- <td><var>preference name</var>
- <td>string, number, boolean
- <td>One entry per preference to override.
- </tr>
-</table>
-
-
-Capabilities example
-====================
-
-The following example selects a specific Firefox binary to run with
-a prepared profile from the filesystem in headless mode (available on
-certain systems and recent Firefoxen). It also increases the number
-of IPC processes through a preference and enables more verbose logging.
-
- {
- "capabilities": {
- "alwaysMatch": {
- "moz:firefoxOptions": {
- "binary": "/usr/local/firefox/bin/firefox",
- "args": ["-headless", "-profile", "/path/to/my/profile"],
- "prefs": {
- "dom.ipc.processCount": 8
- },
- "log": {
- "level": "trace"
- }
- }
- }
- }
- } \ No newline at end of file
+it will be removed once the interactability checks have been
+stabilized.
diff --git a/doc/Notarization.md b/doc/Notarization.md
new file mode 100644
index 0000000..559800d
--- /dev/null
+++ b/doc/Notarization.md
@@ -0,0 +1,41 @@
+macOS notarization
+==================
+
+With the introduction of macOS 10.15 “Catalina” Apple introduced
+[new notarization requirements] that all software must be signed
+and notarized centrally.
+
+Whilst geckodriver is technically both signed and notarized, the
+way we package geckodriver on macOS means the notarization is lost.
+Mozilla considers this a known bug with the [geckodriver 0.26.0
+release] and are taking steps to resolve this. You can track the
+progress in [bug 1588081].
+
+There are some mitigating circumstances:
+
+ * Verification problems only occur when other notarized programs,
+ such as a web browser, downloads the software from the internet.
+
+ * Arbitrary software downloaded through other means, such as
+ curl(1) is _not_ affected by this change.
+
+In other words, if your method for fetching geckodriver on macOS
+is through the GitHub web UI using a web browser, the program will
+not be able to run unless you manually disable the quarantine check
+(explained below). If downloading geckodriver via other means
+than a macOS notarized program, you should not be affected.
+
+To bypass the notarization requirement on macOS if you have downloaded
+the geckodriver .tar.gz via a web browser, you can run the following
+command in a terminal:
+
+ % xattr -r -d com.apple.quarantine geckodriver
+
+A problem with notarization will manifest itself through a security
+dialogue appearing, explaining that the source of the program is
+not trusted.
+
+
+[new notarization requirements]: https://developer.apple.com/news/?id=04102019a
+[geckodriver 0.26.0 release]: https://github.com/mozilla/geckodriver/releases/tag/v0.26.0
+[bug 1588081]: https://bugzilla.mozilla.org/show_bug.cgi?id=1588081
diff --git a/doc/Profiles.md b/doc/Profiles.md
new file mode 100644
index 0000000..c7ff656
--- /dev/null
+++ b/doc/Profiles.md
@@ -0,0 +1,108 @@
+Profiles
+========
+
+geckodriver uses [profiles] to instrument Firefox’ behaviour. The
+user will usually rely on geckodriver to generate a temporary,
+throwaway profile. These profiles are deleted when the WebDriver
+session expires.
+
+In cases where the user needs to use custom, prepared profiles,
+geckodriver will make modifications to the profile that ensures
+correct behaviour. See [_Automation preferences_] below on the
+precedence of user-defined preferences in this case.
+
+Custom profiles can be provided two different ways:
+
+ 1. by appending `--profile /some/location` to the [`args` capability],
+ which will instruct geckodriver to use the profile _in-place_;
+
+ 2. or by setting the [`profile` capability] to a Base64-encoded
+ ZIP of the profile directory.
+
+Note that geckodriver has a [known bug concerning `--profile`] that
+prevents the randomised Marionette port from being passed to
+geckodriver. To circumvent this issue, make sure you specify the
+port manually using `--marionette-port <port>`.
+
+The second way is compatible with shipping Firefox profiles across
+a network, when for example the geckodriver instance is running on
+a remote system. This is the case when using Selenium’s `RemoteWebDriver`
+concept, where the WebDriver client and the server are running on
+two distinct systems.
+
+[profiles]: https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data
+[_Automation preferences_]: #automation-preferences
+[`args` capability]: ./Capabilities.html#capability-args
+[`profile` capability]: ./Capabilities.html#capability-profile
+[known bug concerning `--profile`]: https://github.com/mozilla/geckodriver/issues/1058
+
+
+Default locations for temporary profiles
+----------------------------------------
+
+When a custom user profile is not provided with the `-profile`
+command-line argument geckodriver generates a temporary, throwaway
+profile. This is written to the default system temporary folder
+and subsequently removed when the WebDriver session expires.
+
+The default location for temporary profiles depends on the system.
+On Unix systems it uses /tmp, and on Windows it uses the Windows
+directory.
+
+The default location can be overridden. On Unix you set the `TMPDIR`
+environment variable. On Windows, the following environment variables
+are respected, in order:
+
+ 1. `TMP`
+ 2. `TEMP`
+ 3. `USERPROFILE`
+
+It is not necessary to change the temporary directory system-wide.
+All you have to do is make sure it gets set for the environment of
+the geckodriver process:
+
+ % TMPDIR=/some/location ./geckodriver
+
+
+Automation preferences
+----------------------
+
+As indicated in the introduction, geckodriver configures Firefox
+so it is well-behaved in automation environments. It uses a
+combination of preferences written to the profile prior to launching
+Firefox (1), and a set of recommended preferences set on startup (2).
+
+These can be perused here:
+
+ 1. [testing/geckodriver/src/prefs.rs](https://searchfox.org/mozilla-central/source/testing/geckodriver/src/prefs.rs)
+ 2. [testing/marionette/components/marionette/marionette.js](https://searchfox.org/mozilla-central/source/testing/marionette/components/marionette.js)
+
+As mentioned, these are _recommended_ preferences, and any user-defined
+preferences in the [user.js file] or as part of the [`prefs` capability]
+take precedence. This means for example that the user can tweak
+`browser.startup.page` to override the recommended preference for
+starting the browser with a blank page.
+
+The recommended preferences set at runtime (see 2 above) may also
+be disabled entirely by setting `marionette.prefs.recommended`.
+This may however cause geckodriver to not behave correctly according
+to the WebDriver standard, so it should be used with caution.
+
+Users should take note that the `marionette.port` preference is
+special, and will always be overridden when using geckodriver unless
+the `--marionette-port <port>` flag is used specifically to instruct
+the Marionette server in Firefox which port to use.
+
+[user.js file]: http://kb.mozillazine.org/User.js_file
+[`prefs` capability]: ./Capabilities.html#capability-prefs
+
+
+Temporary profiles not being removed
+------------------------------------
+
+It is a known bug that geckodriver in some instances fails to remove
+the temporary profile, particularly when the session is not explicitly
+deleted or the process gets interrupted. See [geckodriver issue
+299] for more information.
+
+[geckodriver issue 299]: https://github.com/mozilla/geckodriver/issues/299
diff --git a/doc/Releasing.md b/doc/Releasing.md
index 491cb76..9618045 100644
--- a/doc/Releasing.md
+++ b/doc/Releasing.md
@@ -6,12 +6,7 @@ project’s canonical home was on GitHub. Today geckodriver is hosted
in [mozilla-central], and whilst we do want to make future releases
from [Mozilla’s CI infrastructure], we are currently in between two
worlds: development happens in m-c, but releases continue to be made
-from GitHub using Travis.
-
-The reason for this is that we do not compile geckodriver for all
-our target platforms, that Rust cross-compilation on TaskCluster
-builders is somewhat broken, and that tests are not run in automation.
-We intend to fix all these problems.
+from GitHub.
In any case, the steps to release geckodriver are as follows:
@@ -19,14 +14,15 @@ In any case, the steps to release geckodriver are as follows:
[Mozilla’s CI infrastructure]: https://treeherder.mozilla.org/
-Release new in-tree dependency crates
--------------------------------------
+Update in-tree dependency crates
+--------------------------------
geckodriver depends on a number of Rust crates that also live in
central by using relative paths:
[dependencies]
+ mozdevice = { path = "../mozbase/rust/mozdevice" }
mozprofile = { path = "../mozbase/rust/mozprofile" }
mozrunner = { path = "../mozbase/rust/mozrunner" }
mozversion = { path = "../mozbase/rust/mozversion" }
@@ -36,8 +32,9 @@ central by using relative paths:
Because we need to export the geckodriver source code to the old
GitHub repository when we release, we first need to publish these
crates if they have had any changes in the interim since the last
-release. If they have receieved no changes, you can skip them:
+release. If they have received no changes, you can skip them:
+ - `testing/mozbase/rust/mozdevice`
- `testing/mozbase/rust/mozprofile`
- `testing/mozbase/rust/mozrunner`
- `testing/mozbase/rust/mozversion`
@@ -46,7 +43,9 @@ release. If they have receieved no changes, you can skip them:
For each crate:
1. Bump the version number in Cargo.toml
- 2. `cargo publish`
+ 2. Update the crate: `cargo update -p <crate name>`
+ 3. Commit the changes for the modified `Cargo.toml`, and `Cargo.lock`
+ (can be found in the repositories root folder)
Update the change log
@@ -67,14 +66,14 @@ is implemented there.
We follow the writing style of the existing change log, with
one section per version (with a release date), with subsections
-‘Added’, ‘Changed’, and ‘Removed’. If the targetted
+‘Added’, ‘Changed’, and ‘Removed’. If the targeted
Firefox or Selenium versions have changed, it is good to make a
mention of this. Lines are optimally formatted at roughly 72 columns
to make the file readable in a text editor as well as rendered HTML.
fmt(1) does a splendid job at text formatting.
[CHANGES.md]: https://searchfox.org/mozilla-central/source/testing/geckodriver/CHANGES.md
-[rust-mozrunner]: https://github.com/jgraham/rust_mozrunner
+[rust-mozrunner]: https://searchfox.org/mozilla-central/source/testing/mozbase/rust/mozrunner
Update libraries
@@ -85,32 +84,68 @@ Make relevant changes to [Cargo.toml] to upgrade dependencies, then run
% ./mach vendor rust
% ./mach build testing/geckodriver
-to pull down and vendor the upgraded libraries. Remember to check
-in the [Cargo.lock] file since we want reproducible builds for
-geckodriver, uninfluenced by dependency variations.
+to pull down and vendor the upgraded libraries.
The updates to dependencies should always be made as a separate
commit to not confuse reviewers, because vendoring involves checking
in a lot of extra code already reviewed downstream.
[Cargo.toml]: https://searchfox.org/mozilla-central/source/testing/geckodriver/Cargo.toml
-[Cargo.lock]: https://searchfox.org/mozilla-central/source/testing/geckodriver/Cargo.lock
+[Cargo.lock]: https://searchfox.org/mozilla-central/source/Cargo.lock
-Bump the version number
------------------------
+Bump the version number and update the support page
+---------------------------------------------------
Bump the version number in [Cargo.toml] to the next version.
geckodriver follows [semantic versioning] so it’s a good idea to
-familiarise yourself wih that before deciding on the version number.
+familiarise yourself with that before deciding on the version number.
After you’ve changed the version number, run
% ./mach build testing/geckodriver
-again to update [Cargo.lock], and check in the file.
+again to update [Cargo.lock].
+
+Now update the [support page] by adding a new row to the versions table,
+including the required versions of Selenium, and Firefox.
+
+Finally commit all those changes.
[semantic versioning]: http://semver.org/
+[support page]: https://searchfox.org/mozilla-central/source/testing/geckodriver/doc/Support.md
+
+
+Add the changeset id
+--------------------
+
+To easily allow a release build of geckodriver after cloning the
+repository, the changeset id for the release has to be added to the
+change log. Therefore add a final place-holder commit to the patch
+series, to already get review for.
+
+Once all previous revisions of the patch series have been reviewed and
+landed, it's known which commit id the version bump commit has, finalize the
+change log, and land that remaining revision.
+
+
+Release new in-tree dependency crates
+-------------------------------------
+
+Make sure to wait until the complete patch series from above has been
+merged to mozilla-central. Then continue with the following steps.
+
+Before releasing geckodriver all dependency crates as
+[updated earlier](#update-in-tree-dependency-crates) have to be
+released first.
+
+Therefore change into each of the directories for crates with an update
+and run the following command to publish the crate:
+
+ % cargo publish
+
+Note that if a crate has an in-tree dependency make sure to first
+change the dependency information.
Export to GitHub
@@ -126,23 +161,24 @@ state of the project when it was exported to mozilla-central; and
_release_, from where releases are made.
Before we copy the code over to the GitHub repository we need to
-check out the [commit we made earlier](#bump-the-version-number)
-against central that bumped the version number:
+check out the [release commit that bumped the version number](#add-the-changeset-id)
+on mozilla-central:
- % git checkout $BUMP_REVISION
+ % hg update $RELEASE_REVISION
Or:
- % hg update $BUMP_REVISION
+ % git checkout $RELEASE_REVISION
We will now export the contents of [testing/geckodriver] to the
_release_ branch:
% cd $SRC/geckodriver
% git checkout release
+ % git pull
% git rm -rf .
% git clean -fxd
- % cp -r $SRC/gecko/testing/geckodriver .
+ % cp -rt $SRC/gecko/testing/geckodriver .
[README.md]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md
[testing/geckodriver]: https://searchfox.org/mozilla-central/source/testing/geckodriver
@@ -154,7 +190,7 @@ Manually change in-tree path dependencies
After the source code has been imported we need to change the dependency
information for the `mozrunner`, `mozprofile`, `mozversion`, and
`webdriver` crates. As explained previously geckodriver depends
-on a relative path in in the mozilla-central repository to build
+on a relative path in the mozilla-central repository to build
with the latest unreleased source code.
This relative paths do not exist in the GitHub repository and the
@@ -167,51 +203,53 @@ made to it since the last geckodriver release.
Commit local changes
--------------------
-Now commit all the changes you have made locally to the _release_ branch:
+Now commit all the changes you have made locally to the _release_ branch.
+It is recommended to setup a [GPG key] for signing the commit, so
+that the release commit is marked as `verified`.
% git add .
- % git commit -am "import of vX.Y.Z"
+ % git commit -S -am "import of vX.Y.Z" (signed)
-As indicated above, the changes you make to this branch will not
-be upstreamed back into mozilla-central. It is merely used as a
-staging ground for pushing builds to Travis.
+or if you cannot use signing use:
+ % git add .
+ % git commit -am "import of vX.Y.Z" (unsigned)
-Tag the release
----------------
+Then push the changes:
-Run the following command to tag the release:
+ % git push
- % git tag 'vX.Y.Z'
+As indicated above, the changes you make to this branch will not
+be upstreamed back into mozilla-central. It is merely used as a
+place for external consumers to build their own version of geckodriver.
+
+[GPG key]: https://help.github.com/articles/signing-commits/
Make the release
----------------
-geckodriver is released and automatically uploaded from Travis by
-pushing a new version tag to the _release_ branch:
+geckodriver needs to be manually released on github.com. Therefore start to
+[draft a new release], and make the following changes:
- % git push
- % git push --tags
+1. Specify the "Tag version", and select "Release" as target.
+2. Leave the release title empty
-Update the release description
-------------------------------
+3. Paste the raw Markdown source from [CHANGES.md] into the description field.
+ This will highlight for end-users what changes were made in that particular
+ package when they visit the GitHub downloads section. Make sure to check that
+ all references can be resolved, and if not make sure to add those too.
-Copy the raw Markdown source from [CHANGES.md] into the description
-of the [latest release]. This will highlight for end-users what
-changes were made in that particular package when they visit the
-GitHub downloads section.
+4. Find the signed geckodriver archives in the [taskcluster index] by
+ replacing %changeset% with the full release changeset id. Rename the
+ individual files so the basename looks like 'geckodriver-v%version%-%platform%'.
+ Upload them all, including the checksum files for both the Linux platforms.
-Congratulations! You’ve released geckodriver!
+[draft a new release]: https://github.com/mozilla/geckodriver/releases/new
+[taskcluster index]: https://firefox-ci-tc.services.mozilla.com/tasks/index/gecko.v2.mozilla-central.revision.%changeset%.geckodriver
-[latest release]: https://github.com/mozilla/geckodriver/releases
+Congratulations! You’ve released geckodriver!
-Future work
------------
-
-In the future, we intend to [sign releases] so that they are
-verifiable.
-
-[sign releases]: https://github.com/mozilla/geckodriver/issues/292
+[releases page]: https://github.com/mozilla/geckodriver/releases
diff --git a/doc/Support.md b/doc/Support.md
index 5adfb52..c3dcfe7 100644
--- a/doc/Support.md
+++ b/doc/Support.md
@@ -2,12 +2,12 @@ Supported platforms
===================
The following table shows a mapping between [geckodriver releases],
-supported versions of Firefox, and required Selenium version:
+and required versions of Selenium and Firefox:
<style type="text/css">
table { width: 100%; margin-bottom: 2em; }
table, th, td { border: solid gray 1px; }
- td { padding: 5px 10px; text-align: center; }
+ td, th { padding: 5px 10px; text-align: center; }
</style>
<table>
@@ -15,7 +15,7 @@ supported versions of Firefox, and required Selenium version:
<tr>
<th rowspan="2">geckodriver
<th rowspan="2">Selenium
- <th colspan="2">Supported versions of Firefox
+ <th colspan="2">Firefox
</tr>
<tr>
<th>min
@@ -24,20 +24,35 @@ supported versions of Firefox, and required Selenium version:
</thead>
<tr>
- <td>0.23.0
+ <td>0.26.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>n/a
+ <tr>
+ <td>0.25.0
<td>≥ 3.11 (3.14 Python)
<td>57
<td>n/a
<tr>
+ <td>0.24.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>57
+ <td>79
+ <tr>
+ <td>0.23.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>57
+ <td>79
+ <tr>
<td>0.22.0
<td>≥ 3.11 (3.14 Python)
<td>57
- <td>n/a
+ <td>79
<tr>
<td>0.21.0
<td>≥ 3.11 (3.14 Python)
<td>57
- <td>n/a
+ <td>79
<tr>
<td>0.20.1
<td>≥ 3.5
@@ -77,7 +92,6 @@ Clients
Other clients that follow the [W3C WebDriver specification][WebDriver]
are also supported.
-
Firefoxen
---------
@@ -95,6 +109,18 @@ in the most recent Firefox versions, and we strongly advise using the
latest [Firefox Nightly] with geckodriver. Since Windows XP support
in Firefox was dropped with Firefox 53, we do not support this platform.
+Android
+-------
+
+Starting with the 0.26.0 release geckodriver is able to connect
+to Android devices, and to control packages which are based on [GeckoView]
+(eg. [Firefox Preview] aka Fenix, or [Firefox Reality]). But it also still
+supports versions of Fennec up to 68 ESR, which is the last officially
+supported release from Mozilla.
+
+To run tests on Android specific capabilities under `moz:firefoxOptions`
+have to be set when requesting a new session. See the Android section under
+[Firefox Capabilities](Capabilities.html#android) for more details.
[geckodriver releases]: https://github.com/mozilla/geckodriver/releases
[Selenium]: https://github.com/seleniumhq/selenium
@@ -105,3 +131,6 @@ in Firefox was dropped with Firefox 53, we do not support this platform.
[specification]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Aspec
[issue tracker]: https://github.com/mozilla/geckodriver/issues
[Firefox Nightly]: https://nightly.mozilla.org/
+[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView
+[Firefox Preview]: https://play.google.com/store/apps/details?id=org.mozilla.fenix
+[Firefox Reality]: https://play.google.com/store/apps/details?id=org.mozilla.vrbrowser
diff --git a/doc/Testing.md b/doc/Testing.md
index 5ed2f9f..f8dfae0 100644
--- a/doc/Testing.md
+++ b/doc/Testing.md
@@ -25,7 +25,7 @@ make sure you have built Firefox:
% ./mach wpt testing/web-platform/tests/webdriver
As these are functional integration tests and pop up Firefox windows
-sporadically, a helpful tip is to surpress the window whilst you
+sporadically, a helpful tip is to suppress the window whilst you
are running them by using Firefox’ [headless mode]:
% ./mach wpt --headless testing/web-platform/tests/webdriver
@@ -55,4 +55,4 @@ flag to geckodriver through WPT:
[headless mode]: https://developer.mozilla.org/en-US/Firefox/Headless_mode
[mozconfig]: https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options
[trace-level logs]: TraceLogs.html
-[Marionette protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/marionette/Protocol.html
+[Marionette protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html
diff --git a/doc/TraceLogs.md b/doc/TraceLogs.md
index 791f838..ef472e4 100644
--- a/doc/TraceLogs.md
+++ b/doc/TraceLogs.md
@@ -71,7 +71,7 @@ enable trace logs for both geckodriver and Marionette:
% geckodriver -vv
The second way of setting the log level is through capabilities.
-geckodriver accepts a Mozila-specific configuration object
+geckodriver accepts a Mozilla-specific configuration object
in [`moz:firefoxOptions`]. This JSON Object, which is further
described in the [README] can hold Firefox-specific configuration,
such as which Firefox binary to use, additional preferences to set,
diff --git a/doc/Usage.md b/doc/Usage.md
index 67b88de..a5d915e 100644
--- a/doc/Usage.md
+++ b/doc/Usage.md
@@ -32,7 +32,7 @@ Or by passing it as a flag to the [java(1)] launcher:
% java -Dwebdriver.gecko.driver=/home/user/bin YourApplication
-Your milage with this approach may vary based on which programming
+Your mileage with this approach may vary based on which programming
language bindings you are using. It is in any case generally the case
that geckodriver will be picked up if it is available on the system path.
In a bash compatible shell, you can make other programs aware of its
diff --git a/doc/index.rst b/doc/index.rst
index cb159de..be9fc88 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -7,20 +7,17 @@ Gecko-based browsers.
This program provides the HTTP API described by the `WebDriver protocol`_.
to communicate with Gecko browsers, such as Firefox. It translates calls
-into the `Firefox remote protocol`_ by acting as a proxy between the local-
+into the :ref:`Firefox remote protocol <Protocol>` by acting as a proxy between the local-
and remote ends.
You can consult the `change log`_ for a record of all notable changes
to the program. Releases_ are made available on GitHub.
.. _WebDriver protocol: https://w3c.github.io/webdriver/#protocol
-.. _Firefox remote protocol: https://firefox-source-docs.mozilla.org/testing/marionette/marionette/Protocol.html
.. _change log: https://github.com/mozilla/geckodriver/releases
.. _Releases: https://github.com/mozilla/geckodriver/releases
-For users
-=========
.. toctree::
:maxdepth: 1
@@ -29,9 +26,11 @@ For users
Capabilities.md
Usage.md
Flags.md
+ Profiles.md
Bugs.md
TraceLogs.md
CrashReports.md
+ Notarization.md
For developers
@@ -42,6 +41,7 @@ For developers
Building.md
Testing.md
Releasing.md
+ ARM.md
Communication
@@ -50,10 +50,8 @@ Communication
The mailing list for geckodriver discussion is
tools-marionette@lists.mozilla.org (`subscribe`_, `archive`_).
-If you prefer real-time chat, there is often someone in the #interop IRC
-channel on irc.mozilla.org. Don’t ask if you may ask a question;
-just go ahead and ask, and please wait for an answer as we might
-not be in your timezone.
+If you prefer real-time chat, ask your questions
+on `#interop:mozilla.org <https://chat.mozilla.org/#/room/#interop:mozilla.org>`__.
.. _subscribe: https://lists.mozilla.org/listinfo/tools-marionette
.. _archive: https://lists.mozilla.org/pipermail/tools-marionette/
diff --git a/mach_commands.py b/mach_commands.py
index 2dcb7af..efcf759 100644
--- a/mach_commands.py
+++ b/mach_commands.py
@@ -14,7 +14,7 @@ from mach.decorators import (
CommandProvider,
)
-from mozbuild.base import MachCommandBase
+from mozbuild.base import MachCommandBase, BinaryNotFoundException
@CommandProvider
@@ -41,13 +41,16 @@ class GeckoDriver(MachCommandBase):
def run(self, binary, params, debug, debugger, debugger_args):
try:
binpath = self.get_binary_path("geckodriver")
- except Exception as e:
- print("It looks like geckodriver isn't built. "
- "Add ac_add_options --enable-geckodriver to your "
- "mozconfig ",
- "and run |mach build| to build it.")
- print(e)
- return 1
+ except BinaryNotFoundException as e:
+ self.log(logging.ERROR, 'geckodriver',
+ {'error': str(e)},
+ 'ERROR: {error}')
+ self.log(logging.INFO, 'geckodriver', {},
+ "It looks like geckodriver isn't built. "
+ "Add ac_add_options --enable-geckodriver to your "
+ "mozconfig "
+ "and run |./mach build| to build it.")
+ return 1
args = [binpath]
@@ -55,7 +58,16 @@ class GeckoDriver(MachCommandBase):
args.extend(params)
if binary is None:
- binary = self.get_binary_path("app")
+ try:
+ binary = self.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ self.log(logging.ERROR, 'geckodriver',
+ {'error': str(e)},
+ 'ERROR: {error}')
+ self.log(logging.INFO, 'geckodriver',
+ {'help': e.help()},
+ '{help}')
+ return 1
args.extend(["--binary", binary])
diff --git a/marionette/Cargo.toml b/marionette/Cargo.toml
new file mode 100644
index 0000000..1d18558
--- /dev/null
+++ b/marionette/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "marionette"
+version = "0.1.0"
+authors = ["Mozilla"]
+edition = "2018"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_repr = "0.1"
diff --git a/marionette/src/common.rs b/marionette/src/common.rs
new file mode 100644
index 0000000..b051e66
--- /dev/null
+++ b/marionette/src/common.rs
@@ -0,0 +1,246 @@
+use serde::ser::SerializeMap;
+use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
+use serde_json::Value;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct BoolValue {
+ value: bool,
+}
+
+impl BoolValue {
+ pub fn new(val: bool) -> Self {
+ BoolValue { value: val }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Cookie {
+ pub name: String,
+ pub value: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub path: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub domain: Option<String>,
+ #[serde(default)]
+ pub secure: bool,
+ #[serde(default, rename = "httpOnly")]
+ pub http_only: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expiry: Option<Date>,
+ #[serde(skip_serializing_if = "Option::is_none", rename = "sameSite")]
+ pub same_site: Option<String>,
+}
+
+pub fn to_cookie<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+ T: Serialize,
+{
+ #[derive(Serialize)]
+ struct Wrapper<T> {
+ cookie: T,
+ }
+
+ Wrapper { cookie: data }.serialize(serializer)
+}
+
+pub fn from_cookie<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+ D: Deserializer<'de>,
+ T: serde::de::DeserializeOwned,
+ T: std::fmt::Debug,
+{
+ #[derive(Debug, Deserialize)]
+ struct Wrapper<T> {
+ cookie: T,
+ }
+
+ let w = Wrapper::deserialize(deserializer)?;
+ Ok(w.cookie)
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Date(pub u64);
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Frame {
+ Index(u16),
+ Element(String),
+ Parent,
+}
+
+impl Serialize for Frame {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(1))?;
+ match self {
+ Frame::Index(nth) => map.serialize_entry("id", nth)?,
+ Frame::Element(el) => map.serialize_entry("element", el)?,
+ Frame::Parent => map.serialize_entry("id", &Value::Null)?,
+ }
+ map.end()
+ }
+}
+
+impl<'de> Deserialize<'de> for Frame {
+ fn deserialize<D>(deserializer: D) -> Result<Frame, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Debug, Deserialize)]
+ #[serde(rename_all = "lowercase")]
+ struct JsonFrame {
+ id: Option<u16>,
+ element: Option<String>,
+ }
+
+ let json = JsonFrame::deserialize(deserializer)?;
+ match (json.id, json.element) {
+ (Some(_id), Some(_element)) => Err(de::Error::custom("conflicting frame identifiers")),
+ (Some(id), None) => Ok(Frame::Index(id)),
+ (None, Some(element)) => Ok(Frame::Element(element)),
+ (None, None) => Ok(Frame::Parent),
+ }
+ }
+}
+
+// TODO(nupur): Bug 1567165 - Make WebElement in Marionette a unit struct
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct WebElement {
+ #[serde(rename = "element-6066-11e4-a52e-4f735466cecf")]
+ pub element: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Timeouts {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub implicit: Option<u64>,
+ #[serde(
+ default,
+ rename = "pageLoad",
+ alias = "page load",
+ skip_serializing_if = "Option::is_none"
+ )]
+ pub page_load: Option<u64>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[allow(clippy::option_option)]
+ pub script: Option<Option<u64>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Window {
+ pub name: String,
+ pub handle: String,
+}
+
+pub fn to_name<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+ T: Serialize,
+{
+ #[derive(Serialize)]
+ struct Wrapper<T> {
+ name: T,
+ }
+
+ Wrapper { name: data }.serialize(serializer)
+}
+
+pub fn from_name<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+ D: Deserializer<'de>,
+ T: serde::de::DeserializeOwned,
+ T: std::fmt::Debug,
+{
+ #[derive(Debug, Deserialize)]
+ struct Wrapper<T> {
+ name: T,
+ }
+
+ let w = Wrapper::deserialize(deserializer)?;
+ Ok(w.name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test::{assert_de, assert_ser, assert_ser_de, ELEMENT_KEY};
+ use serde_json::json;
+
+ #[test]
+ fn test_cookie_default_values() {
+ let data = Cookie {
+ name: "hello".into(),
+ value: "world".into(),
+ path: None,
+ domain: None,
+ secure: false,
+ http_only: false,
+ expiry: None,
+ same_site: None,
+ };
+ assert_de(&data, json!({"name":"hello", "value":"world"}));
+ }
+
+ #[test]
+ fn test_json_frame_index() {
+ assert_ser_de(&Frame::Index(1234), json!({"id": 1234}));
+ }
+
+ #[test]
+ fn test_json_frame_element() {
+ assert_ser_de(&Frame::Element("elem".into()), json!({"element": "elem"}));
+ }
+
+ #[test]
+ fn test_json_frame_parent() {
+ assert_ser_de(&Frame::Parent, json!({ "id": null }));
+ }
+
+ #[test]
+ fn test_web_element() {
+ let data = WebElement {
+ element: "foo".into(),
+ };
+ assert_ser_de(&data, json!({ELEMENT_KEY: "foo"}));
+ }
+
+ #[test]
+ fn test_timeouts_with_all_params() {
+ let data = Timeouts {
+ implicit: Some(1000),
+ page_load: Some(200000),
+ script: Some(Some(60000)),
+ };
+ assert_ser_de(
+ &data,
+ json!({"implicit":1000,"pageLoad":200000,"script":60000}),
+ );
+ assert_de(
+ &data,
+ json!({"implicit":1000,"page load":200000,"script":60000}),
+ );
+ }
+
+ #[test]
+ fn test_timeouts_with_missing_params() {
+ let data = Timeouts {
+ implicit: Some(1000),
+ page_load: None,
+ script: None,
+ };
+ assert_ser_de(&data, json!({"implicit":1000}));
+ }
+
+ #[test]
+ fn test_timeouts_setting_script_none() {
+ let data = Timeouts {
+ implicit: Some(1000),
+ page_load: None,
+ script: Some(None),
+ };
+ assert_ser(&data, json!({"implicit":1000, "script":null}));
+ }
+}
diff --git a/marionette/src/error.rs b/marionette/src/error.rs
new file mode 100644
index 0000000..512aae6
--- /dev/null
+++ b/marionette/src/error.rs
@@ -0,0 +1,180 @@
+use std::error;
+use std::fmt;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
+#[serde(untagged)]
+pub(crate) enum Error {
+ Marionette(MarionetteError),
+}
+
+impl Error {
+ pub fn kind(&self) -> ErrorKind {
+ match *self {
+ Error::Marionette(ref err) => err.kind,
+ }
+ }
+}
+
+impl fmt::Debug for Error {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Marionette(ref err) => fmt
+ .debug_struct("Marionette")
+ .field("kind", &err.kind)
+ .field("message", &err.message)
+ .field("stacktrace", &err.stack.clone())
+ .finish(),
+ }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Marionette(ref err) => write!(fmt, "{}: {}", err.kind, err.message),
+ }
+ }
+}
+
+impl error::Error for Error {
+ fn description(&self) -> &str {
+ match self {
+ Error::Marionette(_) => self.kind().as_str(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
+pub struct MarionetteError {
+ #[serde(rename = "error")]
+ pub kind: ErrorKind,
+ #[serde(default = "empty_string")]
+ pub message: String,
+ #[serde(rename = "stacktrace", default = "empty_string")]
+ pub stack: String,
+}
+
+fn empty_string() -> String {
+ "".to_owned()
+}
+
+impl Into<Error> for MarionetteError {
+ fn into(self) -> Error {
+ Error::Marionette(self)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
+pub enum ErrorKind {
+ #[serde(rename = "element click intercepted")]
+ ElementClickIntercepted,
+ #[serde(rename = "element not accessible")]
+ ElementNotAccessible,
+ #[serde(rename = "element not interactable")]
+ ElementNotInteractable,
+ #[serde(rename = "insecure certificate")]
+ InsecureCertificate,
+ #[serde(rename = "invalid argument")]
+ InvalidArgument,
+ #[serde(rename = "invalid cookie")]
+ InvalidCookieDomain,
+ #[serde(rename = "invalid element state")]
+ InvalidElementState,
+ #[serde(rename = "invalid selector")]
+ InvalidSelector,
+ #[serde(rename = "invalid session id")]
+ InvalidSessionId,
+ #[serde(rename = "javascript error")]
+ JavaScript,
+ #[serde(rename = "move target out of bounds")]
+ MoveTargetOutOfBounds,
+ #[serde(rename = "no such alert")]
+ NoSuchAlert,
+ #[serde(rename = "no such element")]
+ NoSuchElement,
+ #[serde(rename = "no such frame")]
+ NoSuchFrame,
+ #[serde(rename = "no such window")]
+ NoSuchWindow,
+ #[serde(rename = "script timeout")]
+ ScriptTimeout,
+ #[serde(rename = "session not created")]
+ SessionNotCreated,
+ #[serde(rename = "stale element reference")]
+ StaleElementReference,
+ #[serde(rename = "timeout")]
+ Timeout,
+ #[serde(rename = "unable to set cookie")]
+ UnableToSetCookie,
+ #[serde(rename = "unexpected alert open")]
+ UnexpectedAlertOpen,
+ #[serde(rename = "unknown command")]
+ UnknownCommand,
+ #[serde(rename = "unknown error")]
+ Unknown,
+ #[serde(rename = "unsupported operation")]
+ UnsupportedOperation,
+ #[serde(rename = "webdriver error")]
+ WebDriver,
+}
+
+impl ErrorKind {
+ pub(crate) fn as_str(self) -> &'static str {
+ use ErrorKind::*;
+ match self {
+ ElementClickIntercepted => "element click intercepted",
+ ElementNotAccessible => "element not accessible",
+ ElementNotInteractable => "element not interactable",
+ InsecureCertificate => "insecure certificate",
+ InvalidArgument => "invalid argument",
+ InvalidCookieDomain => "invalid cookie",
+ InvalidElementState => "invalid element state",
+ InvalidSelector => "invalid selector",
+ InvalidSessionId => "invalid session id",
+ JavaScript => "javascript error",
+ MoveTargetOutOfBounds => "move target out of bounds",
+ NoSuchAlert => "no such alert",
+ NoSuchElement => "no such element",
+ NoSuchFrame => "no such frame",
+ NoSuchWindow => "no such window",
+ ScriptTimeout => "script timeout",
+ SessionNotCreated => "session not created",
+ StaleElementReference => "stale eelement referencee",
+ Timeout => "timeout",
+ UnableToSetCookie => "unable to set cookie",
+ UnexpectedAlertOpen => "unexpected alert open",
+ UnknownCommand => "unknown command",
+ Unknown => "unknown error",
+ UnsupportedOperation => "unsupported operation",
+ WebDriver => "webdriver error",
+ }
+ }
+}
+
+impl fmt::Display for ErrorKind {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ write!(fmt, "{}", self.as_str())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test::assert_ser_de;
+ use serde_json::json;
+
+ #[test]
+ fn test_json_error() {
+ let err = MarionetteError {
+ kind: ErrorKind::Timeout,
+ message: "".into(),
+ stack: "".into(),
+ };
+ assert_ser_de(
+ &err,
+ json!({"error": "timeout", "message": "", "stacktrace": ""}),
+ );
+ }
+}
diff --git a/marionette/src/lib.rs b/marionette/src/lib.rs
new file mode 100644
index 0000000..30ef48d
--- /dev/null
+++ b/marionette/src/lib.rs
@@ -0,0 +1,10 @@
+pub mod error;
+
+pub mod common;
+pub mod marionette;
+pub mod message;
+pub mod result;
+pub mod webdriver;
+
+#[cfg(test)]
+mod test;
diff --git a/marionette/src/marionette.rs b/marionette/src/marionette.rs
new file mode 100644
index 0000000..35d0fc0
--- /dev/null
+++ b/marionette/src/marionette.rs
@@ -0,0 +1,65 @@
+use serde::{Deserialize, Serialize};
+
+use crate::common::BoolValue;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[allow(non_camel_case_types)]
+pub enum AppStatus {
+ eAttemptQuit,
+ eConsiderQuit,
+ eForceQuit,
+ eRestart,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Command {
+ #[serde(rename = "Marionette:AcceptConnections")]
+ AcceptConnections(BoolValue),
+ #[serde(rename = "Marionette:Quit")]
+ DeleteSession { flags: Vec<AppStatus> },
+ #[serde(rename = "Marionette:GetContext")]
+ GetContext,
+ #[serde(rename = "Marionette:GetScreenOrientation")]
+ GetScreenOrientation,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test::assert_ser_de;
+ use serde_json::json;
+
+ #[test]
+ fn test_json_command_accept_connections() {
+ assert_ser_de(
+ &Command::AcceptConnections(BoolValue::new(false)),
+ json!({"Marionette:AcceptConnections": {"value": false }}),
+ );
+ }
+
+ #[test]
+ fn test_json_command_delete_session() {
+ let data = &Command::DeleteSession {
+ flags: vec![AppStatus::eForceQuit],
+ };
+ assert_ser_de(data, json!({"Marionette:Quit": {"flags": ["eForceQuit"]}}));
+ }
+
+ #[test]
+ fn test_json_command_get_context() {
+ assert_ser_de(&Command::GetContext, json!("Marionette:GetContext"));
+ }
+
+ #[test]
+ fn test_json_command_get_screen_orientation() {
+ assert_ser_de(
+ &Command::GetScreenOrientation,
+ json!("Marionette:GetScreenOrientation"),
+ );
+ }
+
+ #[test]
+ fn test_json_command_invalid() {
+ assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
+ }
+}
diff --git a/marionette/src/message.rs b/marionette/src/message.rs
new file mode 100644
index 0000000..8ccb816
--- /dev/null
+++ b/marionette/src/message.rs
@@ -0,0 +1,332 @@
+use serde::de::{self, SeqAccess, Unexpected, Visitor};
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde_json::{Map, Value};
+use serde_repr::{Deserialize_repr, Serialize_repr};
+use std::fmt;
+
+use crate::error::MarionetteError;
+use crate::marionette;
+use crate::result::MarionetteResult;
+use crate::webdriver;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum Command {
+ WebDriver(webdriver::Command),
+ Marionette(marionette::Command),
+}
+
+impl Command {
+ pub fn name(&self) -> String {
+ let (command_name, _) = self.first_entry();
+ command_name
+ }
+
+ fn params(&self) -> Value {
+ let (_, params) = self.first_entry();
+ params
+ }
+
+ fn first_entry(&self) -> (String, serde_json::Value) {
+ match serde_json::to_value(&self).unwrap() {
+ Value::String(cmd) => (cmd, Value::Object(Map::new())),
+ Value::Object(items) => {
+ let mut iter = items.iter();
+ let (cmd, params) = iter.next().unwrap();
+ (cmd.to_string(), params.clone())
+ }
+ _ => unreachable!(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize_repr, Deserialize_repr)]
+#[repr(u8)]
+enum MessageDirection {
+ Incoming = 0,
+ Outgoing = 1,
+}
+
+pub type MessageId = u32;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Request(pub MessageId, pub Command);
+
+impl Request {
+ pub fn id(&self) -> MessageId {
+ self.0
+ }
+
+ pub fn command(&self) -> &Command {
+ &self.1
+ }
+
+ pub fn params(&self) -> Value {
+ self.command().params()
+ }
+}
+
+impl Serialize for Request {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (
+ MessageDirection::Incoming,
+ self.id(),
+ self.command().name(),
+ self.params(),
+ )
+ .serialize(serializer)
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum Response {
+ Result {
+ id: MessageId,
+ result: MarionetteResult,
+ },
+ Error {
+ id: MessageId,
+ error: MarionetteError,
+ },
+}
+
+impl Serialize for Response {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ match self {
+ Response::Result { id, result } => {
+ (MessageDirection::Outgoing, id, Value::Null, &result).serialize(serializer)
+ }
+ Response::Error { id, error } => {
+ (MessageDirection::Outgoing, id, &error, Value::Null).serialize(serializer)
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Serialize)]
+#[serde(untagged)]
+pub enum Message {
+ Incoming(Request),
+ Outgoing(Response),
+}
+
+struct MessageVisitor;
+
+impl<'de> Visitor<'de> for MessageVisitor {
+ type Value = Message;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("four-element array")
+ }
+
+ fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
+ let direction = seq
+ .next_element::<MessageDirection>()?
+ .ok_or_else(|| de::Error::invalid_length(0, &self))?;
+ let id: MessageId = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(1, &self))?;
+
+ let msg = match direction {
+ MessageDirection::Incoming => {
+ let name: String = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(2, &self))?;
+ let params: Value = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(3, &self))?;
+
+ let command = match params {
+ Value::Object(ref items) if !items.is_empty() => {
+ let command_to_params = {
+ let mut m = Map::new();
+ m.insert(name, params);
+ Value::Object(m)
+ };
+ serde_json::from_value(command_to_params).map_err(de::Error::custom)
+ }
+ Value::Object(_) | Value::Null => {
+ serde_json::from_value(Value::String(name)).map_err(de::Error::custom)
+ }
+ x => Err(de::Error::custom(format!("unknown params type: {}", x))),
+ }?;
+ Message::Incoming(Request(id, command))
+ }
+
+ MessageDirection::Outgoing => {
+ let maybe_error: Option<MarionetteError> = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(2, &self))?;
+
+ let response = if let Some(error) = maybe_error {
+ seq.next_element::<Value>()?
+ .ok_or_else(|| de::Error::invalid_length(3, &self))?
+ .as_null()
+ .ok_or_else(|| de::Error::invalid_type(Unexpected::Unit, &self))?;
+ Response::Error { id, error }
+ } else {
+ let result: MarionetteResult = seq
+ .next_element()?
+ .ok_or_else(|| de::Error::invalid_length(3, &self))?;
+ Response::Result { id, result }
+ };
+
+ Message::Outgoing(response)
+ }
+ };
+
+ Ok(msg)
+ }
+}
+
+impl<'de> Deserialize<'de> for Message {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_seq(MessageVisitor)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use super::*;
+
+ use crate::common::*;
+ use crate::error::{ErrorKind, MarionetteError};
+ use crate::test::assert_ser_de;
+
+ #[test]
+ fn test_incoming() {
+ let json =
+ json!([0, 42, "WebDriver:FindElement", {"using": "css selector", "value": "value"}]);
+ let find_element = webdriver::Command::FindElement(webdriver::Locator {
+ using: webdriver::Selector::CSS,
+ value: "value".into(),
+ });
+ let req = Request(42, Command::WebDriver(find_element));
+ let msg = Message::Incoming(req);
+ assert_ser_de(&msg, json);
+ }
+
+ #[test]
+ fn test_incoming_empty_params() {
+ let json = json!([0, 42, "WebDriver:GetTimeouts", {}]);
+ let req = Request(42, Command::WebDriver(webdriver::Command::GetTimeouts));
+ let msg = Message::Incoming(req);
+ assert_ser_de(&msg, json);
+ }
+
+ #[test]
+ fn test_incoming_common_params() {
+ let json = json!([0, 42, "Marionette:AcceptConnections", {"value": false}]);
+ let params = BoolValue::new(false);
+ let req = Request(
+ 42,
+ Command::Marionette(marionette::Command::AcceptConnections(params)),
+ );
+ let msg = Message::Incoming(req);
+ assert_ser_de(&msg, json);
+ }
+
+ #[test]
+ fn test_incoming_params_derived() {
+ assert!(serde_json::from_value::<Message>(
+ json!([0,42,"WebDriver:FindElement",{"using":"foo","value":"foo"}])
+ )
+ .is_err());
+ assert!(serde_json::from_value::<Message>(
+ json!([0,42,"Marionette:AcceptConnections",{"value":"foo"}])
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn test_incoming_no_params() {
+ assert!(serde_json::from_value::<Message>(
+ json!([0,42,"WebDriver:GetTimeouts",{"value":true}])
+ )
+ .is_err());
+ assert!(serde_json::from_value::<Message>(
+ json!([0,42,"Marionette:Context",{"value":"foo"}])
+ )
+ .is_err());
+ assert!(serde_json::from_value::<Message>(
+ json!([0,42,"Marionette:GetScreenOrientation",{"value":true}])
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn test_outgoing_result() {
+ let json = json!([1, 42, null, { "value": null }]);
+ let result = MarionetteResult::Null;
+ let msg = Message::Outgoing(Response::Result { id: 42, result });
+
+ assert_ser_de(&msg, json);
+ }
+
+ #[test]
+ fn test_outgoing_error() {
+ let json =
+ json!([1, 42, {"error": "no such element", "message": "", "stacktrace": ""}, null]);
+ let error = MarionetteError {
+ kind: ErrorKind::NoSuchElement,
+ message: "".into(),
+ stack: "".into(),
+ };
+ let msg = Message::Outgoing(Response::Error { id: 42, error });
+
+ assert_ser_de(&msg, json);
+ }
+
+ #[test]
+ fn test_invalid_type() {
+ assert!(
+ serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts", {}])).is_err()
+ );
+ assert!(serde_json::from_value::<Message>(json!([3, 42, "no such element", {}])).is_err());
+ }
+
+ #[test]
+ fn test_missing_fields() {
+ // all fields are required
+ assert!(
+ serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts"])).is_err()
+ );
+ assert!(serde_json::from_value::<Message>(json!([2, 42])).is_err());
+ assert!(serde_json::from_value::<Message>(json!([2])).is_err());
+ assert!(serde_json::from_value::<Message>(json!([])).is_err());
+ }
+
+ #[test]
+ fn test_unknown_command() {
+ assert!(serde_json::from_value::<Message>(json!([0, 42, "hooba", {}])).is_err());
+ }
+
+ #[test]
+ fn test_unknown_error() {
+ assert!(serde_json::from_value::<Message>(json!([1, 42, "flooba", {}])).is_err());
+ }
+
+ #[test]
+ fn test_message_id_bounds() {
+ let overflow = i64::from(std::u32::MAX) + 1;
+ let underflow = -1;
+
+ fn get_timeouts(message_id: i64) -> Value {
+ json!([0, message_id, "WebDriver:GetTimeouts", {}])
+ }
+
+ assert!(serde_json::from_value::<Message>(get_timeouts(overflow)).is_err());
+ assert!(serde_json::from_value::<Message>(get_timeouts(underflow)).is_err());
+ }
+}
diff --git a/marionette/src/result.rs b/marionette/src/result.rs
new file mode 100644
index 0000000..0f10269
--- /dev/null
+++ b/marionette/src/result.rs
@@ -0,0 +1,219 @@
+use serde::de;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde_json::Value;
+
+use crate::common::{Cookie, Timeouts, WebElement};
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct NewWindow {
+ handle: String,
+ #[serde(rename = "type")]
+ type_hint: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct WindowRect {
+ pub x: i32,
+ pub y: i32,
+ pub width: i32,
+ pub height: i32,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ElementRect {
+ pub x: f64,
+ pub y: f64,
+ pub width: f64,
+ pub height: f64,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum MarionetteResult {
+ #[serde(deserialize_with = "from_value", serialize_with = "to_value")]
+ Bool(bool),
+ #[serde(deserialize_with = "from_value", serialize_with = "to_empty_value")]
+ Null,
+ NewWindow(NewWindow),
+ WindowRect(WindowRect),
+ ElementRect(ElementRect),
+ #[serde(deserialize_with = "from_value", serialize_with = "to_value")]
+ String(String),
+ Strings(Vec<String>),
+ #[serde(deserialize_with = "from_value", serialize_with = "to_value")]
+ WebElement(WebElement),
+ WebElements(Vec<WebElement>),
+ Cookies(Vec<Cookie>),
+ Timeouts(Timeouts),
+}
+
+fn to_value<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+ T: Serialize,
+{
+ #[derive(Serialize)]
+ struct Wrapper<T> {
+ value: T,
+ }
+
+ Wrapper { value: data }.serialize(serializer)
+}
+
+fn to_empty_value<S>(serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: Serializer,
+{
+ #[derive(Serialize)]
+ struct Wrapper {
+ value: Value,
+ }
+
+ Wrapper { value: Value::Null }.serialize(serializer)
+}
+
+fn from_value<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+ D: Deserializer<'de>,
+ T: serde::de::DeserializeOwned,
+ T: std::fmt::Debug,
+{
+ #[derive(Debug, Deserialize)]
+ struct Wrapper<T> {
+ value: T,
+ }
+
+ let v = Value::deserialize(deserializer)?;
+ if v.is_object() {
+ let w = serde_json::from_value::<Wrapper<T>>(v).map_err(de::Error::custom)?;
+ Ok(w.value)
+ } else {
+ Err(de::Error::custom("Cannot be deserialized to struct"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::test::{assert_de, assert_ser_de, ELEMENT_KEY};
+ use serde_json::json;
+
+ #[test]
+ fn test_boolean_response() {
+ assert_ser_de(&MarionetteResult::Bool(true), json!({"value": true}));
+ }
+
+ #[test]
+ fn test_cookies_response() {
+ let mut data = Vec::new();
+ data.push(Cookie {
+ name: "foo".into(),
+ value: "bar".into(),
+ path: Some("/common".into()),
+ domain: Some("web-platform.test".into()),
+ secure: false,
+ http_only: false,
+ expiry: None,
+ same_site: Some("Strict".into()),
+ });
+ assert_ser_de(
+ &MarionetteResult::Cookies(data),
+ json!([{"name":"foo","value":"bar","path":"/common","domain":"web-platform.test","secure":false,"httpOnly":false,"sameSite":"Strict"}]),
+ );
+ }
+
+ #[test]
+ fn test_new_window_response() {
+ let data = NewWindow {
+ handle: "6442450945".into(),
+ type_hint: "tab".into(),
+ };
+ let json = json!({"handle": "6442450945", "type": "tab"});
+ assert_ser_de(&MarionetteResult::NewWindow(data), json);
+ }
+
+ #[test]
+ fn test_web_element_response() {
+ let data = WebElement {
+ element: "foo".into(),
+ };
+ assert_ser_de(
+ &MarionetteResult::WebElement(data),
+ json!({"value": {ELEMENT_KEY: "foo"}}),
+ );
+ }
+
+ #[test]
+ fn test_web_elements_response() {
+ let data = vec![
+ WebElement {
+ element: "foo".into(),
+ },
+ WebElement {
+ element: "bar".into(),
+ },
+ ];
+ assert_ser_de(
+ &MarionetteResult::WebElements(data),
+ json!([{ELEMENT_KEY: "foo"}, {ELEMENT_KEY: "bar"}]),
+ );
+ }
+
+ #[test]
+ fn test_timeouts_response() {
+ let data = Timeouts {
+ implicit: Some(1000),
+ page_load: Some(200000),
+ script: Some(Some(60000)),
+ };
+ assert_ser_de(
+ &MarionetteResult::Timeouts(data),
+ json!({"implicit":1000,"pageLoad":200000,"script":60000}),
+ );
+ }
+
+ #[test]
+ fn test_string_response() {
+ assert_ser_de(
+ &MarionetteResult::String("foo".into()),
+ json!({"value": "foo"}),
+ );
+ }
+
+ #[test]
+ fn test_strings_response() {
+ assert_ser_de(
+ &MarionetteResult::Strings(vec!["2147483649".to_string()]),
+ json!(["2147483649"]),
+ );
+ }
+
+ #[test]
+ fn test_null_response() {
+ assert_ser_de(&MarionetteResult::Null, json!({ "value": null }));
+ }
+
+ #[test]
+ fn test_window_rect_response() {
+ let data = WindowRect {
+ x: 100,
+ y: 100,
+ width: 800,
+ height: 600,
+ };
+ let json = json!({"x": 100, "y": 100, "width": 800, "height": 600});
+ assert_ser_de(&MarionetteResult::WindowRect(data), json);
+ }
+
+ #[test]
+ fn test_element_rect_response() {
+ let data = ElementRect {
+ x: 8.0,
+ y: 8.0,
+ width: 148.6666717529297,
+ height: 22.0,
+ };
+ let json = json!({"x": 8, "y": 8, "width": 148.6666717529297, "height": 22});
+ assert_de(&MarionetteResult::ElementRect(data), json);
+ }
+}
diff --git a/marionette/src/test.rs b/marionette/src/test.rs
new file mode 100644
index 0000000..b6309a2
--- /dev/null
+++ b/marionette/src/test.rs
@@ -0,0 +1,31 @@
+pub static ELEMENT_KEY: &'static str = "element-6066-11e4-a52e-4f735466cecf";
+
+pub fn assert_ser_de<T>(data: &T, json: serde_json::Value)
+where
+ T: std::fmt::Debug,
+ T: std::cmp::PartialEq,
+ T: serde::de::DeserializeOwned,
+ T: serde::Serialize,
+{
+ assert_eq!(serde_json::to_value(data).unwrap(), json);
+ assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
+}
+
+#[allow(dead_code)]
+pub fn assert_ser<T>(data: &T, json: serde_json::Value)
+where
+ T: std::fmt::Debug,
+ T: std::cmp::PartialEq,
+ T: serde::Serialize,
+{
+ assert_eq!(serde_json::to_value(data).unwrap(), json);
+}
+
+pub fn assert_de<T>(data: &T, json: serde_json::Value)
+where
+ T: std::fmt::Debug,
+ T: std::cmp::PartialEq,
+ T: serde::de::DeserializeOwned,
+{
+ assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
+}
diff --git a/marionette/src/webdriver.rs b/marionette/src/webdriver.rs
new file mode 100644
index 0000000..a15e1cf
--- /dev/null
+++ b/marionette/src/webdriver.rs
@@ -0,0 +1,452 @@
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::common::{from_cookie, from_name, to_cookie, to_name, Cookie, Frame, Timeouts, Window};
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Url {
+ pub url: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct LegacyWebElement {
+ pub id: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Locator {
+ pub using: Selector,
+ pub value: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Selector {
+ #[serde(rename = "css selector")]
+ CSS,
+ #[serde(rename = "link text")]
+ LinkText,
+ #[serde(rename = "partial link text")]
+ PartialLinkText,
+ #[serde(rename = "tag name")]
+ TagName,
+ #[serde(rename = "xpath")]
+ XPath,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct NewWindow {
+ #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+ pub type_hint: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct WindowRect {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub x: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub y: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub width: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub height: Option<i32>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Keys {
+ pub text: String,
+ pub value: Vec<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct PrintParameters {
+ pub orientation: PrintOrientation,
+ pub scale: f64,
+ pub background: bool,
+ pub page: PrintPage,
+ pub margin: PrintMargins,
+ pub page_ranges: Vec<String>,
+ pub shrink_to_fit: bool,
+}
+
+impl Default for PrintParameters {
+ fn default() -> Self {
+ PrintParameters {
+ orientation: PrintOrientation::default(),
+ scale: 1.0,
+ background: false,
+ page: PrintPage::default(),
+ margin: PrintMargins::default(),
+ page_ranges: Vec::new(),
+ shrink_to_fit: true,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum PrintOrientation {
+ Landscape,
+ Portrait,
+}
+
+impl Default for PrintOrientation {
+ fn default() -> Self {
+ PrintOrientation::Portrait
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct PrintPage {
+ pub width: f64,
+ pub height: f64,
+}
+
+impl Default for PrintPage {
+ fn default() -> Self {
+ PrintPage {
+ width: 21.59,
+ height: 27.94,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct PrintMargins {
+ pub top: f64,
+ pub bottom: f64,
+ pub left: f64,
+ pub right: f64,
+}
+
+impl Default for PrintMargins {
+ fn default() -> Self {
+ PrintMargins {
+ top: 1.0,
+ bottom: 1.0,
+ left: 1.0,
+ right: 1.0,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ScreenshotOptions {
+ pub id: Option<String>,
+ pub highlights: Vec<Option<String>>,
+ pub full: bool,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Script {
+ pub script: String,
+ pub args: Option<Vec<Value>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Command {
+ // Needs to be updated to "WebDriver:AcceptAlert" for Firefox 63
+ #[serde(rename = "WebDriver:AcceptDialog")]
+ AcceptAlert,
+ #[serde(
+ rename = "WebDriver:AddCookie",
+ serialize_with = "to_cookie",
+ deserialize_with = "from_cookie"
+ )]
+ AddCookie(Cookie),
+ #[serde(rename = "WebDriver:CloseWindow")]
+ CloseWindow,
+ #[serde(
+ rename = "WebDriver:DeleteCookie",
+ serialize_with = "to_name",
+ deserialize_with = "from_name"
+ )]
+ DeleteCookie(String),
+ #[serde(rename = "WebDriver:DeleteAllCookies")]
+ DeleteCookies,
+ #[serde(rename = "WebDriver:DismissAlert")]
+ DismissAlert,
+ #[serde(rename = "WebDriver:ElementClear")]
+ ElementClear(LegacyWebElement),
+ #[serde(rename = "WebDriver:ElementClick")]
+ ElementClick(LegacyWebElement),
+ #[serde(rename = "WebDriver:ElementSendKeys")]
+ ElementSendKeys {
+ id: String,
+ text: String,
+ value: Vec<String>,
+ },
+ #[serde(rename = "WebDriver:ExecuteAsyncScript")]
+ ExecuteAsyncScript(Script),
+ #[serde(rename = "WebDriver:ExecuteScript")]
+ ExecuteScript(Script),
+ #[serde(rename = "WebDriver:FindElement")]
+ FindElement(Locator),
+ #[serde(rename = "WebDriver:FindElements")]
+ FindElements(Locator),
+ #[serde(rename = "WebDriver:FindElement")]
+ FindElementElement {
+ element: String,
+ using: Selector,
+ value: String,
+ },
+ #[serde(rename = "WebDriver:FindElements")]
+ FindElementElements {
+ element: String,
+ using: Selector,
+ value: String,
+ },
+ #[serde(rename = "WebDriver:FullscreenWindow")]
+ FullscreenWindow,
+ #[serde(rename = "WebDriver:Navigate")]
+ Get(Url),
+ #[serde(rename = "WebDriver:GetActiveElement")]
+ GetActiveElement,
+ #[serde(rename = "WebDriver:GetAlertText")]
+ GetAlertText,
+ #[serde(rename = "WebDriver:GetCookies")]
+ GetCookies,
+ #[serde(rename = "WebDriver:GetElementCSSValue")]
+ GetCSSValue {
+ id: String,
+ #[serde(rename = "propertyName")]
+ property: String,
+ },
+ #[serde(rename = "WebDriver:GetCurrentURL")]
+ GetCurrentUrl,
+ #[serde(rename = "WebDriver:GetElementAttribute")]
+ GetElementAttribute { id: String, name: String },
+ #[serde(rename = "WebDriver:GetElementProperty")]
+ GetElementProperty { id: String, name: String },
+ #[serde(rename = "WebDriver:GetElementRect")]
+ GetElementRect(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetElementTagName")]
+ GetElementTagName(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetElementText")]
+ GetElementText(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetPageSource")]
+ GetPageSource,
+ #[serde(rename = "WebDriver:GetTimeouts")]
+ GetTimeouts,
+ #[serde(rename = "WebDriver:GetTitle")]
+ GetTitle,
+ #[serde(rename = "WebDriver:GetWindowHandle")]
+ GetWindowHandle,
+ #[serde(rename = "WebDriver:GetWindowHandles")]
+ GetWindowHandles,
+ #[serde(rename = "WebDriver:GetWindowRect")]
+ GetWindowRect,
+ #[serde(rename = "WebDriver:Back")]
+ GoBack,
+ #[serde(rename = "WebDriver:Forward")]
+ GoForward,
+ #[serde(rename = "WebDriver:IsElementDisplayed")]
+ IsDisplayed(LegacyWebElement),
+ #[serde(rename = "WebDriver:IsElementEnabled")]
+ IsEnabled(LegacyWebElement),
+ #[serde(rename = "WebDriver:IsElementSelected")]
+ IsSelected(LegacyWebElement),
+ #[serde(rename = "WebDriver:MaximizeWindow")]
+ MaximizeWindow,
+ #[serde(rename = "WebDriver:MinimizeWindow")]
+ MinimizeWindow,
+ #[serde(rename = "WebDriver:NewWindow")]
+ NewWindow(NewWindow),
+ #[serde(rename = "WebDriver:Print")]
+ Print(PrintParameters),
+ #[serde(rename = "WebDriver:Refresh")]
+ Refresh,
+ #[serde(rename = "WebDriver:ReleaseActions")]
+ ReleaseActions,
+ #[serde(rename = "WebDriver:SendAlertText")]
+ SendAlertText(Keys),
+ #[serde(rename = "WebDriver:SetTimeouts")]
+ SetTimeouts(Timeouts),
+ #[serde(rename = "WebDriver:SetWindowRect")]
+ SetWindowRect(WindowRect),
+ #[serde(rename = "WebDriver:SwitchToFrame")]
+ SwitchToFrame(Frame),
+ #[serde(rename = "WebDriver:SwitchToParentFrame")]
+ SwitchToParentFrame,
+ #[serde(rename = "WebDriver:SwitchToWindow")]
+ SwitchToWindow(Window),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeElementScreenshot(ScreenshotOptions),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeFullScreenshot(ScreenshotOptions),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeScreenshot(ScreenshotOptions),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::common::Date;
+ use crate::test::{assert_ser, assert_ser_de};
+ use serde_json::json;
+
+ #[test]
+ fn test_json_screenshot() {
+ let data = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: false,
+ };
+ let json = json!({"full":false,"highlights":[],"id":null});
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_selector_css() {
+ assert_ser_de(&Selector::CSS, json!("css selector"));
+ }
+
+ #[test]
+ fn test_json_selector_link_text() {
+ assert_ser_de(&Selector::LinkText, json!("link text"));
+ }
+
+ #[test]
+ fn test_json_selector_partial_link_text() {
+ assert_ser_de(&Selector::PartialLinkText, json!("partial link text"));
+ }
+
+ #[test]
+ fn test_json_selector_tag_name() {
+ assert_ser_de(&Selector::TagName, json!("tag name"));
+ }
+
+ #[test]
+ fn test_json_selector_xpath() {
+ assert_ser_de(&Selector::XPath, json!("xpath"));
+ }
+
+ #[test]
+ fn test_json_selector_invalid() {
+ assert!(serde_json::from_value::<Selector>(json!("foo")).is_err());
+ }
+
+ #[test]
+ fn test_json_locator() {
+ let json = json!({
+ "using": "partial link text",
+ "value": "link text",
+ });
+ let data = Locator {
+ using: Selector::PartialLinkText,
+ value: "link text".into(),
+ };
+
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_keys() {
+ let data = Keys {
+ text: "Foo".into(),
+ value: vec!["F".into(), "o".into(), "o".into()],
+ };
+ let json = json!({"text": "Foo", "value": ["F", "o", "o"]});
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_new_window() {
+ let data = NewWindow {
+ type_hint: Some("foo".into()),
+ };
+ assert_ser_de(&data, json!({ "type": "foo" }));
+ }
+
+ #[test]
+ fn test_json_window_rect() {
+ let data = WindowRect {
+ x: Some(123),
+ y: None,
+ width: None,
+ height: None,
+ };
+ assert_ser_de(&data, json!({"x": 123}));
+ }
+
+ #[test]
+ fn test_command_with_params() {
+ let locator = Locator {
+ using: Selector::CSS,
+ value: "value".into(),
+ };
+ let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}});
+ assert_ser_de(&Command::FindElement(locator), json);
+ }
+
+ #[test]
+ fn test_command_with_wrapper_params() {
+ let cookie = Cookie {
+ name: "hello".into(),
+ value: "world".into(),
+ path: None,
+ domain: None,
+ secure: false,
+ http_only: false,
+ expiry: Some(Date(1564488092)),
+ same_site: None,
+ };
+ let json = json!({"WebDriver:AddCookie": {"cookie": {"name": "hello", "value": "world", "secure": false, "httpOnly": false, "expiry": 1564488092}}});
+ assert_ser_de(&Command::AddCookie(cookie), json);
+ }
+
+ #[test]
+ fn test_empty_commands() {
+ assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts"));
+ }
+
+ #[test]
+ fn test_json_command_invalid() {
+ assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
+ }
+
+ #[test]
+ fn test_json_delete_cookie_command() {
+ let json = json!({"WebDriver:DeleteCookie": {"name": "foo"}});
+ assert_ser_de(&Command::DeleteCookie("foo".into()), json);
+ }
+
+ #[test]
+ fn test_json_new_window_command() {
+ let data = NewWindow {
+ type_hint: Some("foo".into()),
+ };
+ let json = json!({"WebDriver:NewWindow": {"type": "foo"}});
+ assert_ser_de(&Command::NewWindow(data), json);
+ }
+
+ #[test]
+ fn test_json_new_window_command_with_none_value() {
+ let data = NewWindow { type_hint: None };
+ let json = json!({"WebDriver:NewWindow": {}});
+ assert_ser_de(&Command::NewWindow(data), json);
+ }
+
+ #[test]
+ fn test_json_command_as_struct() {
+ assert_ser(
+ &Command::FindElementElement {
+ element: "foo".into(),
+ using: Selector::XPath,
+ value: "bar".into(),
+ },
+ json!({"WebDriver:FindElement": {"element": "foo", "using": "xpath", "value": "bar" }}),
+ );
+ }
+
+ #[test]
+ fn test_json_get_css_value() {
+ assert_ser_de(
+ &Command::GetCSSValue {
+ id: "foo".into(),
+ property: "bar".into(),
+ },
+ json!({"WebDriver:GetElementCSSValue": {"id": "foo", "propertyName": "bar"}}),
+ );
+ }
+}
diff --git a/moz.build b/moz.build
index 1dd7026..9f19159 100644
--- a/moz.build
+++ b/moz.build
@@ -9,9 +9,11 @@ AllowCompilerWarnings()
RUST_TESTS = [
"geckodriver",
"webdriver",
+ "marionette",
# TODO: Move to mozbase/rust/moz.build once those crates can be
# tested separately.
+ # "mozdevice", // Tests require adb, and cannot be run in CI
"mozprofile",
"mozrunner",
"mozversion",
@@ -20,7 +22,7 @@ RUST_TESTS = [
with Files("**"):
BUG_COMPONENT = ("Testing", "geckodriver")
-SPHINX_TREES["geckodriver"] = "doc"
+SPHINX_TREES["/testing/geckodriver"] = "doc"
-with Files('doc/**'):
- SCHEDULES.exclusive = ['docs']
+with Files("doc/**"):
+ SCHEDULES.exclusive = ["docs"]
diff --git a/src/android.rs b/src/android.rs
new file mode 100644
index 0000000..87cce8b
--- /dev/null
+++ b/src/android.rs
@@ -0,0 +1,376 @@
+use crate::capabilities::AndroidOptions;
+use mozdevice::{Device, Host};
+use mozprofile::profile::Profile;
+use serde::Serialize;
+use serde_yaml::{Mapping, Value};
+use std::fmt;
+use std::io;
+use std::path::PathBuf;
+use std::time;
+
+// TODO: avoid port clashes across GeckoView-vehicles.
+// For now, we always use target port 2829, leading to issues like bug 1533704.
+const TARGET_PORT: u16 = 2829;
+
+const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML
+##
+## Auto-generated by geckodriver.
+## See https://mozilla.github.io/geckoview/consumer/docs/automation.
+"#;
+
+pub type Result<T> = std::result::Result<T, AndroidError>;
+
+#[derive(Debug)]
+pub enum AndroidError {
+ ActivityNotFound(String),
+ Device(mozdevice::DeviceError),
+ IO(io::Error),
+ NotConnected,
+ PackageNotFound(String),
+ Serde(serde_yaml::Error),
+}
+
+impl fmt::Display for AndroidError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ AndroidError::ActivityNotFound(ref package) => {
+ write!(f, "Activity for package '{}' not found", package)
+ }
+ AndroidError::Device(ref message) => message.fmt(f),
+ AndroidError::IO(ref message) => message.fmt(f),
+ AndroidError::NotConnected => write!(f, "Not connected to any Android device"),
+ AndroidError::PackageNotFound(ref package) => {
+ write!(f, "Package '{}' not found", package)
+ }
+ AndroidError::Serde(ref message) => message.fmt(f),
+ }
+ }
+}
+
+impl From<io::Error> for AndroidError {
+ fn from(value: io::Error) -> AndroidError {
+ AndroidError::IO(value)
+ }
+}
+
+impl From<mozdevice::DeviceError> for AndroidError {
+ fn from(value: mozdevice::DeviceError) -> AndroidError {
+ AndroidError::Device(value)
+ }
+}
+
+impl From<serde_yaml::Error> for AndroidError {
+ fn from(value: serde_yaml::Error) -> AndroidError {
+ AndroidError::Serde(value)
+ }
+}
+
+/// A remote Gecko instance.
+///
+/// Host refers to the device running `geckodriver`. Target refers to the
+/// Android device running Gecko in a GeckoView-based vehicle.
+#[derive(Debug)]
+pub struct AndroidProcess {
+ pub device: Device,
+ pub package: String,
+ pub activity: String,
+}
+
+impl AndroidProcess {
+ pub fn new(
+ device: Device,
+ package: String,
+ activity: String,
+ ) -> mozdevice::Result<AndroidProcess> {
+ Ok(AndroidProcess {
+ device,
+ package,
+ activity,
+ })
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct AndroidHandler {
+ pub config: PathBuf,
+ pub options: AndroidOptions,
+ pub process: Option<AndroidProcess>,
+ pub profile: PathBuf,
+
+ // For port forwarding host => target
+ pub host_port: u16,
+ pub target_port: u16,
+}
+
+impl Drop for AndroidHandler {
+ fn drop(&mut self) {
+ // Try to clean up various settings
+ if let Some(ref process) = self.process {
+ let clear_command = format!("am clear-debug-app {}", process.package);
+ match process.device.execute_host_shell_command(&clear_command) {
+ Ok(_) => debug!("Disabled reading from configuration file"),
+ Err(e) => error!("Failed disabling from configuration file: {}", e),
+ }
+
+ let remove_command = format!("rm -rf {}", self.config.display());
+ match process.device.execute_host_shell_command(&remove_command) {
+ Ok(_) => debug!("Deleted GeckoView configuration file"),
+ Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
+ }
+
+ match process.device.kill_forward_port(self.host_port) {
+ Ok(_) => debug!(
+ "Android port forward ({} -> {}) stopped",
+ &self.host_port, &self.target_port
+ ),
+ Err(e) => error!(
+ "Android port forward ({} -> {}) failed to stop: {}",
+ &self.host_port, &self.target_port, e
+ ),
+ }
+ }
+ }
+}
+
+impl AndroidHandler {
+ pub fn new(options: &AndroidOptions) -> AndroidHandler {
+ // We need to push profile.pathbuf to a safe space on the device.
+ // Make it per-Android package to avoid clashes and confusion.
+ // This naming scheme follows GeckoView's configuration file naming scheme,
+ // see bug 1533385.
+ let profile = PathBuf::from(format!(
+ "/mnt/sdcard/{}-geckodriver-profile",
+ &options.package
+ ));
+
+ let config = PathBuf::from(format!(
+ "/data/local/tmp/{}-geckoview-config.yaml",
+ &options.package
+ ));
+
+ AndroidHandler {
+ options: options.clone(),
+ profile,
+ config,
+ process: None,
+ ..Default::default()
+ }
+ }
+
+ pub fn connect(&mut self, host_port: u16) -> Result<()> {
+ let host = Host {
+ host: None,
+ port: None,
+ read_timeout: Some(time::Duration::from_millis(5000)),
+ write_timeout: Some(time::Duration::from_millis(5000)),
+ };
+
+ let device = host.device_or_default(self.options.device_serial.as_ref())?;
+
+ self.host_port = host_port;
+ self.target_port = TARGET_PORT;
+
+ // Set up port forward. Port forwarding will be torn down, if possible,
+ device.forward_port(self.host_port, self.target_port)?;
+ debug!(
+ "Android port forward ({} -> {}) started",
+ &self.host_port, &self.target_port
+ );
+
+ // Check if the specified package is installed
+ let response = device
+ .execute_host_shell_command(&format!("pm list packages {}", &self.options.package))?;
+ let packages = response
+ .split_terminator('\n')
+ .filter(|line| line.starts_with("package:"))
+ .map(|line| line.rsplit(':').next().expect("Package name found"))
+ .collect::<Vec<&str>>();
+ if !packages.contains(&self.options.package.as_str()) {
+ return Err(AndroidError::PackageNotFound(self.options.package.clone()));
+ }
+
+ // If activity hasn't been specified default to the main activity of the package
+ let activity = match self.options.activity {
+ Some(ref activity) => activity.clone(),
+ None => {
+ let response = device.execute_host_shell_command(&format!(
+ "cmd package resolve-activity --brief {}",
+ &self.options.package
+ ))?;
+ let activities = response
+ .split_terminator('\n')
+ .filter(|line| line.starts_with(&self.options.package))
+ .map(|line| line.rsplit('/').next().unwrap())
+ .collect::<Vec<&str>>();
+ if activities.is_empty() {
+ return Err(AndroidError::ActivityNotFound(self.options.package.clone()));
+ }
+
+ activities[0].to_owned()
+ }
+ };
+
+ self.process = Some(AndroidProcess::new(
+ device,
+ self.options.package.clone(),
+ activity,
+ )?);
+
+ Ok(())
+ }
+
+ pub fn generate_config_file<I, K, V>(&self, envs: I) -> Result<String>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: ToString,
+ V: ToString,
+ {
+ // To configure GeckoView, we use the automation techniques documented at
+ // https://mozilla.github.io/geckoview/consumer/docs/automation.
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ pub struct Config {
+ pub env: Mapping,
+ pub args: Value,
+ }
+
+ // TODO: Allow to write custom arguments and preferences from moz:firefoxOptions
+ let mut config = Config {
+ args: Value::Sequence(vec![
+ Value::String("--marionette".into()),
+ Value::String("--profile".into()),
+ Value::String(self.profile.display().to_string()),
+ ]),
+ env: Mapping::new(),
+ };
+
+ for (key, value) in envs {
+ config.env.insert(
+ Value::String(key.to_string()),
+ Value::String(value.to_string()),
+ );
+ }
+
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER".to_owned()),
+ Value::String("1".to_owned()),
+ );
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()),
+ Value::String("1".to_owned()),
+ );
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()),
+ Value::String("1".to_owned()),
+ );
+
+ let mut contents: Vec<String> = vec![CONFIG_FILE_HEADING.to_owned()];
+ contents.push(serde_yaml::to_string(&config)?);
+
+ Ok(contents.concat())
+ }
+
+ pub fn prepare<I, K, V>(&self, profile: &Profile, env: I) -> Result<()>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: ToString,
+ V: ToString,
+ {
+ match self.process {
+ Some(ref process) => {
+ process.device.clear_app_data(&process.package)?;
+
+ // These permissions, at least, are required to read profiles in /mnt/sdcard.
+ for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
+ process.device.execute_host_shell_command(&format!(
+ "pm grant {} android.permission.{}",
+ &process.package, perm
+ ))?;
+ }
+
+ debug!("Deleting {}", self.profile.display());
+ process
+ .device
+ .execute_host_shell_command(&format!("rm -rf {}", self.profile.display()))?;
+
+ debug!(
+ "Pushing {} to {}",
+ profile.path.display(),
+ self.profile.display()
+ );
+ process
+ .device
+ .push_dir(&profile.path, &self.profile, 0o777)?;
+
+ let contents = self.generate_config_file(env)?;
+ debug!("Content of generated GeckoView config file:\n{}", contents);
+ let reader = &mut io::BufReader::new(contents.as_bytes());
+
+ debug!(
+ "Pushing GeckoView configuration file to {}",
+ self.config.display()
+ );
+ process.device.push(reader, &self.config, 0o777)?;
+
+ // Bug 1584966: File permissions are not correctly set by push()
+ process
+ .device
+ .execute_host_shell_command(&format!("chmod a+rw {}", self.config.display()))?;
+
+ // Tell GeckoView to read configuration even when `android:debuggable="false"`.
+ process.device.execute_host_shell_command(&format!(
+ "am set-debug-app --persistent {}",
+ process.package
+ ))?;
+ }
+ None => return Err(AndroidError::NotConnected),
+ }
+
+ Ok(())
+ }
+
+ pub fn launch(&self) -> Result<()> {
+ match self.process {
+ Some(ref process) => {
+ // TODO: Remove the usage of intent arguments once Fennec is no longer
+ // supported. Packages which are using GeckoView always read the arguments
+ // via the YAML configuration file.
+ let mut intent_arguments = self
+ .options
+ .intent_arguments
+ .clone()
+ .unwrap_or_else(|| Vec::with_capacity(3));
+ intent_arguments.push("--es".to_owned());
+ intent_arguments.push("args".to_owned());
+ intent_arguments
+ .push(format!("--marionette --profile {}", self.profile.display()).to_owned());
+
+ debug!("Launching {}/{}", process.package, process.activity);
+ process
+ .device
+ .launch(&process.package, &process.activity, &intent_arguments)
+ .map_err(|e| {
+ let message = format!(
+ "Could not launch Android {}/{}: {}",
+ process.package, process.activity, e
+ );
+ mozdevice::DeviceError::Adb(message)
+ })?;
+ }
+ None => return Err(AndroidError::NotConnected),
+ }
+
+ Ok(())
+ }
+
+ pub fn force_stop(&self) -> Result<()> {
+ match &self.process {
+ Some(process) => {
+ debug!("Force stopping the Android package: {}", &process.package);
+ process.device.force_stop(&process.package)?;
+ }
+ None => return Err(AndroidError::NotConnected),
+ }
+
+ Ok(())
+ }
+}
diff --git a/src/build.rs b/src/build.rs
index 7119daf..c959033 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -38,3 +38,8 @@ impl Into<Value> for BuildInfo {
Value::String(BuildInfo::version().to_string())
}
}
+
+/// Returns build-time information about geckodriver.
+pub fn build_info() -> BuildInfo {
+ BuildInfo {}
+}
diff --git a/src/capabilities.rs b/src/capabilities.rs
index ed85705..ef1b0d7 100644
--- a/src/capabilities.rs
+++ b/src/capabilities.rs
@@ -1,6 +1,6 @@
-use base64;
use crate::command::LogOptions;
use crate::logging::Level;
+use base64;
use mozprofile::preferences::Pref;
use mozprofile::profile::Profile;
use mozrunner::runner::platform::firefox_default_path;
@@ -9,7 +9,6 @@ use regex::bytes::Regex;
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use std::default::Default;
-use std::error::Error;
use std::fs;
use std::io;
use std::io::BufWriter;
@@ -38,7 +37,7 @@ impl<'a> FirefoxCapabilities<'a> {
pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
FirefoxCapabilities {
chosen_binary: None,
- fallback_binary: fallback_binary,
+ fallback_binary,
version_cache: BTreeMap::new(),
}
}
@@ -49,12 +48,12 @@ impl<'a> FirefoxCapabilities<'a> {
.and_then(|x| x.get("binary"))
.and_then(|x| x.as_str())
.map(PathBuf::from)
- .or_else(|| self.fallback_binary.map(|x| x.clone()))
- .or_else(firefox_default_path)
+ .or_else(|| self.fallback_binary.cloned())
+ .or_else(firefox_default_path);
}
- fn version(&mut self) -> Option<String> {
- if let Some(ref binary) = self.chosen_binary {
+ fn version(&mut self, binary: Option<&Path>) -> Option<String> {
+ if let Some(binary) = binary {
if let Some(value) = self.version_cache.get(binary) {
return Some((*value).clone());
}
@@ -68,7 +67,8 @@ impl<'a> FirefoxCapabilities<'a> {
});
if let Some(ref version) = rv {
debug!("Found version {}", version);
- self.version_cache.insert(binary.clone(), version.clone());
+ self.version_cache
+ .insert(binary.to_path_buf(), version.clone());
} else {
debug!("Failed to get binary version");
}
@@ -78,11 +78,11 @@ impl<'a> FirefoxCapabilities<'a> {
}
}
- fn version_from_binary(&self, binary: &PathBuf) -> Option<String> {
- let version_regexp =
- Regex::new(r#"\d+\.\d+(?:[a-z]\d+)?"#).expect("Error parsing version regexp");
+ fn version_from_binary(&self, binary: &Path) -> Option<String> {
+ let version_regexp = Regex::new(r#"Mozilla Firefox [0-9]+\.[0-9]+(?:[a-z][0-9]+)?"#)
+ .expect("Error parsing version regexp");
let output = Command::new(binary)
- .args(&["-version"])
+ .args(&["--version"])
.stdout(Stdio::piped())
.spawn()
.and_then(|child| child.wait_with_output())
@@ -102,10 +102,7 @@ impl<'a> FirefoxCapabilities<'a> {
// TODO: put this in webdriver-rust
fn convert_version_error(err: mozversion::Error) -> WebDriverError {
- WebDriverError::new(
- ErrorStatus::SessionNotCreated,
- err.description().to_string(),
- )
+ WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
}
impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
@@ -118,7 +115,8 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
}
fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
- Ok(self.version())
+ let binary = self.chosen_binary.clone();
+ Ok(self.version(binary.as_ref().map(|x| x.as_ref())))
}
fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
@@ -134,7 +132,8 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
}
fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
- let version_str = self.version();
+ let binary = self.chosen_binary.clone();
+ let version_str = self.version(binary.as_ref().map(|x| x.as_ref()));
if let Some(x) = version_str {
Ok(Version::from_str(&*x)
.or_else(|x| Err(convert_version_error(x)))?
@@ -168,7 +167,7 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
Ok(true)
}
- fn validate_custom(&self, name: &str, value: &Value) -> WebDriverResult<()> {
+ fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> {
if !name.starts_with("moz:") {
return Ok(());
}
@@ -181,33 +180,59 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
);
for (key, value) in data.iter() {
match &**key {
- "binary" => {
+ "androidActivity"
+ | "androidDeviceSerial"
+ | "androidPackage"
+ | "profile" => {
if !value.is_string() {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
- "binary path is not a string",
+ format!("{} is not a string", &**key),
));
}
}
- "args" => {
+ "androidIntentArguments" | "args" => {
if !try_opt!(
value.as_array(),
ErrorStatus::InvalidArgument,
- "args is not an array"
- ).iter()
- .all(|value| value.is_string())
+ format!("{} is not an array", &**key)
+ )
+ .iter()
+ .all(|value| value.is_string())
{
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
- "args entry is not a string",
+ format!("{} entry is not a string", &**key),
));
}
}
- "profile" => {
- if !value.is_string() {
+ "binary" => {
+ if let Some(binary) = value.as_str() {
+ if !data.contains_key("androidPackage")
+ && self.version(Some(Path::new(binary))).is_none()
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a Firefox executable", &**key),
+ ));
+ }
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "env" => {
+ let env_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "env value is not an object"
+ );
+ if !env_data.values().all(Value::is_string) {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
- "profile is not a string",
+ "Environment values were not all strings",
));
}
}
@@ -247,9 +272,10 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
ErrorStatus::InvalidArgument,
"prefs value is not an object"
);
- if !prefs_data.values().all(|x| {
+ let is_pref_value_type = |x: &Value| {
x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean()
- }) {
+ };
+ if !prefs_data.values().all(is_pref_value_type) {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"Preference values not all string or integer or boolean",
@@ -296,6 +322,26 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
}
}
+/// Android-specific options in the `moz:firefoxOptions` struct.
+/// These map to "androidCamelCase", following [chromedriver's Android-specific
+/// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android).
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct AndroidOptions {
+ pub activity: Option<String>,
+ pub device_serial: Option<String>,
+ pub intent_arguments: Option<Vec<String>>,
+ pub package: String,
+}
+
+impl AndroidOptions {
+ fn new(package: String) -> AndroidOptions {
+ AndroidOptions {
+ package,
+ ..Default::default()
+ }
+ }
+}
+
/// Rust representation of `moz:firefoxOptions`.
///
/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
@@ -307,8 +353,10 @@ pub struct FirefoxOptions {
pub binary: Option<PathBuf>,
pub profile: Option<Profile>,
pub args: Option<Vec<String>>,
+ pub env: Option<Vec<(String, String)>>,
pub log: LogOptions,
pub prefs: Vec<(String, Pref)>,
+ pub android: Option<AndroidOptions>,
}
impl FirefoxOptions {
@@ -324,16 +372,20 @@ impl FirefoxOptions {
rv.binary = binary_path;
if let Some(json) = matched.remove("moz:firefoxOptions") {
- let options = json.as_object().ok_or(WebDriverError::new(
- ErrorStatus::InvalidArgument,
- "'moz:firefoxOptions' \
- capability is not an object"
- ))?;
+ let options = json.as_object().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "'moz:firefoxOptions' \
+ capability is not an object",
+ )
+ })?;
- rv.profile = FirefoxOptions::load_profile(&options)?;
+ rv.android = FirefoxOptions::load_android(&options)?;
rv.args = FirefoxOptions::load_args(&options)?;
+ rv.env = FirefoxOptions::load_env(&options)?;
rv.log = FirefoxOptions::load_log(&options)?;
rv.prefs = FirefoxOptions::load_prefs(&options)?;
+ rv.profile = FirefoxOptions::load_profile(&options)?;
}
Ok(rv)
@@ -341,21 +393,20 @@ impl FirefoxOptions {
fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> {
if let Some(profile_json) = options.get("profile") {
- let profile_base64 = profile_json.as_str().ok_or(WebDriverError::new(
- ErrorStatus::UnknownError,
- "Profile is not a string"
- ))?;
+ let profile_base64 = profile_json.as_str().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
+ })?;
let profile_zip = &*base64::decode(profile_base64)?;
// Create an emtpy profile directory
- let profile = Profile::new(None)?;
+ let profile = Profile::new()?;
unzip_buffer(
profile_zip,
profile
.temp_dir
.as_ref()
.expect("Profile doesn't have a path")
- .path()
+ .path(),
)?;
Ok(Some(profile))
@@ -366,43 +417,72 @@ impl FirefoxOptions {
fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> {
if let Some(args_json) = options.get("args") {
- let args_array = args_json.as_array().ok_or(WebDriverError::new(
- ErrorStatus::UnknownError,
- "Arguments were not an \
- array"
- ))?;
+ let args_array = args_json.as_array().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Arguments were not an \
+ array",
+ )
+ })?;
let args = args_array
.iter()
.map(|x| x.as_str().map(|x| x.to_owned()))
.collect::<Option<Vec<String>>>()
- .ok_or(WebDriverError::new(
- ErrorStatus::UnknownError,
- "Arguments entries were not all \
- strings"
- ))?;
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Arguments entries were not all strings",
+ )
+ })?;
Ok(Some(args))
} else {
Ok(None)
}
}
+ pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> {
+ if let Some(env_data) = options.get("env") {
+ let env = env_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object")
+ })?;
+ let mut rv = Vec::with_capacity(env.len());
+ for (key, value) in env.iter() {
+ rv.push((
+ key.clone(),
+ value
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Env value is not a string",
+ )
+ })?
+ .to_string(),
+ ));
+ }
+ Ok(Some(rv))
+ } else {
+ Ok(None)
+ }
+ }
+
fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> {
if let Some(json) = options.get("log") {
- let log = json.as_object().ok_or(WebDriverError::new(
- ErrorStatus::InvalidArgument,
- "Log section is not an object",
- ))?;
+ let log = json.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object")
+ })?;
let level = match log.get("level") {
Some(json) => {
- let s = json.as_str().ok_or(WebDriverError::new(
- ErrorStatus::InvalidArgument,
- "Log level is not a string",
- ))?;
- Some(Level::from_str(s).ok().ok_or(WebDriverError::new(
- ErrorStatus::InvalidArgument,
- "Log level is unknown",
- ))?)
+ let s = json.as_str().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Log level is not a string",
+ )
+ })?;
+ Some(Level::from_str(s).ok().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown")
+ })?)
}
None => None,
};
@@ -415,10 +495,9 @@ impl FirefoxOptions {
pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> {
if let Some(prefs_data) = options.get("prefs") {
- let prefs = prefs_data.as_object().ok_or(WebDriverError::new(
- ErrorStatus::UnknownError,
- "Prefs were not an object"
- ))?;
+ let prefs = prefs_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object")
+ })?;
let mut rv = Vec::with_capacity(prefs.len());
for (key, value) in prefs.iter() {
rv.push((key.clone(), pref_from_json(value)?));
@@ -428,13 +507,105 @@ impl FirefoxOptions {
Ok(vec![])
}
}
+
+ pub fn load_android(options: &Capabilities) -> WebDriverResult<Option<AndroidOptions>> {
+ if let Some(package_json) = options.get("androidPackage") {
+ let package = package_json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidPackage is not a string",
+ )
+ })?
+ .to_owned();
+
+ // https://developer.android.com/studio/build/application-id
+ let package_regexp =
+ Regex::new(r#"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$"#).unwrap();
+ if !package_regexp.is_match(package.as_bytes()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Not a valid androidPackage name",
+ ));
+ }
+
+ let mut android = AndroidOptions::new(package);
+
+ android.activity = match options.get("androidActivity") {
+ Some(json) => {
+ let activity = json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity is not a string",
+ )
+ })?
+ .to_owned();
+
+ if activity.contains("/") {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity should not contain '/",
+ ));
+ }
+
+ Some(activity)
+ }
+ None => None,
+ };
+
+ android.device_serial = match options.get("androidDeviceSerial") {
+ Some(json) => Some(
+ json.as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidDeviceSerial is not a string",
+ )
+ })?
+ .to_owned(),
+ ),
+ None => None,
+ };
+
+ android.intent_arguments = match options.get("androidIntentArguments") {
+ Some(json) => {
+ let args_array = json.as_array().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments is not an array",
+ )
+ })?;
+ let args = args_array
+ .iter()
+ .map(|x| x.as_str().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments entries are not all strings",
+ )
+ })?;
+
+ Some(args)
+ }
+ None => None,
+ };
+
+ Ok(Some(android))
+ } else {
+ Ok(None)
+ }
+ }
}
fn pref_from_json(value: &Value) -> WebDriverResult<Pref> {
- match value {
- &Value::String(ref x) => Ok(Pref::new(x.clone())),
- &Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())),
- &Value::Bool(x) => Ok(Pref::new(x)),
+ match *value {
+ Value::String(ref x) => Ok(Pref::new(x.clone())),
+ Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())),
+ Value::Bool(x) => Ok(Pref::new(x)),
_ => Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Could not convert pref value to string, boolean, or integer",
@@ -456,7 +627,7 @@ fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
})?;
let unzip_path = {
let name = file.name();
- let is_dir = name.ends_with("/");
+ let is_dir = name.ends_with('/');
let rel_path = Path::new(name);
let dest_path = dest_dir.join(rel_path);
@@ -498,9 +669,11 @@ fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
mod tests {
extern crate mozprofile;
- use self::mozprofile::preferences::Pref;
use super::*;
use crate::marionette::MarionetteHandler;
+
+ use self::mozprofile::preferences::Pref;
+ use serde_json::json;
use std::default::Default;
use std::fs::File;
use std::io::Read;
@@ -514,11 +687,234 @@ mod tests {
Value::String(base64::encode(&profile_data))
}
- fn make_options(firefox_opts: Capabilities) -> FirefoxOptions {
+ fn make_options(firefox_opts: Capabilities) -> WebDriverResult<FirefoxOptions> {
let mut caps = Capabilities::new();
caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
- let binary = None;
- FirefoxOptions::from_capabilities(binary, &mut caps).unwrap()
+
+ FirefoxOptions::from_capabilities(None, &mut caps)
+ }
+
+ #[test]
+ fn fx_options_default() {
+ let opts = FirefoxOptions::new();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ // Profile doesn't support PartialEq
+ // assert_eq!(opts.profile, None);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_no_binary_and_caps() {
+ let mut caps = Capabilities::new();
+
+ let opts = FirefoxOptions::from_capabilities(None, &mut caps).unwrap();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_binary_and_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert(
+ "moz:firefoxOptions".into(),
+ Value::Object(Capabilities::new()),
+ );
+
+ let binary = PathBuf::from("foo");
+
+ let opts = FirefoxOptions::from_capabilities(Some(binary.clone()), &mut caps).unwrap();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, Some(binary));
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_invalid_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), json!(42));
+
+ FirefoxOptions::from_capabilities(None, &mut caps)
+ .expect_err("Firefox options need to be of type object");
+ }
+
+ #[test]
+ fn fx_options_android_no_package() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidAvtivity".into(), json!("foo"));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ assert_eq!(opts.android, None);
+ }
+
+ #[test]
+ fn fx_options_android_package_valid_value() {
+ for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ assert_eq!(opts.android, Some(AndroidOptions::new(value.to_string())));
+ }
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_value() {
+ for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_valid_value() {
+ for value in ["cheese", "Cheese_9"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!(value));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ activity: Some(value.to_string()),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese"));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_device_serial() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!("cheese"));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ device_serial: Some("cheese".to_owned()),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+
+ #[test]
+ fn fx_options_android_serial_invalid() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"]));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ intent_arguments: Some(vec!["lorem".to_owned(), "ipsum".to_owned()]),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_no_array() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42]));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into()));
+ env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ let mut opts = make_options(firefox_opts).expect("valid firefox options");
+ for sorted in opts.env.iter_mut() {
+ sorted.sort()
+ }
+ assert_eq!(
+ opts.env,
+ Some(vec![
+ ("TEST_KEY_A".into(), "test_value_a".into()),
+ ("TEST_KEY_B".into(), "test_value_b".into()),
+ ])
+ );
+ }
+
+ #[test]
+ fn fx_options_env_invalid_container() {
+ let env = Value::Number(1.into());
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env_invalid_value() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY".into(), Value::Number(1.into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
}
#[test]
@@ -527,9 +923,9 @@ mod tests {
let mut firefox_opts = Capabilities::new();
firefox_opts.insert("profile".into(), encoded_profile);
- let opts = make_options(firefox_opts);
- let mut profile = opts.profile.unwrap();
- let prefs = profile.user_prefs().unwrap();
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let mut profile = opts.profile.expect("valid firefox profile");
+ let prefs = profile.user_prefs().expect("valid preferences");
println!("{:#?}", prefs.prefs);
@@ -552,15 +948,15 @@ mod tests {
firefox_opts.insert("profile".into(), encoded_profile);
firefox_opts.insert("prefs".into(), Value::Object(prefs));
- let opts = make_options(firefox_opts);
- let mut profile = opts.profile.unwrap();
+ let opts = make_options(firefox_opts).expect("valid profile and prefs");
+ let mut profile = opts.profile.expect("valid firefox profile");
let handler = MarionetteHandler::new(Default::default());
handler
.set_prefs(2828, &mut profile, true, opts.prefs)
- .unwrap();
+ .expect("set preferences");
- let prefs_set = profile.user_prefs().unwrap();
+ let prefs_set = profile.user_prefs().expect("valid user preferences");
println!("{:#?}", prefs_set.prefs);
assert_eq!(
diff --git a/src/command.rs b/src/command.rs
index 9847d02..560598b 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -1,7 +1,6 @@
-use base64;
use crate::logging;
+use base64;
use hyper::Method;
-use regex::Captures;
use serde::de::{self, Deserialize, Deserializer};
use serde_json::{self, Value};
use std::env;
@@ -12,9 +11,9 @@ use webdriver::command::{WebDriverCommand, WebDriverExtensionCommand};
use webdriver::common::WebElement;
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
use webdriver::httpapi::WebDriverExtensionRoute;
+use webdriver::Parameters;
-pub const CHROME_ELEMENT_KEY: &'static str = "chromeelement-9fc5-4b51-a3c8-01716eedeb04";
-pub const LEGACY_ELEMENT_KEY: &'static str = "ELEMENT";
+pub const CHROME_ELEMENT_KEY: &str = "chromeelement-9fc5-4b51-a3c8-01716eedeb04";
pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> {
return vec![
@@ -72,7 +71,7 @@ impl WebDriverExtensionRoute for GeckoExtensionRoute {
fn command(
&self,
- params: &Captures,
+ params: &Parameters,
body_data: &Value,
) -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> {
use self::GeckoExtensionRoute::*;
@@ -84,21 +83,21 @@ impl WebDriverExtensionRoute for GeckoExtensionRoute {
}
XblAnonymousChildren => {
let element_id = try_opt!(
- params.name("elementId"),
+ params.get("elementId"),
ErrorStatus::InvalidArgument,
"Missing elementId parameter"
);
- let element = WebElement::new(element_id.as_str().to_string());
+ let element = WebElement(element_id.as_str().to_string());
GeckoExtensionCommand::XblAnonymousChildren(element)
}
XblAnonymousByAttribute => {
let element_id = try_opt!(
- params.name("elementId"),
+ params.get("elementId"),
ErrorStatus::InvalidArgument,
"Missing elementId parameter"
);
GeckoExtensionCommand::XblAnonymousByAttribute(
- WebElement::new(element_id.as_str().into()),
+ WebElement(element_id.as_str().into()),
serde_json::from_value(body_data.clone())?,
)
}
@@ -195,7 +194,7 @@ impl<'de> Deserialize<'de> for AddonInstallParameters {
};
AddonInstallParameters {
- path: path,
+ path,
temporary: data.temporary,
}
}
@@ -228,72 +227,59 @@ pub struct XblLocatorParameters {
pub value: String,
}
-#[derive(Default, Debug)]
+#[derive(Default, Debug, PartialEq)]
pub struct LogOptions {
pub level: Option<logging::Level>,
}
#[cfg(test)]
mod tests {
+ use serde_json::json;
+
use super::*;
- use crate::test::check_deserialize;
- use std::fs::File;
- use std::io::Read;
+ use crate::test::assert_de;
#[test]
- fn test_json_addon_install_parameters_null() {
- let json = r#""#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ fn test_json_addon_install_parameters_invalid() {
+ assert!(serde_json::from_str::<AddonInstallParameters>("").is_err());
+ assert!(serde_json::from_value::<AddonInstallParameters>(json!(null)).is_err());
+ assert!(serde_json::from_value::<AddonInstallParameters>(json!({})).is_err());
}
#[test]
- fn test_json_addon_install_parameters_empty() {
- let json = r#"{}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_addon_install_parameters_with_path() {
- let json = r#"{"path": "/path/to.xpi", "temporary": true}"#;
- let data = AddonInstallParameters {
+ fn test_json_addon_install_parameters_with_path_and_temporary() {
+ let params = AddonInstallParameters {
path: "/path/to.xpi".to_string(),
temporary: Some(true),
};
-
- check_deserialize(&json, &data);
+ assert_de(&params, json!({"path": "/path/to.xpi", "temporary": true}));
}
#[test]
- fn test_json_addon_install_parameters_with_path_only() {
- let json = r#"{"path": "/path/to.xpi"}"#;
- let data = AddonInstallParameters {
+ fn test_json_addon_install_parameters_with_path() {
+ let params = AddonInstallParameters {
path: "/path/to.xpi".to_string(),
temporary: None,
};
-
- check_deserialize(&json, &data);
+ assert_de(&params, json!({"path": "/path/to.xpi"}));
}
#[test]
fn test_json_addon_install_parameters_with_path_invalid_type() {
- let json = r#"{"path": true, "temporary": true}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({"path": true, "temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
fn test_json_addon_install_parameters_with_path_and_temporary_invalid_type() {
- let json = r#"{"path": "/path/to.xpi", "temporary": "foo"}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({"path": "/path/to.xpi", "temporary": "foo"});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
fn test_json_addon_install_parameters_with_addon() {
- let json = r#"{"addon": "aGVsbG8=", "temporary": true}"#;
- let data = serde_json::from_str::<AddonInstallParameters>(&json).unwrap();
+ let json = json!({"addon": "aGVsbG8=", "temporary": true});
+ let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
assert_eq!(data.temporary, Some(true));
let mut file = File::open(data.path).unwrap();
@@ -304,8 +290,8 @@ mod tests {
#[test]
fn test_json_addon_install_parameters_with_addon_only() {
- let json = r#"{"addon": "aGVsbG8="}"#;
- let data = serde_json::from_str::<AddonInstallParameters>(&json).unwrap();
+ let json = json!({"addon": "aGVsbG8="});
+ let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
assert_eq!(data.temporary, None);
let mut file = File::open(data.path).unwrap();
@@ -316,158 +302,92 @@ mod tests {
#[test]
fn test_json_addon_install_parameters_with_addon_invalid_type() {
- let json = r#"{"addon": true, "temporary": true}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({"addon": true, "temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
fn test_json_addon_install_parameters_with_addon_and_temporary_invalid_type() {
- let json = r#"{"addon": "aGVsbG8=", "temporary": "foo"}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({"addon": "aGVsbG8=", "temporary": "foo"});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
fn test_json_install_parameters_with_temporary_only() {
- let json = r#"{"temporary": true}"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({"temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
fn test_json_addon_install_parameters_with_both_path_and_addon() {
- let json = r#"{
- "path":"/path/to.xpi",
- "addon":"aGVsbG8=",
- "temporary":true
- }"#;
-
- assert!(serde_json::from_str::<AddonInstallParameters>(&json).is_err());
+ let json = json!({
+ "path": "/path/to.xpi",
+ "addon": "aGVsbG8=",
+ "temporary": true,
+ });
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
}
#[test]
- fn test_json_addon_uninstall_parameters_null() {
- let json = r#""#;
-
- assert!(serde_json::from_str::<AddonUninstallParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_addon_uninstall_parameters_empty() {
- let json = r#"{}"#;
-
- assert!(serde_json::from_str::<AddonUninstallParameters>(&json).is_err());
+ fn test_json_addon_uninstall_parameters_invalid() {
+ assert!(serde_json::from_str::<AddonUninstallParameters>("").is_err());
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json!(null)).is_err());
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json!({})).is_err());
}
#[test]
fn test_json_addon_uninstall_parameters() {
- let json = r#"{"id": "foo"}"#;
- let data = AddonUninstallParameters {
+ let params = AddonUninstallParameters {
id: "foo".to_string(),
};
-
- check_deserialize(&json, &data);
+ assert_de(&params, json!({"id": "foo"}));
}
#[test]
fn test_json_addon_uninstall_parameters_id_invalid_type() {
- let json = r#"{"id": true}"#;
-
- assert!(serde_json::from_str::<AddonUninstallParameters>(&json).is_err());
+ let json = json!({"id": true});
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json).is_err());
}
#[test]
fn test_json_gecko_context_parameters_content() {
- let json = r#"{"context": "content"}"#;
- let data = GeckoContextParameters {
+ let params = GeckoContextParameters {
context: GeckoContext::Content,
};
-
- check_deserialize(&json, &data);
+ assert_de(&params, json!({"context": "content"}));
}
#[test]
fn test_json_gecko_context_parameters_chrome() {
- let json = r#"{"context": "chrome"}"#;
- let data = GeckoContextParameters {
+ let params = GeckoContextParameters {
context: GeckoContext::Chrome,
};
-
- check_deserialize(&json, &data);
+ assert_de(&params, json!({"context": "chrome"}));
}
#[test]
- fn test_json_gecko_context_parameters_context_missing() {
- let json = r#"{}"#;
-
- assert!(serde_json::from_str::<GeckoContextParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_gecko_context_parameters_context_null() {
- let json = r#"{"context": null}"#;
-
- assert!(serde_json::from_str::<GeckoContextParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_gecko_context_parameters_context_invalid_value() {
- let json = r#"{"context": "foo"}"#;
-
- assert!(serde_json::from_str::<GeckoContextParameters>(&json).is_err());
+ fn test_json_gecko_context_parameters_context_invalid() {
+ type P = GeckoContextParameters;
+ assert!(serde_json::from_value::<P>(json!({})).is_err());
+ assert!(serde_json::from_value::<P>(json!({ "context": null })).is_err());
+ assert!(serde_json::from_value::<P>(json!({"context": "foo"})).is_err());
}
#[test]
fn test_json_xbl_anonymous_by_attribute() {
- let json = r#"{
- "name": "foo",
- "value": "bar"
- }"#;
-
- let data = XblLocatorParameters {
+ let locator = XblLocatorParameters {
name: "foo".to_string(),
value: "bar".to_string(),
};
-
- check_deserialize(&json, &data);
+ assert_de(&locator, json!({"name": "foo", "value": "bar"}));
}
#[test]
- fn test_json_xbl_anonymous_by_attribute_with_name_missing() {
- let json = r#"{
- "value": "bar"
- }"#;
-
- assert!(serde_json::from_str::<XblLocatorParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_xbl_anonymous_by_attribute_with_name_invalid_type() {
- let json = r#"{
- "name": null,
- "value": "bar"
- }"#;
-
- assert!(serde_json::from_str::<XblLocatorParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_xbl_anonymous_by_attribute_with_value_missing() {
- let json = r#"{
- "name": "foo",
- }"#;
-
- assert!(serde_json::from_str::<XblLocatorParameters>(&json).is_err());
- }
-
- #[test]
- fn test_json_xbl_anonymous_by_attribute_with_value_invalid_type() {
- let json = r#"{
- "name": "foo",
- "value": null
- }"#;
-
- assert!(serde_json::from_str::<XblLocatorParameters>(&json).is_err());
+ fn test_json_xbl_anonymous_by_attribute_with_name_invalid() {
+ type P = XblLocatorParameters;
+ assert!(serde_json::from_value::<P>(json!({"value": "bar"})).is_err());
+ assert!(serde_json::from_value::<P>(json!({"name": null, "value": "bar"})).is_err());
+ assert!(serde_json::from_value::<P>(json!({"name": "foo"})).is_err());
+ assert!(serde_json::from_value::<P>(json!({"name": "foo", "value": null})).is_err());
}
}
diff --git a/src/logging.rs b/src/logging.rs
index a8bda4d..7c0117d 100644
--- a/src/logging.rs
+++ b/src/logging.rs
@@ -31,15 +31,16 @@ use std::fmt;
use std::io;
use std::io::Write;
use std::str;
-use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};
+use std::sync::atomic::{AtomicUsize, Ordering};
use chrono;
use log;
use mozprofile::preferences::Pref;
-static MAX_LOG_LEVEL: AtomicUsize = ATOMIC_USIZE_INIT;
-const LOGGED_TARGETS: &'static [&'static str] = &[
+static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0);
+const LOGGED_TARGETS: &[&str] = &[
"geckodriver",
+ "mozdevice",
"mozprofile",
"mozrunner",
"mozversion",
diff --git a/src/main.rs b/src/main.rs
index efa774f..9ce17ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,5 @@
+#![forbid(unsafe_code)]
+
extern crate base64;
extern crate chrono;
#[macro_use]
@@ -5,6 +7,8 @@ extern crate clap;
#[macro_use]
extern crate lazy_static;
extern crate hyper;
+extern crate marionette as marionette_rs;
+extern crate mozdevice;
extern crate mozprofile;
extern crate mozrunner;
extern crate mozversion;
@@ -13,6 +17,7 @@ extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
+extern crate serde_yaml;
extern crate uuid;
extern crate webdriver;
extern crate zip;
@@ -20,9 +25,12 @@ extern crate zip;
#[macro_use]
extern crate log;
-use std::io::Write;
+use std::env;
+use std::fmt;
+use std::io;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
+use std::result;
use std::str::FromStr;
use clap::{App, Arg};
@@ -36,6 +44,7 @@ macro_rules! try_opt {
}};
}
+mod android;
mod build;
mod capabilities;
mod command;
@@ -46,68 +55,230 @@ mod prefs;
#[cfg(test)]
pub mod test;
-use crate::build::BuildInfo;
use crate::command::extension_routes;
+use crate::logging::Level;
use crate::marionette::{MarionetteHandler, MarionetteSettings};
-type ProgramResult = std::result::Result<(), (ExitCode, String)>;
+const EXIT_SUCCESS: i32 = 0;
+const EXIT_USAGE: i32 = 64;
+const EXIT_UNAVAILABLE: i32 = 69;
-enum ExitCode {
- Ok = 0,
- Usage = 64,
- Unavailable = 69,
+enum FatalError {
+ Parsing(clap::Error),
+ Usage(String),
+ Server(io::Error),
}
-fn print_version() {
- println!("geckodriver {}", BuildInfo);
- println!("");
- println!("The source code of this program is available from");
- println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
- println!("");
- println!("This program is subject to the terms of the Mozilla Public License 2.0.");
- println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
+impl FatalError {
+ fn exit_code(&self) -> i32 {
+ use FatalError::*;
+ match *self {
+ Parsing(_) | Usage(_) => EXIT_USAGE,
+ Server(_) => EXIT_UNAVAILABLE,
+ }
+ }
+
+ fn help_included(&self) -> bool {
+ match *self {
+ FatalError::Parsing(_) => true,
+ _ => false,
+ }
+ }
+}
+
+impl From<clap::Error> for FatalError {
+ fn from(err: clap::Error) -> FatalError {
+ FatalError::Parsing(err)
+ }
+}
+
+impl From<io::Error> for FatalError {
+ fn from(err: io::Error) -> FatalError {
+ FatalError::Server(err)
+ }
+}
+
+// harmonise error message from clap to avoid duplicate "error:" prefix
+impl fmt::Display for FatalError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use FatalError::*;
+ let s = match *self {
+ Parsing(ref err) => err.to_string(),
+ Usage(ref s) => format!("error: {}", s),
+ Server(ref err) => format!("error: {}", err.to_string()),
+ };
+ write!(f, "{}", s)
+ }
+}
+
+macro_rules! usage {
+ ($msg:expr) => {
+ return Err(FatalError::Usage($msg.to_string()));
+ };
+
+ ($fmt:expr, $($arg:tt)+) => {
+ return Err(FatalError::Usage(format!($fmt, $($arg)+)));
+ };
+}
+
+type ProgramResult<T> = result::Result<T, FatalError>;
+
+enum Operation {
+ Help,
+ Version,
+ Server {
+ log_level: Option<Level>,
+ address: SocketAddr,
+ settings: MarionetteSettings,
+ },
}
-fn app<'a, 'b>() -> App<'a, 'b> {
- App::new(format!("geckodriver {}", crate_version!()))
- .about("WebDriver implementation for Firefox.")
+fn parse_args(app: &mut App) -> ProgramResult<Operation> {
+ let matches = app.get_matches_from_safe_borrow(env::args())?;
+
+ let log_level = if matches.is_present("log_level") {
+ Level::from_str(matches.value_of("log_level").unwrap()).ok()
+ } else {
+ Some(match matches.occurrences_of("verbosity") {
+ 0 => Level::Info,
+ 1 => Level::Debug,
+ _ => Level::Trace,
+ })
+ };
+
+ let host = matches.value_of("webdriver_host").unwrap();
+ let port = {
+ let s = matches.value_of("webdriver_port").unwrap();
+ match u16::from_str(s) {
+ Ok(n) => n,
+ Err(e) => usage!("invalid --port: {}: {}", e, s),
+ }
+ };
+ let address = match IpAddr::from_str(host) {
+ Ok(addr) => SocketAddr::new(addr, port),
+ Err(e) => usage!("{}: {}:{}", e, host, port),
+ };
+
+ let binary = matches.value_of("binary").map(PathBuf::from);
+
+ let marionette_host = matches.value_of("marionette_host").unwrap();
+ let marionette_port = match matches.value_of("marionette_port") {
+ Some(s) => match u16::from_str(s) {
+ Ok(n) => Some(n),
+ Err(e) => usage!("invalid --marionette-port: {}", e),
+ },
+ None => None,
+ };
+
+ let op = if matches.is_present("help") {
+ Operation::Help
+ } else if matches.is_present("version") {
+ Operation::Version
+ } else {
+ let settings = MarionetteSettings {
+ host: marionette_host.to_string(),
+ port: marionette_port,
+ binary,
+ connect_existing: matches.is_present("connect_existing"),
+ jsdebugger: matches.is_present("jsdebugger"),
+ };
+ Operation::Server {
+ log_level,
+ address,
+ settings,
+ }
+ };
+
+ Ok(op)
+}
+
+fn inner_main(app: &mut App) -> ProgramResult<()> {
+ match parse_args(app)? {
+ Operation::Help => print_help(app),
+ Operation::Version => print_version(),
+
+ Operation::Server {
+ log_level,
+ address,
+ settings,
+ } => {
+ if let Some(ref level) = log_level {
+ logging::init_with_level(*level).unwrap();
+ } else {
+ logging::init().unwrap();
+ }
+
+ let handler = MarionetteHandler::new(settings);
+ let listening = webdriver::server::start(address, handler, extension_routes())?;
+ info!("Listening on {}", listening.socket);
+ }
+ }
+
+ Ok(())
+}
+
+fn main() {
+ use std::process::exit;
+
+ let mut app = make_app();
+
+ // use std::process:Termination when it graduates
+ exit(match inner_main(&mut app) {
+ Ok(_) => EXIT_SUCCESS,
+
+ Err(e) => {
+ eprintln!("{}: {}", get_program_name(), e);
+ if !e.help_included() {
+ print_help(&mut app);
+ }
+
+ e.exit_code()
+ }
+ });
+}
+
+fn make_app<'a, 'b>() -> App<'a, 'b> {
+ App::new(format!("geckodriver {}", build::build_info()))
+ .about("WebDriver implementation for Firefox")
.arg(
Arg::with_name("webdriver_host")
.long("host")
+ .takes_value(true)
.value_name("HOST")
- .help("Host ip to use for WebDriver server (default: 127.0.0.1)")
- .takes_value(true),
+ .default_value("127.0.0.1")
+ .help("Host IP to use for WebDriver server"),
)
.arg(
Arg::with_name("webdriver_port")
.short("p")
.long("port")
- .value_name("PORT")
- .help("Port to use for WebDriver server (default: 4444)")
.takes_value(true)
- .alias("webdriver-port"),
+ .value_name("PORT")
+ .default_value("4444")
+ .help("Port to use for WebDriver server"),
)
.arg(
Arg::with_name("binary")
.short("b")
.long("binary")
+ .takes_value(true)
.value_name("BINARY")
- .help("Path to the Firefox binary")
- .takes_value(true),
+ .help("Path to the Firefox binary"),
)
.arg(
Arg::with_name("marionette_host")
.long("marionette-host")
- .value_name("HOST")
- .help("Host to use to connect to Gecko (default: 127.0.0.1)")
.takes_value(true)
+ .value_name("HOST")
+ .default_value("127.0.0.1")
+ .help("Host to use to connect to Gecko"),
)
.arg(
Arg::with_name("marionette_port")
.long("marionette-port")
+ .takes_value(true)
.value_name("PORT")
- .help("Port to use to connect to Gecko (default: system-allocated port)")
- .takes_value(true),
+ .help("Port to use to connect to Gecko [default: system-allocated port]"),
)
.arg(
Arg::with_name("connect_existing")
@@ -118,14 +289,13 @@ fn app<'a, 'b>() -> App<'a, 'b> {
.arg(
Arg::with_name("jsdebugger")
.long("jsdebugger")
- .takes_value(false)
.help("Attach browser toolbox debugger for Firefox"),
)
.arg(
Arg::with_name("verbosity")
- .short("v")
.multiple(true)
.conflicts_with("log_level")
+ .short("v")
.help("Log level verbosity (-v for debug and -vv for trace level)"),
)
.arg(
@@ -137,6 +307,12 @@ fn app<'a, 'b>() -> App<'a, 'b> {
.help("Set Gecko log level"),
)
.arg(
+ Arg::with_name("help")
+ .short("h")
+ .long("help")
+ .help("Prints this message"),
+ )
+ .arg(
Arg::with_name("version")
.short("V")
.long("version")
@@ -144,80 +320,21 @@ fn app<'a, 'b>() -> App<'a, 'b> {
)
}
-fn run() -> ProgramResult {
- let matches = app().get_matches();
-
- if matches.is_present("version") {
- print_version();
- return Ok(());
- }
-
- let host = matches.value_of("webdriver_host").unwrap_or("127.0.0.1");
- let port = match u16::from_str(
- matches
- .value_of("webdriver_port")
- .or(matches.value_of("webdriver_port_alias"))
- .unwrap_or("4444"),
- ) {
- Ok(x) => x,
- Err(_) => return Err((ExitCode::Usage, "invalid WebDriver port".into())),
- };
- let addr = match IpAddr::from_str(host) {
- Ok(addr) => SocketAddr::new(addr, port),
- Err(_) => return Err((ExitCode::Usage, "invalid host address".into())),
- };
-
- let binary = matches.value_of("binary").map(PathBuf::from);
-
- let marionette_host = matches.value_of("marionette_host")
- .unwrap_or("127.0.0.1").to_string();
- let marionette_port = match matches.value_of("marionette_port") {
- Some(x) => match u16::from_str(x) {
- Ok(x) => Some(x),
- Err(_) => return Err((ExitCode::Usage, "invalid Marionette port".into())),
- },
- None => None,
- };
-
- let log_level = if matches.is_present("log_level") {
- logging::Level::from_str(matches.value_of("log_level").unwrap()).ok()
- } else {
- match matches.occurrences_of("verbosity") {
- 0 => Some(logging::Level::Info),
- 1 => Some(logging::Level::Debug),
- _ => Some(logging::Level::Trace),
- }
- };
- if let Some(ref level) = log_level {
- logging::init_with_level(*level).unwrap();
- } else {
- logging::init().unwrap();
- }
-
- let settings = MarionetteSettings {
- host: marionette_host,
- port: marionette_port,
- binary,
- connect_existing: matches.is_present("connect_existing"),
- jsdebugger: matches.is_present("jsdebugger"),
- };
- let handler = MarionetteHandler::new(settings);
- let listening = webdriver::server::start(addr, handler, &extension_routes()[..])
- .map_err(|err| (ExitCode::Unavailable, err.to_string()))?;
- debug!("Listening on {}", listening.socket);
-
- Ok(())
+fn get_program_name() -> String {
+ env::args().next().unwrap()
}
-fn main() {
- let exit_code = match run() {
- Ok(_) => ExitCode::Ok,
- Err((exit_code, reason)) => {
- error!("{}", reason);
- exit_code
- }
- };
+fn print_help(app: &mut App) {
+ app.print_help().ok();
+ println!();
+}
- std::io::stdout().flush().unwrap();
- std::process::exit(exit_code as i32);
+fn print_version() {
+ println!("geckodriver {}", build::build_info());
+ println!();
+ println!("The source code of this program is available from");
+ println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
+ println!();
+ println!("This program is subject to the terms of the Mozilla Public License 2.0.");
+ println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
}
diff --git a/src/marionette.rs b/src/marionette.rs
index 4ed383e..666d56d 100644
--- a/src/marionette.rs
+++ b/src/marionette.rs
@@ -1,7 +1,21 @@
+use crate::android::AndroidHandler;
use crate::command::{
AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters,
GeckoExtensionCommand, GeckoExtensionRoute, XblLocatorParameters, CHROME_ELEMENT_KEY,
- LEGACY_ELEMENT_KEY,
+};
+use marionette_rs::common::{
+ Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame,
+ Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window,
+};
+use marionette_rs::marionette::AppStatus;
+use marionette_rs::message::{Command, Message, MessageId, Request};
+use marionette_rs::webdriver::{
+ Command as MarionetteWebDriverCommand, Keys as MarionetteKeys, LegacyWebElement,
+ Locator as MarionetteLocator, NewWindow as MarionetteNewWindow,
+ PrintMargins as MarionettePrintMargins, PrintOrientation as MarionettePrintOrientation,
+ PrintPage as MarionettePrintPage, PrintParameters as MarionettePrintParameters,
+ ScreenshotOptions, Script as MarionetteScript, Selector as MarionetteSelector,
+ Url as MarionetteUrl, WindowRect as MarionetteWindowRect,
};
use mozprofile::preferences::Pref;
use mozprofile::profile::Profile;
@@ -9,7 +23,6 @@ use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
use serde::de::{self, Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
use serde_json::{self, Map, Value};
-use std::error::Error;
use std::io::prelude::*;
use std::io::Error as IoError;
use std::io::ErrorKind;
@@ -20,41 +33,50 @@ use std::sync::Mutex;
use std::thread;
use std::time;
use webdriver::capabilities::CapabilitiesMatching;
-use webdriver::command::WebDriverCommand::{AcceptAlert, AddCookie, NewWindow, CloseWindow,
- DeleteCookie, DeleteCookies, DeleteSession,
- DismissAlert, ElementClear, ElementClick,
- ElementSendKeys, ExecuteAsyncScript, ExecuteScript,
- Extension, FindElement, FindElementElement,
- FindElementElements, FindElements, FullscreenWindow,
- Get, GetActiveElement, GetAlertText, GetCSSValue,
- GetCookies, GetCurrentUrl, GetElementAttribute,
- GetElementProperty, GetElementRect, GetElementTagName,
- GetElementText, GetNamedCookie, GetPageSource,
- GetTimeouts, GetTitle, GetWindowHandle,
- GetWindowHandles, GetWindowRect, GoBack, GoForward,
- IsDisplayed, IsEnabled, IsSelected, MaximizeWindow,
- MinimizeWindow, NewSession, PerformActions, Refresh,
- ReleaseActions, SendAlertText, SetTimeouts,
- SetWindowRect, Status, SwitchToFrame,
- SwitchToParentFrame, SwitchToWindow,
- TakeElementScreenshot, TakeScreenshot};
-use webdriver::command::{ActionsParameters, AddCookieParameters, GetNamedCookieParameters,
- GetParameters, JavascriptCommandParameters, LocatorParameters,
- NewSessionParameters, SwitchToFrameParameters, SwitchToWindowParameters,
- TimeoutsParameters, WindowRectParameters, NewWindowParameters};
+use webdriver::command::WebDriverCommand::{
+ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
+ ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
+ FindElement, FindElementElement, FindElementElements, FindElements, FullscreenWindow, Get,
+ GetActiveElement, GetAlertText, GetCSSValue, GetCookies, GetCurrentUrl, GetElementAttribute,
+ GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie,
+ GetPageSource, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, GetWindowRect, GoBack,
+ GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, MinimizeWindow, NewSession,
+ NewWindow, PerformActions, Print, Refresh, ReleaseActions, SendAlertText, SetTimeouts,
+ SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, SwitchToWindow,
+ TakeElementScreenshot, TakeScreenshot,
+};
+use webdriver::command::{
+ ActionsParameters, AddCookieParameters, GetNamedCookieParameters, GetParameters,
+ JavascriptCommandParameters, LocatorParameters, NewSessionParameters, NewWindowParameters,
+ PrintMargins, PrintOrientation, PrintPage, PrintParameters, SendKeysParameters,
+ SwitchToFrameParameters, SwitchToWindowParameters, TimeoutsParameters, WindowRectParameters,
+};
use webdriver::command::{WebDriverCommand, WebDriverMessage};
-use webdriver::common::{Cookie, FrameId, WebElement, ELEMENT_KEY, FRAME_KEY, WINDOW_KEY};
+use webdriver::common::{
+ Cookie, Date, FrameId, LocatorStrategy, WebElement, ELEMENT_KEY, FRAME_KEY, WINDOW_KEY,
+};
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
-use webdriver::response::{NewWindowResponse, CloseWindowResponse, CookieResponse, CookiesResponse,
- ElementRectResponse, NewSessionResponse, TimeoutsResponse,
- ValueResponse, WebDriverResponse, WindowRectResponse};
+use webdriver::response::{
+ CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse,
+ NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse,
+};
use webdriver::server::{Session, WebDriverHandler};
-use crate::build::BuildInfo;
+use crate::build;
use crate::capabilities::{FirefoxCapabilities, FirefoxOptions};
use crate::logging;
use crate::prefs;
+/// A running Gecko instance.
+#[derive(Debug)]
+pub enum Browser {
+ /// A local Firefox process, running on this (host) device.
+ Host(FirefoxProcess),
+
+ /// A remote instance, running on a (target) Android device.
+ Target(AndroidHandler),
+}
+
#[derive(Debug, PartialEq, Deserialize)]
pub struct MarionetteHandshake {
#[serde(rename = "marionetteProtocol")]
@@ -79,7 +101,7 @@ pub struct MarionetteSettings {
pub struct MarionetteHandler {
pub connection: Mutex<Option<MarionetteConnection>>,
pub settings: MarionetteSettings,
- pub browser: Option<FirefoxProcess>,
+ pub browser: Option<Browser>,
}
impl MarionetteHandler {
@@ -100,10 +122,12 @@ impl MarionetteHandler {
let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
let mut capabilities = new_session_parameters
.match_browser(&mut fx_capabilities)?
- .ok_or(WebDriverError::new(
- ErrorStatus::SessionNotCreated,
- "Unable to find a matching set of capabilities",
- ))?;
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Unable to find a matching set of capabilities",
+ )
+ })?;
let options = FirefoxOptions::from_capabilities(
fx_capabilities.chosen_binary,
@@ -118,35 +142,104 @@ impl MarionetteHandler {
let host = self.settings.host.to_owned();
let port = self.settings.port.unwrap_or(get_free_port(&host)?);
- if !self.settings.connect_existing {
- self.start_browser(port, options)?;
+
+ match options.android {
+ Some(_) => {
+ // TODO: support connecting to running Apps. There's no real obstruction here,
+ // just some details about port forwarding to work through. We can't follow
+ // `chromedriver` here since it uses an abstract socket rather than a TCP socket:
+ // see bug 1240830 for thoughts on doing that for Marionette.
+ if self.settings.connect_existing {
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Cannot connect to an existing Android App yet",
+ ));
+ }
+
+ self.start_android(port, options)?;
+ }
+ None => {
+ if !self.settings.connect_existing {
+ self.start_browser(port, options)?;
+ }
+ }
}
let mut connection = MarionetteConnection::new(host, port, session_id.clone());
connection.connect(&mut self.browser).or_else(|e| {
- if let Some(ref mut runner) = self.browser {
- runner.kill()?;
+ match self.browser {
+ Some(Browser::Host(ref mut runner)) => {
+ runner.kill()?;
+ }
+ Some(Browser::Target(ref mut handler)) => {
+ handler.force_stop().map_err(|e| {
+ WebDriverError::new(ErrorStatus::UnknownError, e.to_string())
+ })?;
+ }
+ _ => {}
}
+
Err(e)
})?;
self.connection = Mutex::new(Some(connection));
Ok(capabilities)
}
+ fn start_android(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> {
+ let android_options = options.android.unwrap();
+
+ let mut handler = AndroidHandler::new(&android_options);
+ handler
+ .connect(port)
+ .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?;
+
+ // Profile management.
+ let is_custom_profile = options.profile.is_some();
+
+ let mut profile = options.profile.unwrap_or(Profile::new()?);
+
+ self.set_prefs(
+ handler.target_port,
+ &mut profile,
+ is_custom_profile,
+ options.prefs,
+ )
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+
+ handler
+ .prepare(&profile, options.env.unwrap_or_default())
+ .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?;
+
+ handler
+ .launch()
+ .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?;
+
+ self.browser = Some(Browser::Target(handler));
+
+ Ok(())
+ }
+
fn start_browser(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> {
- let binary = options.binary.ok_or(WebDriverError::new(
- ErrorStatus::SessionNotCreated,
- "Expected browser binary location, but unable to find \
+ let binary = options.binary.ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Expected browser binary location, but unable to find \
binary in default location, no \
'moz:firefoxOptions.binary' capability provided, and \
no binary flag set on the command line",
- ))?;
+ )
+ })?;
let is_custom_profile = options.profile.is_some();
let mut profile = match options.profile {
Some(x) => x,
- None => Profile::new(None)?,
+ None => Profile::new()?,
};
self.set_prefs(port, &mut profile, is_custom_profile, options.prefs)
@@ -159,28 +252,27 @@ impl MarionetteHandler {
let mut runner = FirefoxRunner::new(&binary, profile);
- // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
- runner
- .env("MOZ_CRASHREPORTER", "1")
- .env("MOZ_CRASHREPORTER_NO_REPORT", "1")
- .env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
-
- // double-dashed flags are not accepted on Windows systems
- runner.arg("-marionette");
+ runner.arg("--marionette");
if self.settings.jsdebugger {
- runner.arg("-jsdebugger");
+ runner.arg("--jsdebugger");
}
if let Some(args) = options.args.as_ref() {
runner.args(args);
}
+ // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
+ runner
+ .env("MOZ_CRASHREPORTER", "1")
+ .env("MOZ_CRASHREPORTER_NO_REPORT", "1")
+ .env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
+
let browser_proc = runner.start().map_err(|e| {
WebDriverError::new(
ErrorStatus::SessionNotCreated,
format!("Failed to start browser {}: {}", binary.display(), e),
)
})?;
- self.browser = Some(browser_proc);
+ self.browser = Some(Browser::Host(browser_proc));
Ok(())
}
@@ -201,7 +293,7 @@ impl MarionetteHandler {
for &(ref name, ref value) in prefs::DEFAULT.iter() {
if !custom_profile || !prefs.contains_key(name) {
- prefs.insert((*name).clone(), (*value).clone());
+ prefs.insert((*name).to_string(), (*value).clone());
}
}
@@ -239,7 +331,8 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
// First handle the status message which doesn't actually require a marionette
// connection or message
if msg.command == Status {
- let (ready, message) = self.connection
+ let (ready, message) = self
+ .connection
.lock()
.map(|ref connection| {
connection
@@ -324,13 +417,23 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
}
}
- if let Some(ref mut runner) = self.browser {
- // TODO(https://bugzil.la/1443922):
- // Use toolkit.asyncshutdown.crash_timout pref
- match runner.wait(time::Duration::from_secs(70)) {
- Ok(x) => debug!("Browser process stopped: {}", x),
- Err(e) => error!("Failed to stop browser process: {}", e),
+ match self.browser {
+ Some(Browser::Host(ref mut runner)) => {
+ // TODO(https://bugzil.la/1443922):
+ // Use toolkit.asyncshutdown.crash_timout pref
+ match runner.wait(time::Duration::from_secs(70)) {
+ Ok(x) => debug!("Browser process stopped: {}", x),
+ Err(e) => error!("Failed to stop browser process: {}", e),
+ }
}
+ Some(Browser::Target(ref mut handler)) => {
+ // Try to force-stop the process on the target device
+ match handler.force_stop() {
+ Ok(_) => debug!("Android package force-stopped"),
+ Err(e) => error!("Failed to force-stop Android package: {}", e),
+ }
+ }
+ None => {}
}
self.connection = Mutex::new(None);
@@ -342,12 +445,12 @@ pub struct MarionetteSession {
pub session_id: String,
protocol: Option<u16>,
application_type: Option<String>,
- command_id: u64,
+ command_id: MessageId,
}
impl MarionetteSession {
pub fn new(session_id: Option<String>) -> MarionetteSession {
- let initital_id = session_id.unwrap_or("".to_string());
+ let initital_id = session_id.unwrap_or_else(|| "".to_string());
MarionetteSession {
session_id: initital_id,
protocol: None,
@@ -361,21 +464,19 @@ impl MarionetteSession {
msg: &WebDriverMessage<GeckoExtensionRoute>,
resp: &MarionetteResponse,
) -> WebDriverResult<()> {
- match msg.command {
- NewSession(_) => {
- let session_id = try_opt!(
- try_opt!(
- resp.result.get("sessionId"),
- ErrorStatus::SessionNotCreated,
- "Unable to get session id"
- ).as_str(),
+ if let NewSession(_) = msg.command {
+ let session_id = try_opt!(
+ try_opt!(
+ resp.result.get("sessionId"),
ErrorStatus::SessionNotCreated,
- "Unable to convert session id to string"
- );
- self.session_id = session_id.to_string().clone();
- }
- _ => {}
- }
+ "Unable to get session id"
+ )
+ .as_str(),
+ ErrorStatus::SessionNotCreated,
+ "Unable to convert session id to string"
+ );
+ self.session_id = session_id.to_string().clone();
+ };
Ok(())
}
@@ -393,15 +494,10 @@ impl MarionetteSession {
let chrome_element = data.get(CHROME_ELEMENT_KEY);
let element = data.get(ELEMENT_KEY);
let frame = data.get(FRAME_KEY);
- let legacy_element = data.get(LEGACY_ELEMENT_KEY);
let window = data.get(WINDOW_KEY);
let value = try_opt!(
- element
- .or(legacy_element)
- .or(chrome_element)
- .or(frame)
- .or(window),
+ element.or(chrome_element).or(frame).or(window),
ErrorStatus::UnknownError,
"Failed to extract web element from Marionette response"
);
@@ -409,12 +505,13 @@ impl MarionetteSession {
value.as_str(),
ErrorStatus::UnknownError,
"Failed to convert web element reference value to string"
- ).to_string();
- Ok(WebElement::new(id))
+ )
+ .to_string();
+ Ok(WebElement(id))
}
- pub fn next_command_id(&mut self) -> u64 {
- self.command_id = self.command_id + 1;
+ pub fn next_command_id(&mut self) -> MessageId {
+ self.command_id += 1;
self.command_id
}
@@ -479,28 +576,34 @@ impl MarionetteSession {
| ExecuteAsyncScript(_)
| GetAlertText
| TakeScreenshot
- | TakeElementScreenshot(_) => WebDriverResponse::Generic(resp.to_value_response(true)?),
+ | Print(_)
+ | TakeElementScreenshot(_) => {
+ WebDriverResponse::Generic(resp.into_value_response(true)?)
+ }
GetTimeouts => {
let script = match try_opt!(
- resp.result.get("script"),
+ resp.result.get("script"),
+ ErrorStatus::UnknownError,
+ "Missing field: script"
+ ) {
+ Value::Null => None,
+ n => try_opt!(
+ Some(n.as_u64()),
ErrorStatus::UnknownError,
- "Missing field: script"
- ) {
- Value::Null => None,
- n => try_opt!(
- Some(n.as_u64()),
- ErrorStatus::UnknownError,
- "Failed to interpret script timeout duration as u64"
- ),
+ "Failed to interpret script timeout duration as u64"
+ ),
};
// Check for the spec-compliant "pageLoad", but also for "page load",
// which was sent by Firefox 52 and earlier.
let page_load = try_opt!(
try_opt!(
- resp.result.get("pageLoad").or(resp.result.get("page load")),
+ resp.result
+ .get("pageLoad")
+ .or_else(|| resp.result.get("page load")),
ErrorStatus::UnknownError,
"Missing field: pageLoad"
- ).as_u64(),
+ )
+ .as_u64(),
ErrorStatus::UnknownError,
"Failed to interpret page load duration as u64"
);
@@ -509,38 +612,43 @@ impl MarionetteSession {
resp.result.get("implicit"),
ErrorStatus::UnknownError,
"Missing field: implicit"
- ).as_u64(),
+ )
+ .as_u64(),
ErrorStatus::UnknownError,
"Failed to interpret implicit search duration as u64"
);
WebDriverResponse::Timeouts(TimeoutsResponse {
- script: script,
- page_load: page_load,
- implicit: implicit,
+ script,
+ page_load,
+ implicit,
})
}
Status => panic!("Got status command that should already have been handled"),
- GetWindowHandles => WebDriverResponse::Generic(resp.to_value_response(false)?),
+ GetWindowHandles => WebDriverResponse::Generic(resp.into_value_response(false)?),
NewWindow(_) => {
let handle: String = try_opt!(
try_opt!(
resp.result.get("handle"),
ErrorStatus::UnknownError,
"Failed to find handle field"
- ).as_str(),
+ )
+ .as_str(),
ErrorStatus::UnknownError,
"Failed to interpret handle as string"
- ).into();
+ )
+ .into();
let typ: String = try_opt!(
try_opt!(
resp.result.get("type"),
ErrorStatus::UnknownError,
"Failed to find type field"
- ).as_str(),
+ )
+ .as_str(),
ErrorStatus::UnknownError,
"Failed to interpret type as string"
- ).into();
+ )
+ .into();
WebDriverResponse::NewWindow(NewWindowResponse { handle, typ })
}
@@ -557,8 +665,10 @@ impl MarionetteSession {
x.as_str(),
ErrorStatus::UnknownError,
"Failed to interpret window handle as string"
- ).to_owned())
- }).collect::<Result<Vec<_>, _>>()?;
+ )
+ .to_owned())
+ })
+ .collect::<Result<Vec<_>, _>>()?;
WebDriverResponse::CloseWindow(CloseWindowResponse(handles))
}
GetElementRect(_) => {
@@ -567,7 +677,8 @@ impl MarionetteSession {
resp.result.get("x"),
ErrorStatus::UnknownError,
"Failed to find x field"
- ).as_f64(),
+ )
+ .as_f64(),
ErrorStatus::UnknownError,
"Failed to interpret x as float"
);
@@ -577,7 +688,8 @@ impl MarionetteSession {
resp.result.get("y"),
ErrorStatus::UnknownError,
"Failed to find y field"
- ).as_f64(),
+ )
+ .as_f64(),
ErrorStatus::UnknownError,
"Failed to interpret y as float"
);
@@ -587,7 +699,8 @@ impl MarionetteSession {
resp.result.get("width"),
ErrorStatus::UnknownError,
"Failed to find width field"
- ).as_f64(),
+ )
+ .as_f64(),
ErrorStatus::UnknownError,
"Failed to interpret width as float"
);
@@ -597,7 +710,8 @@ impl MarionetteSession {
resp.result.get("height"),
ErrorStatus::UnknownError,
"Failed to find height field"
- ).as_f64(),
+ )
+ .as_f64(),
ErrorStatus::UnknownError,
"Failed to interpret width as float"
);
@@ -617,7 +731,8 @@ impl MarionetteSession {
resp.result.get("width"),
ErrorStatus::UnknownError,
"Failed to find width field"
- ).as_u64(),
+ )
+ .as_u64(),
ErrorStatus::UnknownError,
"Failed to interpret width as positive integer"
);
@@ -627,7 +742,8 @@ impl MarionetteSession {
resp.result.get("height"),
ErrorStatus::UnknownError,
"Failed to find heigenht field"
- ).as_u64(),
+ )
+ .as_u64(),
ErrorStatus::UnknownError,
"Failed to interpret height as positive integer"
);
@@ -637,7 +753,8 @@ impl MarionetteSession {
resp.result.get("x"),
ErrorStatus::UnknownError,
"Failed to find x field"
- ).as_i64(),
+ )
+ .as_i64(),
ErrorStatus::UnknownError,
"Failed to interpret x as integer"
);
@@ -647,7 +764,8 @@ impl MarionetteSession {
resp.result.get("y"),
ErrorStatus::UnknownError,
"Failed to find y field"
- ).as_i64(),
+ )
+ .as_i64(),
ErrorStatus::UnknownError,
"Failed to interpret y as integer"
);
@@ -715,7 +833,8 @@ impl MarionetteSession {
resp.result.get("sessionId"),
ErrorStatus::InvalidSessionId,
"Failed to find sessionId field"
- ).as_str(),
+ )
+ .as_str(),
ErrorStatus::InvalidSessionId,
"sessionId is not a string"
);
@@ -725,12 +844,14 @@ impl MarionetteSession {
resp.result.get("capabilities"),
ErrorStatus::UnknownError,
"Failed to find capabilities field"
- ).as_object(),
+ )
+ .as_object(),
ErrorStatus::UnknownError,
"capabilities field is not an object"
- ).clone();
+ )
+ .clone();
- capabilities.insert("moz:geckodriverVersion".into(), BuildInfo.into());
+ capabilities.insert("moz:geckodriverVersion".into(), build::build_info().into());
WebDriverResponse::NewSession(NewSessionResponse::new(
session_id.to_string(),
@@ -739,7 +860,7 @@ impl MarionetteSession {
}
DeleteSession => WebDriverResponse::DeleteSession,
Extension(ref extension) => match extension {
- GetContext => WebDriverResponse::Generic(resp.to_value_response(true)?),
+ GetContext => WebDriverResponse::Generic(resp.into_value_response(true)?),
SetContext(_) => WebDriverResponse::Void,
XblAnonymousChildren(_) => {
let els_vec = try_opt!(
@@ -762,17 +883,228 @@ impl MarionetteSession {
))?;
WebDriverResponse::Generic(ValueResponse(serde_json::to_value(el)?))
}
- InstallAddon(_) => WebDriverResponse::Generic(resp.to_value_response(true)?),
+ InstallAddon(_) => WebDriverResponse::Generic(resp.into_value_response(true)?),
UninstallAddon(_) => WebDriverResponse::Void,
- TakeFullScreenshot => WebDriverResponse::Generic(resp.to_value_response(true)?),
+ TakeFullScreenshot => WebDriverResponse::Generic(resp.into_value_response(true)?),
},
})
}
}
+fn try_convert_to_marionette_message(
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+) -> WebDriverResult<Option<Command>> {
+ use self::GeckoExtensionCommand::*;
+ use self::WebDriverCommand::*;
+
+ Ok(match msg.command {
+ AcceptAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::AcceptAlert)),
+ AddCookie(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::AddCookie(
+ x.to_marionette()?,
+ ))),
+ CloseWindow => Some(Command::WebDriver(MarionetteWebDriverCommand::CloseWindow)),
+ DeleteCookie(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::DeleteCookie(x.clone()),
+ )),
+ DeleteCookies => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::DeleteCookies,
+ )),
+ DeleteSession => Some(Command::Marionette(
+ marionette_rs::marionette::Command::DeleteSession {
+ flags: vec![AppStatus::eForceQuit],
+ },
+ )),
+ DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)),
+ ElementClear(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClear(e.to_marionette()?),
+ )),
+ ElementClick(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClick(e.to_marionette()?),
+ )),
+ ElementSendKeys(ref e, ref x) => {
+ let keys = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementSendKeys {
+ id: e.clone().to_string(),
+ text: keys.text.clone(),
+ value: keys.value.clone(),
+ },
+ ))
+ }
+ ExecuteAsyncScript(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ExecuteAsyncScript(x.to_marionette()?),
+ )),
+ ExecuteScript(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ExecuteScript(x.to_marionette()?),
+ )),
+ FindElement(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::FindElement(
+ x.to_marionette()?,
+ ))),
+ FindElements(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElements(x.to_marionette()?),
+ )),
+ FindElementElement(ref e, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElementElement {
+ element: e.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value.clone(),
+ },
+ ))
+ }
+ FindElementElements(ref e, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElementElements {
+ element: e.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value.clone(),
+ },
+ ))
+ }
+ FullscreenWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FullscreenWindow,
+ )),
+ Get(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Get(
+ x.to_marionette()?,
+ ))),
+ GetActiveElement => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetActiveElement,
+ )),
+ GetAlertText => Some(Command::WebDriver(MarionetteWebDriverCommand::GetAlertText)),
+ GetCookies | GetNamedCookie(_) => {
+ Some(Command::WebDriver(MarionetteWebDriverCommand::GetCookies))
+ }
+ GetCSSValue(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetCSSValue {
+ id: e.clone().to_string(),
+ property: x.clone(),
+ },
+ )),
+ GetCurrentUrl => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetCurrentUrl,
+ )),
+ GetElementAttribute(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementAttribute {
+ id: e.clone().to_string(),
+ name: x.clone(),
+ },
+ )),
+ GetElementProperty(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementProperty {
+ id: e.clone().to_string(),
+ name: x.clone(),
+ },
+ )),
+ GetElementRect(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementRect(x.to_marionette()?),
+ )),
+ GetElementTagName(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementTagName(x.to_marionette()?),
+ )),
+ GetElementText(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementText(x.to_marionette()?),
+ )),
+ GetPageSource => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetPageSource,
+ )),
+ GetTitle => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTitle)),
+ GetWindowHandle => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowHandle,
+ )),
+ GetWindowHandles => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowHandles,
+ )),
+ GetWindowRect => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowRect,
+ )),
+ GetTimeouts => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTimeouts)),
+ GoBack => Some(Command::WebDriver(MarionetteWebDriverCommand::GoBack)),
+ GoForward => Some(Command::WebDriver(MarionetteWebDriverCommand::GoForward)),
+ IsDisplayed(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsDisplayed(
+ x.to_marionette()?,
+ ))),
+ IsEnabled(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsEnabled(
+ x.to_marionette()?,
+ ))),
+ IsSelected(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsSelected(
+ x.to_marionette()?,
+ ))),
+ MaximizeWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::MaximizeWindow,
+ )),
+ MinimizeWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::MinimizeWindow,
+ )),
+ NewWindow(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::NewWindow(
+ x.to_marionette()?,
+ ))),
+ Print(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Print(
+ x.to_marionette()?,
+ ))),
+ Refresh => Some(Command::WebDriver(MarionetteWebDriverCommand::Refresh)),
+ ReleaseActions => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ReleaseActions,
+ )),
+ SendAlertText(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SendAlertText(x.to_marionette()?),
+ )),
+ SetTimeouts(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::SetTimeouts(
+ x.to_marionette()?,
+ ))),
+ SetWindowRect(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SetWindowRect(x.to_marionette()?),
+ )),
+ SwitchToFrame(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToFrame(x.to_marionette()?),
+ )),
+ SwitchToParentFrame => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToParentFrame,
+ )),
+ SwitchToWindow(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToWindow(x.to_marionette()?),
+ )),
+ TakeElementScreenshot(ref e) => {
+ let screenshot = ScreenshotOptions {
+ id: Some(e.clone().to_string()),
+ highlights: vec![],
+ full: false,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeElementScreenshot(screenshot),
+ ))
+ }
+ TakeScreenshot => {
+ let screenshot = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: false,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeScreenshot(screenshot),
+ ))
+ }
+ Extension(ref extension) => match extension {
+ TakeFullScreenshot => {
+ let screenshot = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: true,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeFullScreenshot(screenshot),
+ ))
+ }
+ _ => None,
+ },
+ _ => None,
+ })
+}
+
#[derive(Debug, PartialEq)]
pub struct MarionetteCommand {
- pub id: u64,
+ pub id: MessageId,
pub name: String,
pub params: Map<String, Value>,
}
@@ -788,218 +1120,84 @@ impl Serialize for MarionetteCommand {
}
impl MarionetteCommand {
- fn new(id: u64, name: String, params: Map<String, Value>) -> MarionetteCommand {
- MarionetteCommand {
- id: id,
- name: name,
- params: params,
- }
+ fn new(id: MessageId, name: String, params: Map<String, Value>) -> MarionetteCommand {
+ MarionetteCommand { id, name, params }
+ }
+
+ fn encode_msg<T>(msg: T) -> WebDriverResult<String>
+ where
+ T: serde::Serialize,
+ {
+ let data = serde_json::to_string(&msg)?;
+
+ Ok(format!("{}:{}", data.len(), data))
}
fn from_webdriver_message(
- id: u64,
+ id: MessageId,
capabilities: Option<Map<String, Value>>,
msg: &WebDriverMessage<GeckoExtensionRoute>,
- ) -> WebDriverResult<MarionetteCommand> {
+ ) -> WebDriverResult<String> {
use self::GeckoExtensionCommand::*;
- let (opt_name, opt_parameters) = match msg.command {
- Status => panic!("Got status command that should already have been handled"),
- AcceptAlert => {
- // Needs to be updated to "WebDriver:AcceptAlert" for Firefox 63
- (Some("WebDriver:AcceptDialog"), None)
- }
- AddCookie(ref x) => (Some("WebDriver:AddCookie"), Some(x.to_marionette())),
- NewWindow(ref x) => (Some("WebDriver:NewWindow"), Some(x.to_marionette())),
- CloseWindow => (Some("WebDriver:CloseWindow"), None),
- DeleteCookie(ref x) => {
- let mut data = Map::new();
- data.insert("name".to_string(), Value::String(x.clone()));
- (Some("WebDriver:DeleteCookie"), Some(Ok(data)))
- }
- DeleteCookies => (Some("WebDriver:DeleteAllCookies"), None),
- DeleteSession => {
- let mut body = Map::new();
- body.insert(
- "flags".to_owned(),
- serde_json::to_value(vec!["eForceQuit".to_string()])?,
- );
- (Some("Marionette:Quit"), Some(Ok(body)))
- }
- DismissAlert => (Some("WebDriver:DismissAlert"), None),
- ElementClear(ref x) => (Some("WebDriver:ElementClear"), Some(x.to_marionette())),
- ElementClick(ref x) => (Some("WebDriver:ElementClick"), Some(x.to_marionette())),
- ElementSendKeys(ref e, ref x) => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::String(e.id.clone()));
- data.insert("text".to_string(), Value::String(x.text.clone()));
- data.insert(
- "value".to_string(),
- serde_json::to_value(
- x.text
- .chars()
- .map(|x| x.to_string())
- .collect::<Vec<String>>(),
- )?,
- );
- (Some("WebDriver:ElementSendKeys"), Some(Ok(data)))
- }
- ExecuteAsyncScript(ref x) => (
- Some("WebDriver:ExecuteAsyncScript"),
- Some(x.to_marionette()),
- ),
- ExecuteScript(ref x) => (Some("WebDriver:ExecuteScript"), Some(x.to_marionette())),
- FindElement(ref x) => (Some("WebDriver:FindElement"), Some(x.to_marionette())),
- FindElementElement(ref e, ref x) => {
- let mut data = x.to_marionette()?;
- data.insert("element".to_string(), Value::String(e.id.clone()));
- (Some("WebDriver:FindElement"), Some(Ok(data)))
- }
- FindElements(ref x) => (Some("WebDriver:FindElements"), Some(x.to_marionette())),
- FindElementElements(ref e, ref x) => {
- let mut data = x.to_marionette()?;
- data.insert("element".to_string(), Value::String(e.id.clone()));
- (Some("WebDriver:FindElements"), Some(Ok(data)))
- }
- FullscreenWindow => (Some("WebDriver:FullscreenWindow"), None),
- Get(ref x) => (Some("WebDriver:Navigate"), Some(x.to_marionette())),
- GetAlertText => (Some("WebDriver:GetAlertText"), None),
- GetActiveElement => (Some("WebDriver:GetActiveElement"), None),
- GetCookies | GetNamedCookie(_) => (Some("WebDriver:GetCookies"), None),
- GetCurrentUrl => (Some("WebDriver:GetCurrentURL"), None),
- GetCSSValue(ref e, ref x) => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::String(e.id.clone()));
- data.insert("propertyName".to_string(), Value::String(x.clone()));
- (Some("WebDriver:GetElementCSSValue"), Some(Ok(data)))
- }
- GetElementAttribute(ref e, ref x) => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::String(e.id.clone()));
- data.insert("name".to_string(), Value::String(x.clone()));
- (Some("WebDriver:GetElementAttribute"), Some(Ok(data)))
- }
- GetElementProperty(ref e, ref x) => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::String(e.id.clone()));
- data.insert("name".to_string(), Value::String(x.clone()));
- (Some("WebDriver:GetElementProperty"), Some(Ok(data)))
- }
- GetElementRect(ref x) => (Some("WebDriver:GetElementRect"), Some(x.to_marionette())),
- GetElementTagName(ref x) => {
- (Some("WebDriver:GetElementTagName"), Some(x.to_marionette()))
- }
- GetElementText(ref x) => (Some("WebDriver:GetElementText"), Some(x.to_marionette())),
- GetPageSource => (Some("WebDriver:GetPageSource"), None),
- GetTimeouts => (Some("WebDriver:GetTimeouts"), None),
- GetTitle => (Some("WebDriver:GetTitle"), None),
- GetWindowHandle => (Some("WebDriver:GetWindowHandle"), None),
- GetWindowHandles => (Some("WebDriver:GetWindowHandles"), None),
- GetWindowRect => (Some("WebDriver:GetWindowRect"), None),
- GoBack => (Some("WebDriver:Back"), None),
- GoForward => (Some("WebDriver:Forward"), None),
- IsDisplayed(ref x) => (
- Some("WebDriver:IsElementDisplayed"),
- Some(x.to_marionette()),
- ),
- IsEnabled(ref x) => (Some("WebDriver:IsElementEnabled"), Some(x.to_marionette())),
- IsSelected(ref x) => (Some("WebDriver:IsElementSelected"), Some(x.to_marionette())),
- MaximizeWindow => (Some("WebDriver:MaximizeWindow"), None),
- MinimizeWindow => (Some("WebDriver:MinimizeWindow"), None),
- NewSession(_) => {
- let caps = capabilities
- .expect("Tried to create new session without processing capabilities");
-
- let mut data = Map::new();
- for (k, v) in caps.iter() {
- data.insert(k.to_string(), serde_json::to_value(v)?);
- }
+ if let Some(cmd) = try_convert_to_marionette_message(msg)? {
+ let req = Message::Incoming(Request(id, cmd));
+ MarionetteCommand::encode_msg(req)
+ } else {
+ let (opt_name, opt_parameters) = match msg.command {
+ Status => panic!("Got status command that should already have been handled"),
+ NewSession(_) => {
+ let caps = capabilities
+ .expect("Tried to create new session without processing capabilities");
- (Some("WebDriver:NewSession"), Some(Ok(data)))
- }
- PerformActions(ref x) => (Some("WebDriver:PerformActions"), Some(x.to_marionette())),
- Refresh => (Some("WebDriver:Refresh"), None),
- ReleaseActions => (Some("WebDriver:ReleaseActions"), None),
- SendAlertText(ref x) => {
- let mut data = Map::new();
- data.insert("text".to_string(), Value::String(x.text.clone()));
- data.insert(
- "value".to_string(),
- serde_json::to_value(
- x.text
- .chars()
- .map(|x| x.to_string())
- .collect::<Vec<String>>(),
- )?,
- );
- (Some("WebDriver:SendAlertText"), Some(Ok(data)))
- }
- SetTimeouts(ref x) => (Some("WebDriver:SetTimeouts"), Some(x.to_marionette())),
- SetWindowRect(ref x) => (Some("WebDriver:SetWindowRect"), Some(x.to_marionette())),
- SwitchToFrame(ref x) => (Some("WebDriver:SwitchToFrame"), Some(x.to_marionette())),
- SwitchToParentFrame => (Some("WebDriver:SwitchToParentFrame"), None),
- SwitchToWindow(ref x) => (Some("WebDriver:SwitchToWindow"), Some(x.to_marionette())),
- TakeElementScreenshot(ref e) => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::String(e.id.clone()));
- data.insert("highlights".to_string(), Value::Array(vec![]));
- data.insert("full".to_string(), Value::Bool(false));
- (Some("WebDriver:TakeScreenshot"), Some(Ok(data)))
- }
- TakeScreenshot => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::Null);
- data.insert("highlights".to_string(), Value::Array(vec![]));
- data.insert("full".to_string(), Value::Bool(false));
- (Some("WebDriver:TakeScreenshot"), Some(Ok(data)))
- }
- Extension(ref extension) => match extension {
- GetContext => (Some("Marionette:GetContext"), None),
- InstallAddon(x) => {
- (Some("Addon:Install"), Some(x.to_marionette()))
- }
- SetContext(x) => {
- (Some("Marionette:SetContext"), Some(x.to_marionette()))
- }
- UninstallAddon(x) => {
- (Some("Addon:Uninstall"), Some(x.to_marionette()))
- }
- XblAnonymousByAttribute(e, x) => {
- let mut data = x.to_marionette()?;
- data.insert("element".to_string(), Value::String(e.id.clone()));
- (Some("WebDriver:FindElement"), Some(Ok(data)))
- }
- XblAnonymousChildren(e) => {
let mut data = Map::new();
- data.insert("using".to_owned(), serde_json::to_value("anon")?);
- data.insert("value".to_owned(), Value::Null);
- data.insert("element".to_string(), serde_json::to_value(e.id.clone())?);
- (Some("WebDriver:FindElements"), Some(Ok(data)))
+ for (k, v) in caps.iter() {
+ data.insert(k.to_string(), serde_json::to_value(v)?);
+ }
+
+ (Some("WebDriver:NewSession"), Some(Ok(data)))
}
- TakeFullScreenshot => {
- let mut data = Map::new();
- data.insert("id".to_string(), Value::Null);
- data.insert("highlights".to_string(), Value::Array(vec![]));
- data.insert("full".to_string(), Value::Bool(true));
- (Some("WebDriver:TakeScreenshot"), Some(Ok(data)))
+ PerformActions(ref x) => {
+ (Some("WebDriver:PerformActions"), Some(x.to_marionette()))
}
- },
- };
+ Extension(ref extension) => match extension {
+ GetContext => (Some("Marionette:GetContext"), None),
+ InstallAddon(x) => (Some("Addon:Install"), Some(x.to_marionette())),
+ SetContext(x) => (Some("Marionette:SetContext"), Some(x.to_marionette())),
+ UninstallAddon(x) => (Some("Addon:Uninstall"), Some(x.to_marionette())),
+ XblAnonymousByAttribute(e, x) => {
+ let mut data = x.to_marionette()?;
+ data.insert("element".to_string(), Value::String(e.to_string()));
+ (Some("WebDriver:FindElement"), Some(Ok(data)))
+ }
+ XblAnonymousChildren(e) => {
+ let mut data = Map::new();
+ data.insert("using".to_owned(), serde_json::to_value("anon")?);
+ data.insert("value".to_owned(), Value::Null);
+ data.insert("element".to_string(), serde_json::to_value(e.to_string())?);
+ (Some("WebDriver:FindElements"), Some(Ok(data)))
+ }
+ _ => (None, None),
+ },
+ _ => (None, None),
+ };
- let name = try_opt!(
- opt_name,
- ErrorStatus::UnsupportedOperation,
- "Operation not supported"
- );
- let parameters = opt_parameters.unwrap_or(Ok(Map::new()))?;
+ let name = try_opt!(
+ opt_name,
+ ErrorStatus::UnsupportedOperation,
+ "Operation not supported"
+ );
+ let parameters = opt_parameters.unwrap_or_else(|| Ok(Map::new()))?;
- Ok(MarionetteCommand::new(id, name.into(), parameters))
+ let req = MarionetteCommand::new(id, name.into(), parameters);
+ MarionetteCommand::encode_msg(req)
+ }
}
}
#[derive(Debug, PartialEq)]
pub struct MarionetteResponse {
- pub id: u64,
+ pub id: MessageId,
pub error: Option<MarionetteError>,
pub result: Value,
}
@@ -1012,7 +1210,7 @@ impl<'de> Deserialize<'de> for MarionetteResponse {
#[derive(Deserialize)]
struct ResponseWrapper {
msg_type: u64,
- id: u64,
+ id: MessageId,
error: Option<MarionetteError>,
result: Value,
}
@@ -1034,14 +1232,15 @@ impl<'de> Deserialize<'de> for MarionetteResponse {
}
impl MarionetteResponse {
- fn to_value_response(self, value_required: bool) -> WebDriverResult<ValueResponse> {
- let value: &Value = match value_required {
- true => try_opt!(
+ fn into_value_response(self, value_required: bool) -> WebDriverResult<ValueResponse> {
+ let value: &Value = if value_required {
+ try_opt!(
self.result.get("value"),
ErrorStatus::UnknownError,
"Failed to find value field"
- ),
- false => &self.result,
+ )
+ } else {
+ &self.result
};
Ok(ValueResponse(value.clone()))
@@ -1093,7 +1292,7 @@ impl MarionetteConnection {
}
}
- pub fn connect(&mut self, browser: &mut Option<FirefoxProcess>) -> WebDriverResult<()> {
+ pub fn connect(&mut self, browser: &mut Option<Browser>) -> WebDriverResult<()> {
let timeout = time::Duration::from_secs(60);
let poll_interval = time::Duration::from_millis(100);
let now = time::Instant::now();
@@ -1104,15 +1303,16 @@ impl MarionetteConnection {
self.host,
self.port
);
+
loop {
// immediately abort connection attempts if process disappears
- if let &mut Some(ref mut runner) = browser {
+ if let Some(Browser::Host(ref mut runner)) = *browser {
let exit_status = match runner.try_wait() {
Ok(Some(status)) => Some(
status
.code()
.map(|c| c.to_string())
- .unwrap_or("signal".into()),
+ .unwrap_or_else(|| "signal".into()),
),
Ok(None) => None,
Err(_) => Some("{unknown}".into()),
@@ -1125,58 +1325,58 @@ impl MarionetteConnection {
}
}
- match TcpStream::connect((&self.host[..], self.port)) {
- Ok(stream) => {
+ let try_connect = || -> WebDriverResult<(TcpStream, MarionetteHandshake)> {
+ let mut stream = TcpStream::connect((&self.host[..], self.port))?;
+ let data = MarionetteConnection::handshake(&mut stream)?;
+
+ Ok((stream, data))
+ };
+
+ match try_connect() {
+ Ok((stream, data)) => {
+ debug!(
+ "Connection to Marionette established on {}:{}.",
+ self.host, self.port,
+ );
+
self.stream = Some(stream);
+ self.session.application_type = Some(data.application_type);
+ self.session.protocol = Some(data.protocol);
break;
}
Err(e) => {
if now.elapsed() < timeout {
thread::sleep(poll_interval);
} else {
- return Err(WebDriverError::new(
- ErrorStatus::UnknownError,
- e.description().to_owned(),
- ));
+ return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string()));
}
}
}
}
- debug!(
- "Connection established on {}:{}. Waiting for Marionette handshake",
- self.host, self.port,
- );
-
- let data = self.handshake()?;
- self.session.application_type = Some(data.application_type);
- self.session.protocol = Some(data.protocol);
-
- debug!("Connected to Marionette");
Ok(())
}
- fn handshake(&mut self) -> WebDriverResult<MarionetteHandshake> {
- let resp = (match self.stream.as_mut().unwrap().read_timeout() {
+ fn handshake(stream: &mut TcpStream) -> WebDriverResult<MarionetteHandshake> {
+ let resp = (match stream.read_timeout() {
Ok(timeout) => {
// If platform supports changing the read timeout of the stream,
// use a short one only for the handshake with Marionette.
- self.stream
- .as_mut()
- .unwrap()
- .set_read_timeout(Some(time::Duration::from_secs(10)))
+ stream
+ .set_read_timeout(Some(time::Duration::from_millis(100)))
.ok();
- let data = self.read_resp();
- self.stream.as_mut().unwrap().set_read_timeout(timeout).ok();
+ let data = MarionetteConnection::read_resp(stream);
+ stream.set_read_timeout(timeout).ok();
data
}
- _ => self.read_resp(),
- }).or_else(|e| {
- Err(WebDriverError::new(
+ _ => MarionetteConnection::read_resp(stream),
+ })
+ .map_err(|e| {
+ WebDriverError::new(
ErrorStatus::UnknownError,
format!("Socket timeout reading Marionette handshake data: {}", e),
- ))
+ )
})?;
let data = serde_json::from_str::<MarionetteHandshake>(&resp)?;
@@ -1184,10 +1384,7 @@ impl MarionetteConnection {
if data.application_type != "gecko" {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
- format!(
- "Unrecognized application type {}",
- data.application_type
- ),
+ format!("Unrecognized application type {}", data.application_type),
));
}
@@ -1206,38 +1403,32 @@ impl MarionetteConnection {
pub fn close(&self) {}
- fn encode_msg(&self, msg: MarionetteCommand) -> WebDriverResult<String> {
- let data = serde_json::to_string(&msg)?;
-
- Ok(format!("{}:{}", data.len(), data))
- }
-
pub fn send_command(
&mut self,
capabilities: Option<Map<String, Value>>,
msg: &WebDriverMessage<GeckoExtensionRoute>,
) -> WebDriverResult<WebDriverResponse> {
let id = self.session.next_command_id();
- let command = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?;
- let resp_data = self.send(command)?;
+ let enc_cmd = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?;
+ let resp_data = self.send(enc_cmd)?;
let data: MarionetteResponse = serde_json::from_str(&resp_data)?;
self.session.response(msg, data)
}
- fn send(&mut self, msg: MarionetteCommand) -> WebDriverResult<String> {
- let data = self.encode_msg(msg)?;
-
- match self.stream {
+ fn send(&mut self, data: String) -> WebDriverResult<String> {
+ let stream = match self.stream {
Some(ref mut stream) => {
if stream.write(&*data.as_bytes()).is_err() {
let mut err = WebDriverError::new(
ErrorStatus::UnknownError,
- "Failed to write response to stream",
+ "Failed to write request to stream",
);
err.delete_session = true;
return Err(err);
}
+
+ stream
}
None => {
let mut err = WebDriverError::new(
@@ -1247,9 +1438,9 @@ impl MarionetteConnection {
err.delete_session = true;
return Err(err);
}
- }
+ };
- match self.read_resp() {
+ match MarionetteConnection::read_resp(stream) {
Ok(resp) => Ok(resp),
Err(_) => {
let mut err = WebDriverError::new(
@@ -1262,11 +1453,9 @@ impl MarionetteConnection {
}
}
- fn read_resp(&mut self) -> IoResult<String> {
+ fn read_resp(stream: &mut TcpStream) -> IoResult<String> {
let mut bytes = 0usize;
- // TODO(jgraham): Check before we unwrap?
- let stream = self.stream.as_mut().unwrap();
loop {
let buf = &mut [0 as u8];
let num_read = stream.read(buf)?;
@@ -1281,8 +1470,8 @@ impl MarionetteConnection {
_ => panic!("Expected one byte got more"),
};
match byte {
- '0'...'9' => {
- bytes = bytes * 10;
+ '0'..='9' => {
+ bytes *= 10;
bytes += byte as usize - '0' as usize;
}
':' => break,
@@ -1312,22 +1501,25 @@ impl MarionetteConnection {
}
}
-trait ToMarionette {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>>;
+trait ToMarionette<T> {
+ fn to_marionette(&self) -> WebDriverResult<T>;
}
-impl ToMarionette for AddonInstallParameters {
+impl ToMarionette<Map<String, Value>> for AddonInstallParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
let mut data = Map::new();
data.insert("path".to_string(), serde_json::to_value(&self.path)?);
if self.temporary.is_some() {
- data.insert("temporary".to_string(), serde_json::to_value(&self.temporary)?);
+ data.insert(
+ "temporary".to_string(),
+ serde_json::to_value(&self.temporary)?,
+ );
}
Ok(data)
}
}
-impl ToMarionette for AddonUninstallParameters {
+impl ToMarionette<Map<String, Value>> for AddonUninstallParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
let mut data = Map::new();
data.insert("id".to_string(), Value::String(self.id.clone()));
@@ -1335,7 +1527,7 @@ impl ToMarionette for AddonUninstallParameters {
}
}
-impl ToMarionette for GeckoContextParameters {
+impl ToMarionette<Map<String, Value>> for GeckoContextParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
let mut data = Map::new();
data.insert(
@@ -1346,7 +1538,50 @@ impl ToMarionette for GeckoContextParameters {
}
}
-impl ToMarionette for XblLocatorParameters {
+impl ToMarionette<MarionettePrintParameters> for PrintParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintParameters> {
+ Ok(MarionettePrintParameters {
+ orientation: self.orientation.to_marionette()?,
+ scale: self.scale,
+ background: self.background,
+ page: self.page.to_marionette()?,
+ margin: self.margin.to_marionette()?,
+ page_ranges: self.page_ranges.clone(),
+ shrink_to_fit: self.shrink_to_fit,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintOrientation> for PrintOrientation {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintOrientation> {
+ Ok(match self {
+ PrintOrientation::Landscape => MarionettePrintOrientation::Landscape,
+ PrintOrientation::Portrait => MarionettePrintOrientation::Portrait,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintPage> for PrintPage {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintPage> {
+ Ok(MarionettePrintPage {
+ width: self.width,
+ height: self.height,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintMargins> for PrintMargins {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintMargins> {
+ Ok(MarionettePrintMargins {
+ top: self.top,
+ bottom: self.bottom,
+ left: self.left,
+ right: self.right,
+ })
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for XblLocatorParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
let mut value = Map::new();
value.insert(self.name.to_owned(), Value::String(self.value.clone()));
@@ -1361,154 +1596,167 @@ impl ToMarionette for XblLocatorParameters {
}
}
-impl ToMarionette for ActionsParameters {
+impl ToMarionette<Map<String, Value>> for ActionsParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
Ok(try_opt!(
serde_json::to_value(self)?.as_object(),
ErrorStatus::UnknownError,
"Expected an object"
- ).clone())
+ )
+ .clone())
}
}
-impl ToMarionette for AddCookieParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut cookie = Map::new();
- cookie.insert("name".to_string(), serde_json::to_value(&self.name)?);
- cookie.insert("value".to_string(), serde_json::to_value(&self.value)?);
- if self.path.is_some() {
- cookie.insert("path".to_string(), serde_json::to_value(&self.path)?);
- }
- if self.domain.is_some() {
- cookie.insert("domain".to_string(), serde_json::to_value(&self.domain)?);
- }
- if self.expiry.is_some() {
- cookie.insert("expiry".to_string(), serde_json::to_value(&self.expiry)?);
- }
- cookie.insert("secure".to_string(), serde_json::to_value(self.secure)?);
- cookie.insert("httpOnly".to_string(), serde_json::to_value(self.httpOnly)?);
-
- let mut data = Map::new();
- data.insert("cookie".to_string(), serde_json::to_value(cookie)?);
- Ok(data)
+impl ToMarionette<MarionetteCookie> for AddCookieParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteCookie> {
+ Ok(MarionetteCookie {
+ name: self.name.clone(),
+ value: self.value.clone(),
+ path: self.path.clone(),
+ domain: self.domain.clone(),
+ secure: self.secure,
+ http_only: self.httpOnly,
+ expiry: match &self.expiry {
+ Some(date) => Some(date.to_marionette()?),
+ None => None,
+ },
+ same_site: self.sameSite.clone(),
+ })
}
}
-impl ToMarionette for FrameId {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut data = Map::new();
- match *self {
- FrameId::Short(x) => data.insert("id".to_string(), serde_json::to_value(x)?),
- FrameId::Element(ref x) => data.insert(
- "element".to_string(),
- Value::Object(x.to_marionette()?),
- ),
- };
- Ok(data)
+impl ToMarionette<MarionetteDate> for Date {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteDate> {
+ Ok(MarionetteDate(self.0))
}
}
-impl ToMarionette for GetNamedCookieParameters {
+impl ToMarionette<Map<String, Value>> for GetNamedCookieParameters {
fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
Ok(try_opt!(
serde_json::to_value(self)?.as_object(),
ErrorStatus::UnknownError,
"Expected an object"
- ).clone())
+ )
+ .clone())
}
}
-impl ToMarionette for GetParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- Ok(try_opt!(
- serde_json::to_value(self)?.as_object(),
- ErrorStatus::UnknownError,
- "Expected an object"
- ).clone())
+impl ToMarionette<MarionetteUrl> for GetParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteUrl> {
+ Ok(MarionetteUrl {
+ url: self.url.clone(),
+ })
}
}
-impl ToMarionette for JavascriptCommandParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- Ok(try_opt!(
- serde_json::to_value(self)?.as_object(),
- ErrorStatus::UnknownError,
- "Expected an object"
- ).clone())
+impl ToMarionette<MarionetteScript> for JavascriptCommandParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteScript> {
+ Ok(MarionetteScript {
+ script: self.script.clone(),
+ args: self.args.clone(),
+ })
}
}
-impl ToMarionette for LocatorParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- Ok(try_opt!(
- serde_json::to_value(self)?.as_object(),
- ErrorStatus::UnknownError,
- "Expected an object"
- ).clone())
+impl ToMarionette<MarionetteLocator> for LocatorParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteLocator> {
+ Ok(MarionetteLocator {
+ using: self.using.to_marionette()?,
+ value: self.value.clone(),
+ })
}
}
-impl ToMarionette for NewWindowParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut data = Map::new();
- if let Some(ref x) = self.type_hint {
- data.insert("type".to_string(), serde_json::to_value(x)?);
+impl ToMarionette<MarionetteSelector> for LocatorStrategy {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteSelector> {
+ use self::LocatorStrategy::*;
+ match self {
+ CSSSelector => Ok(MarionetteSelector::CSS),
+ LinkText => Ok(MarionetteSelector::LinkText),
+ PartialLinkText => Ok(MarionetteSelector::PartialLinkText),
+ TagName => Ok(MarionetteSelector::TagName),
+ XPath => Ok(MarionetteSelector::XPath),
}
- Ok(data)
}
}
-impl ToMarionette for SwitchToFrameParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut data = Map::new();
- let key = match self.id {
- None => None,
- Some(FrameId::Short(_)) => Some("id"),
- Some(FrameId::Element(_)) => Some("element"),
- };
- if let Some(x) = key {
- data.insert(x.to_string(), serde_json::to_value(&self.id)?);
- }
- Ok(data)
+impl ToMarionette<MarionetteNewWindow> for NewWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteNewWindow> {
+ Ok(MarionetteNewWindow {
+ type_hint: self.type_hint.clone(),
+ })
}
}
-impl ToMarionette for SwitchToWindowParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut data = Map::new();
- data.insert(
- "name".to_string(),
- serde_json::to_value(self.handle.clone())?,
- );
- Ok(data)
+impl ToMarionette<MarionetteKeys> for SendKeysParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteKeys> {
+ Ok(MarionetteKeys {
+ text: self.text.clone(),
+ value: self
+ .text
+ .chars()
+ .map(|x| x.to_string())
+ .collect::<Vec<String>>(),
+ })
}
}
-impl ToMarionette for TimeoutsParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- Ok(try_opt!(
- serde_json::to_value(self)?.as_object(),
- ErrorStatus::UnknownError,
- "Expected an object"
- ).clone())
+impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> {
+ Ok(match &self.id {
+ Some(x) => match x {
+ FrameId::Short(n) => MarionetteFrame::Index(n.clone()),
+ FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()),
+ },
+ None => MarionetteFrame::Parent,
+ })
}
}
-impl ToMarionette for WebElement {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- let mut data = Map::new();
- data.insert("id".to_string(), serde_json::to_value(&self.id)?);
- Ok(data)
+impl ToMarionette<Window> for SwitchToWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<Window> {
+ Ok(Window {
+ name: self.handle.clone(),
+ handle: self.handle.clone(),
+ })
}
}
-impl ToMarionette for WindowRectParameters {
- fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
- Ok(try_opt!(
- serde_json::to_value(self)?.as_object(),
- ErrorStatus::UnknownError,
- "Expected an object"
- ).clone())
+impl ToMarionette<MarionetteTimeouts> for TimeoutsParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteTimeouts> {
+ Ok(MarionetteTimeouts {
+ implicit: self.implicit,
+ page_load: self.page_load,
+ script: self.script,
+ })
+ }
+}
+
+impl ToMarionette<LegacyWebElement> for WebElement {
+ fn to_marionette(&self) -> WebDriverResult<LegacyWebElement> {
+ Ok(LegacyWebElement {
+ id: self.to_string(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteWebElement> for WebElement {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteWebElement> {
+ Ok(MarionetteWebElement {
+ element: self.to_string(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteWindowRect> for WindowRectParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteWindowRect> {
+ Ok(MarionetteWindowRect {
+ x: self.x,
+ y: self.y,
+ width: self.width,
+ height: self.height,
+ })
}
}
@@ -1523,7 +1771,7 @@ mod tests {
// several regressions related to marionette.log.level.
#[test]
fn test_marionette_log_level() {
- let mut profile = Profile::new(None).unwrap();
+ let mut profile = Profile::new().unwrap();
let handler = MarionetteHandler::new(MarionetteSettings::default());
handler.set_prefs(2828, &mut profile, false, vec![]).ok();
let user_prefs = profile.user_prefs().unwrap();
diff --git a/src/prefs.rs b/src/prefs.rs
index 171ccb6..dfcaac2 100644
--- a/src/prefs.rs
+++ b/src/prefs.rs
@@ -47,11 +47,6 @@ lazy_static! {
// Skip check for default browser on startup
("browser.shell.checkDefaultBrowser", Pref::new(false)),
- // Disable Android snippets
- ("browser.snippets.enabled", Pref::new(false)),
- ("browser.snippets.syncPromo.enabled", Pref::new(false)),
- ("browser.snippets.firstrunHomepage.enabled", Pref::new(false)),
-
// Do not redirect user when a milestone upgrade of Firefox
// is detected
("browser.startup.homepage_override.mstone", Pref::new("ignore")),
@@ -96,10 +91,6 @@ lazy_static! {
// Disable intalling any distribution extensions or add-ons
("extensions.installDistroAddons", Pref::new(false)),
- // Make sure Shield doesn't hit the network.
- // TODO: Remove once minimum supported Firefox release is 60.
- ("extensions.shield-recipe-client.api_url", Pref::new("")),
-
// Disable extensions compatibility dialogue.
// TODO: Remove once minimum supported Firefox release is 61.
("extensions.showMismatchUI", Pref::new(false)),
@@ -125,6 +116,10 @@ lazy_static! {
// No hang monitor
("hangmonitor.timeout", Pref::new(0)),
+ // Disable idle-daily notifications to avoid expensive operations
+ // that may cause unexpected test timeouts.
+ ("idle.lastDailyNotification", Pref::new(-1)),
+
// Show chrome errors and warnings in the error console
("javascript.options.showInConsole", Pref::new(true)),
@@ -146,6 +141,9 @@ lazy_static! {
// c.f. https://github.com/mozilla/geckodriver/issues/225.
("plugin.state.flash", Pref::new(0)),
+ // Don't do network connections for mitm priming
+ ("security.certerrors.mitm.priming.enabled", Pref::new(false)),
+
// Ensure blocklist updates don't hit the network
("services.settings.server", Pref::new("http://%(server)s/dummy/blocklist/")),
diff --git a/src/test.rs b/src/test.rs
index 6010174..71cacee 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -1,19 +1,8 @@
-use regex::Regex;
-use serde;
-use serde_json;
-use std;
-
-lazy_static! {
- static ref MIN_REGEX: Regex = Regex::new(r"[\n\t]|\s{4}").unwrap();
-}
-
-pub fn check_deserialize<T>(json: &str, data: &T)
+pub fn assert_de<T>(data: &T, json: serde_json::Value)
where
T: std::fmt::Debug,
T: std::cmp::PartialEq,
T: serde::de::DeserializeOwned,
{
- let min_json = MIN_REGEX.replace_all(json, "");
-
- assert_eq!(serde_json::from_str::<T>(&min_json).unwrap(), *data);
+ assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
}