summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver
diff options
context:
space:
mode:
Diffstat (limited to 'testing/geckodriver')
-rw-r--r--testing/geckodriver/.cargo/config3
-rw-r--r--testing/geckodriver/CHANGES.md1831
-rw-r--r--testing/geckodriver/CONTRIBUTING.md2
-rw-r--r--testing/geckodriver/Cargo.toml51
-rw-r--r--testing/geckodriver/ISSUE_TEMPLATE.md31
-rw-r--r--testing/geckodriver/LICENSE385
-rw-r--r--testing/geckodriver/README.md85
-rw-r--r--testing/geckodriver/build.rs136
-rw-r--r--testing/geckodriver/doc/ARM.md50
-rw-r--r--testing/geckodriver/doc/Bugs.md45
-rw-r--r--testing/geckodriver/doc/Building.md46
-rw-r--r--testing/geckodriver/doc/Capabilities.md98
-rw-r--r--testing/geckodriver/doc/CrashReports.md67
-rw-r--r--testing/geckodriver/doc/Flags.md216
-rw-r--r--testing/geckodriver/doc/Notarization.md44
-rw-r--r--testing/geckodriver/doc/Patches.md31
-rw-r--r--testing/geckodriver/doc/Profiles.md103
-rw-r--r--testing/geckodriver/doc/Releasing.md292
-rw-r--r--testing/geckodriver/doc/Support.md183
-rw-r--r--testing/geckodriver/doc/Testing.md69
-rw-r--r--testing/geckodriver/doc/TraceLogs.md206
-rw-r--r--testing/geckodriver/doc/Usage.md143
-rw-r--r--testing/geckodriver/doc/index.rst55
-rw-r--r--testing/geckodriver/mach_commands.py122
-rw-r--r--testing/geckodriver/marionette/Cargo.toml14
-rw-r--r--testing/geckodriver/marionette/src/common.rs240
-rw-r--r--testing/geckodriver/marionette/src/error.rs184
-rw-r--r--testing/geckodriver/marionette/src/lib.rs14
-rw-r--r--testing/geckodriver/marionette/src/marionette.rs69
-rw-r--r--testing/geckodriver/marionette/src/message.rs336
-rw-r--r--testing/geckodriver/marionette/src/result.rs223
-rw-r--r--testing/geckodriver/marionette/src/test.rs35
-rw-r--r--testing/geckodriver/marionette/src/webdriver.rs512
-rw-r--r--testing/geckodriver/moz.build27
-rw-r--r--testing/geckodriver/src/android.rs533
-rw-r--r--testing/geckodriver/src/browser.rs556
-rw-r--r--testing/geckodriver/src/build.rs47
-rw-r--r--testing/geckodriver/src/capabilities.rs1417
-rw-r--r--testing/geckodriver/src/command.rs343
-rw-r--r--testing/geckodriver/src/logging.rs403
-rw-r--r--testing/geckodriver/src/main.rs549
-rw-r--r--testing/geckodriver/src/marionette.rs1623
-rw-r--r--testing/geckodriver/src/prefs.rs150
-rw-r--r--testing/geckodriver/src/test.rs12
-rw-r--r--testing/geckodriver/src/tests/profile.zipbin0 -> 444 bytes
45 files changed, 11581 insertions, 0 deletions
diff --git a/testing/geckodriver/.cargo/config b/testing/geckodriver/.cargo/config
new file mode 100644
index 0000000000..18086c84c5
--- /dev/null
+++ b/testing/geckodriver/.cargo/config
@@ -0,0 +1,3 @@
+[target.i686-pc-windows-gnu]
+linker = "i686-w64-mingw32-gcc"
+rustflags = "-C panic=abort"
diff --git a/testing/geckodriver/CHANGES.md b/testing/geckodriver/CHANGES.md
new file mode 100644
index 0000000000..3fb016a82d
--- /dev/null
+++ b/testing/geckodriver/CHANGES.md
@@ -0,0 +1,1831 @@
+<!-- markdownlint-disable MD024 -->
+# Change log
+
+All notable changes to this program are documented in this file.
+
+## 0.33.0 (2023-04-03, `a80e5fd61076`)
+
+### Known problems
+
+- _Startup hang with Firefox running in a container (e.g. snap, flatpak):_
+
+ When Firefox is packaged inside a container (like the default Firefox browser
+ shipped with Ubuntu 22.04), it may see a different filesystem to the host.
+ This can affect access to the generated profile directory, which may result
+ in a hang when starting Firefox. Workarounds are listed in the geckodriver
+ [usage documentation].
+
+- _Potential hang with `moz:debuggerAddress` capability set to `true`:_
+
+ After enabling the site-isolation feature in Firefox with geckodriver 0.32.1
+ some WebDriver clients like Selenium that use the Chrome DevTools Protocol (CDP)
+ by default for logging events could trigger a hang in Firefox's experimental CDP
+ implementation. The fix for this problem will be shipped with Firefox 112.
+ Until then the following Firefox preferences should be set:
+
+ - `fission.bfcacheInParent: false`
+ - `fission.webContentIsolationStrategy: 0`
+
+### Added
+
+- Support for [Get Computed Label] and [Get Computed Role]
+
+ The command [Get Computed Label] returns the accessibility label (sometimes
+ also referred to as Accessible Name), which is a short string that labels the
+ function of the control (e.g. the string "Comment" or "Sign In" on a button).
+
+ The command [Get Computed Role] returns the reserved token value (in ARIA,
+ button, heading, etc.) that describes the type of control or content in the
+ element.
+
+ Note that the minimum required Firefox version is 113.0.
+
+- Support for [Find Element From Shadow Root] and [Find Elements From Shadow Root]
+
+ The commands allow a lookup of individual elements or collections of elements
+ within an open or closed Shadow DOM. All location strategies except `Tag name` and
+ `XPath selector` are currently supported.
+
+ Note that the minimum required Firefox version is 113.0.
+
+### Changed
+
+- The Mozilla specific capability `moz:useNonSpecCompliantPointerOrigin` has been
+ marked as deprecated. Its removal is planned for the Firefox 116.0 release.
+
+## 0.32.2 (2023-02-08, `602aa16c20d4`)
+
+### Known problems
+
+- _Startup hang with Firefox running in a container (e.g. snap, flatpak):_
+
+ When Firefox is packaged inside a container (like the default Firefox browser
+ shipped with Ubuntu 22.04), it may see a different filesystem to the host.
+ This can affect access to the generated profile directory, which may result
+ in a hang when starting Firefox. Workarounds are listed in the geckodriver
+ [usage documentation].
+
+- _Potential hang with `moz:debuggerAddress` capability set to `true`:_
+
+ After enabling the site-isolation feature in Firefox with geckodriver 0.32.1
+ some WebDriver clients like Selenium that use the Chrome DevTools Protocol (CDP)
+ by default for logging events could trigger a hang in Firefox's experimental CDP
+ implementation. The fix for this problem will be shipped with Firefox 112.
+ Until then the following Firefox preferences should be set:
+
+ - `fission.bfcacheInParent: false`
+ - `fission.webContentIsolationStrategy: 0`
+
+### Fixed
+
+- With the release of geckodriver 0.32.1 the marionette crate was inappropriately
+ bumped to a semver incompatible version and caused `cargo install geckodriver`
+ to fail for older releases.
+
+## 0.32.1 (2023-02-02, `b7f075124503`)
+
+### Known problems
+
+- _Startup hang with Firefox running in a container (e.g. snap, flatpak):_
+
+ When Firefox is packaged inside a container (like the default Firefox browser
+ shipped with Ubuntu 22.04), it may see a different filesystem to the host.
+ This can affect access to the generated profile directory, which may result
+ in a hang when starting Firefox. Workarounds are listed in the geckodriver
+ [usage documentation].
+
+- _Potential hang with `moz:debuggerAddress` capability set to `true`:_
+
+ After enabling the site-isolation feature in Firefox with geckodriver 0.32.1
+ some WebDriver clients like Selenium that use the Chrome DevTools Protocol (CDP)
+ by default for logging events could trigger a hang in Firefox's experimental CDP
+ implementation. The fix for this problem will be shipped with Firefox 112.
+ Until then the following Firefox preferences should be set:
+
+ - `fission.bfcacheInParent: false`
+ - `fission.webContentIsolationStrategy: 0`
+
+### Fixed
+
+- When using the boolean capability `moz:debuggerAddress` with a value of `true`
+ the site-isolation feature in Firefox will no longer accidentally be turned off.
+ This behavior affected all users of WebDriver clients especially Selenium, which
+ set this capability by default, and caused Firefox on desktop systems to be
+ launched in an unsupported mode.
+
+## 0.32.0 (2022-10-13, `4563dd583110`)
+
+### Added
+
+- Native aarch64 builds of geckodriver for Linux and Windows are now available.
+
+- Support `wheel` input source for [Actions], which is associated with a
+ wheel-type input device. This endpoint is supported by geckodriver when
+ using Firefox version ≥106.
+
+- Support `touch` as `pointerType` for `pointer` input source for [Actions],
+ which is associated with a touch input device. This also includes the
+ addition of all the remaining properties for `pointer` input sources as
+ specified by WebDriver. This endpoint is supported by geckodriver when using
+ Firefox version ≥104.
+
+### Fixed
+
+- Using geckodriver to launch Firefox inside a sandbox -- for example
+ a Firefox distribution using Snap or Flatpak -- can fail with a
+ "Profile not found" error if the sandbox restricts Firefox's ability
+ to access the system temporary directory. geckodriver uses the
+ temporary directory to store Firefox profiles created during the run.
+
+ This issue can now be worked around by using the `--profile-root`
+ command line option or setting the `TMPDIR` environment variable to
+ a location that both Firefox and geckodriver have read/write access
+ to e.g.:
+
+ ```bash
+ % mkdir $HOME/tmp
+ % geckodriver --profile-root=~/tmp
+ ```
+
+ or
+
+ ```bash
+ % TMPDIR=$HOME/tmp geckodriver
+ ```
+
+ Alternatively, geckodriver may be used with a Firefox install that
+ is not packaged inside a sandbox e.g. from [mozilla.org].
+
+- The sandboxed Firefox binary is now automatically detected when geckodriver
+ is used from within a Snap confinement.
+
+ Implemented by [Olivier Tilloy].
+
+- On MacOS the geckodriver binary is now technically both signed and notarized.
+
+ Note: The actual validation can only be performed if the machine that starts
+ the geckodriver binary for the very first time is online. You can find more
+ details on how to work around this issue in the [macOS notarization] section
+ of the documentation.
+
+- The backup of the original Firefox preferences are now correctly restored
+ on Android when the WebDriver session ends.
+
+### Changed
+
+- Update dependencies
+
+## 0.31.0 (2022-04-11, `b617178ef491`)
+
+### Known problems
+
+- _Firefox running in Linux Sandbox (e.g. Snap package):_
+
+ Using geckodriver to launch Firefox inside a sandbox -- for example
+ a Firefox distribution using Snap or Flatpak -- can fail with a
+ "Profile not found" error if the sandbox restricts Firefox's ability
+ to access the system temporary directory. geckodriver uses the
+ temporary directory to store Firefox profiles created during the run.
+
+ As workaround geckodriver may be used with a Firefox install that
+ is not packaged inside a sandbox e.g. from [mozilla.org].
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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
+
+- Users with the [Rust] toolchain installed can now build and install
+ geckodriver from [crates.io] using Cargo:
+
+ % cargo install geckodriver
+
+- Support for [Get Element Shadow Root]
+
+ Implemented by [David Burns].
+
+ The standardised WebDriver [Get Element Shadow Root] endpoint provides a way
+ to retrieve the Shadow Root of a given web element. This endpoint is
+ supported by geckodriver when using Firefox version ≥96.
+
+- Support for additional hosts and origins
+
+ Users can now specify a list of allowed `Host` and `Origin` headers for
+ incoming requests using the [`--allow-hosts`] and [`--allow-origins`] command
+ line options, respectively. When such a flag is provided, exactly the given
+ values will be permitted.
+
+ By default any request with an `Origin` header is rejected, and only requests
+ containing the bound hostname (specified via `--host`), or an IP address,
+ in the Host header are allowed. These configuration options are
+ designed to support scenarios where geckodriver is running on a different
+ network node to the host e.g. some container based setups.
+
+### Fixed
+
+- Geckodriver lets Marionette itself select a system allocated port, so that
+ it's no longer required to specify a fixed port when using a custom Firefox
+ profile. This is done by reading the `MarionetteActivePort` file of the
+ Firefox profile in-use. This helps to avoid port collisions when multiple
+ Firefox instances are run in parallel.
+
+- It's no longer possible to specify both the `androidPackage` and `binary`
+ capabilities togther within [`moz:firefoxOptions`] because these capabilites
+ are mutually exclusive.
+
+## 0.30.0 (2021-09-16, `d372710b98a6`)
+
+### Security Fixes
+
+- CVE-2021-4138
+
+ Fixed a DNS rebinding issues by enforcing a stricter `Host` header check.
+
+ Reported by Gabriel Corona.
+
+ - Improved `Host` header checks to reject requests not sent to a well-known
+ local hostname or IP, or the server-specified hostname.
+
+### Known problems
+
+- geckodriver restricts connections to local IP addresses. This can interfere
+ with deployments in which geckodriver is running on a different network node
+ to the tests e.g. some container or virtual-machine based setups.
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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.
+
+- _Android:_
+
+ For releases of Firefox 89.0 and earlier Marionette will only be enabled in
+ GeckoView based applications when the Firefox preference
+ `devtools.debugger.remote-enabled` is set to `true` via [`moz:firefoxOptions`].
+
+### Added
+
+- Support for WebDriver clients to opt in to WebDriver BiDi.
+
+ Introduced the new boolean capability [`webSocketUrl`] that can be used by
+ WebDriver clients to opt in to a bidirectional connection. A string capability
+ with the same name will be returned by [`NewSession`], which contains the
+ WebSocket URL of the newly created WebDriver session in the form of:
+ `ws://host:port/session/<session id>`.
+
+ When running on Android a port forward will be set on the host machine,
+ which is using the exact same port as on the device.
+
+ All the supported WebDriver BiDi commands depend on the version of
+ Firefox, and not geckodriver. The first commands will be shipped in
+ Firefox 94.
+
+- It's now possible to set additional preferences when a custom profile has been
+ specified. At the end of the session they will be removed.
+
+### Fixed
+
+- Added validation that the `--host` argument resolves to a local IP address.
+
+- Limit the `--foreground` argument of Firefox to MacOS only.
+
+- Increased Marionette handshake timeout to not fail for slow connections.
+
+- `Marionette:Quit` is no longer sent twice during session deletion.
+
+- When deleting a session that was attached to an already running browser
+ instance, the browser is not getting closed anymore.
+
+- Android
+
+ - Starting Firefox on Android from a Windows based host will now succeed as
+ we are using the correct Unix path separator to construct on-device paths.
+
+ - Arguments as specified in [`moz:firefoxOptions`] are now used when starting
+ Firefox.
+
+ - Port forwards set for Marionette and the WebSocket server (WebDriver BiDi)
+ are now correctly removed when geckodriver exits.
+
+ - The test root folder is now removed when geckodriver exists.
+
+## 0.29.1 (2021-04-09, `970ef713fe58`)
+
+### Known problems
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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.
+
+- _Android:_
+
+ Marionette will only be enabled in GeckoView based applications when the
+ Firefox preference `devtools.debugger.remote-enabled` is set to `true` via
+ [`moz:firefoxOptions`]. This will be fixed in the Firefox 90 release for
+ Android.
+
+### Added
+
+- When testing GeckoView based applications on Android it's now enough to
+ specify the `androidPackage` capability. The appropriate activity name,
+ and required intent arguments will now automatically be used for
+ applications released by Mozilla.
+
+- Native AArch64 (M1) builds of geckodriver for MacOS are now available. These
+ are currently shipped as Tier2 due to missing test infrastructure. Please let
+ us know if you experience issues.
+
+### Fixed
+
+- Fixed a stack overflow crash in thread 'webdriver dispatcher' when
+ handling certain device errors.
+
+- Fixed an application crash due to missing permissions on unrooted devices
+ by changing the location of the test related files, e.g the profile folder.
+ Therefore the deprecated &#x2D;&#x2D;android-storage command line argument
+ now defaults to the `sdcard` option, which changed its location to
+ `$EXTERNAL_STORAGE/Android/data/%androidPackage%/files/`. With this change
+ proper support for unrooted devices running Android 10+ has been added.
+
+ _Note_: Do not use the &#x2D;&#x2D;android-storage command line argument
+ anymore unless there is a strong reason. It will be removed in a future
+ release.
+
+## 0.29.0 (2021-01-14, `cf6956a5ec8e`)
+
+### Known problems
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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.
+
+- _Android:_
+
+ Marionette will only be enabled in GeckoView based applications when the
+ Firefox preference `devtools.debugger.remote-enabled` is set to `true` via
+ [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox
+ for Android releases.
+
+ In some cases geckodriver could crash due to a stack overflow when handling
+ certain device errors.
+
+ On unrooted Android 10+ devices startup crashes of the application can be
+ experienced due to an inappropriate location of test related files, e.g the
+ profile folder.
+
+### Added
+
+- Introduced the new boolean capability [`moz:debuggerAddress`] that can be used
+ to opt-in to the experimental Chrome DevTools Protocol (CDP) implementation.
+ A string capability with the same name will be returned by [`NewSession`],
+ which contains the `host:port` combination of the HTTP server that can be
+ used to query for websockets of available targets.
+
+ Note: For this experimental feature the site-isolation support of
+ Firefox aka [Fission] will be not available.
+
+## 0.28.0 (2020-11-03, `c00d2b6acd3f`)
+
+### Known problems
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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.
+
+- _Android:_
+
+ Marionette will only be enabled in GeckoView based applications when the
+ Firefox preference `devtools.debugger.remote-enabled` is set to `true` via
+ [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox
+ for Android releases.
+
+ In some cases geckodriver could crash due to a stack overflow when handling
+ certain device errors.
+
+ On unrooted Android 10+ devices startup crashes of the application can be
+ experienced due to an inappropriate location of test related files, e.g the
+ profile folder.
+
+### Added
+
+- The command line flag `--android-storage` has been added, to allow geckodriver
+ to also control Firefox on root-less Android devices.
+ See the [documentation][Flags] for available values.
+
+### Fixed
+
+- Firefox can be started again via a shell script that is located outside of the
+ Firefox directory on Linux.
+
+- If Firefox cannot be started by geckodriver the real underlying error message is
+ now being reported.
+
+- Version numbers for minor and extended support releases of Firefox are now parsed correctly.
+
+### Removed
+
+- Since Firefox 72 extension commands for finding an element’s anonymous children
+ and querying its attributes are no longer needed, and have been removed.
+
+## 0.27.0 (2020-07-27, `7b8c4f32cdde`)
+
+### Security Fixes
+
+- CVE-2020-15660
+
+ Improved validation of incoming requests to prevent remote
+ requests being treated as local.
+
+ Reported by Gabriel Corona.
+
+ - Added additional checks on the `Content-Type` header for `POST`
+ requests to disallow `application/x-www-form-urlencoded`,
+ `multipart/form-data` and `text/plain`.
+
+ - Added checking of the `Origin` header for `POST` requests.
+
+ - The version number of Firefox is now checked when establishing a session.
+
+### Known problems
+
+- _macOS 10.15 (Catalina) and later:_
+
+ 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.
+
+- _Android:_
+
+ Marionette will only be enabled in GeckoView based applications when the
+ Firefox preference `devtools.debugger.remote-enabled` is set to `true` via
+ [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox
+ for Android releases.
+
+ In some cases geckodriver could crash due to a stack overflow when handling
+ certain device errors.
+
+### 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) and later:_
+
+ 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.
+
+- _Android:_
+
+ Marionette will only be enabled in GeckoView based applications when the
+ Firefox preference `devtools.debugger.remote-enabled` is set to `true` via
+ [`moz:firefoxOptions`]. This will be fixed in one of the upcoming Firefox
+ for Android releases.
+
+ In some cases geckodriver could crash due to a stack overflow when handling
+ certain device errors.
+
+### 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 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
+ upload controls.
+
+- 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
+ 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. 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
+ `error()` can be routed to stdout.
+
+- geckodriver now sets the `app.update.disabledForTesting` preference
+ to prevent Firefox >= 65 from automatically updating whilst under
+ automation.
+
+### Removed
+
+- ARMv7 HF builds have been discontinued
+
+ We announced 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
+
+- Allow file uploads to hidden `<input type=file>` elements
+
+ Through a series of changes to the WebDriver specification,
+ geckodriver is now aligned with chromedriver’s behaviour that
+ allows interaction with hidden `<input type=file>` elements.
+
+ This allows WebDriver to be used with various popular web
+ frameworks that—through indirection—hides the file upload control
+ and invokes it through other means.
+
+- Allow use of an indefinite script timeout for the [Set Timeouts]
+ command, thanks to reimu.
+
+### Fixed
+
+- Corrected `Content-Type` of response header to `utf-8` to fix
+ an HTTP/1.1 compatibility bug.
+
+- Relaxed the deserialization of timeouts parameters to allow unknown
+ fields for the [Set Timeouts] command.
+
+- Fixed a regression in the [Take Element Screenshot] to not screenshot
+ the viewport, but the requested element.
+
+## 0.23.0 (2018-10-03)
+
+This release contains a number of fixes for regressions introduced
+in 0.22.0, where we shipped a significant refactoring to the way
+geckodriver internally dealt with JSON serialisation.
+
+### Removed
+
+- The POST `/session/{session id}/element/{element id}/tap` endpoint
+ was removed, thanks to Kerem Kat.
+
+### Changed
+
+- [webdriver crate] upgraded to 0.38.0.
+
+### Fixed
+
+- `desiredCapabilities` and `requiredCapabilities` are again
+ recognised on session creation
+
+ A regression in 0.22.0 caused geckodriver to recognise `desired`
+ and `required` instead of the correct `desiredCapabilities`
+ and `requiredCapabilities`. This will have caused significant
+ problems for users who relied on this legacy Selenium-style
+ session creation pattern.
+
+ Do however note that support for Selenium-styled new session
+ requests is temporary and that this will be removed sometime
+ before the 1.0 release.
+
+- `duration` field made optional on pause actions
+
+ A regression in 0.22.0 caused the pause action primitive to
+ require a `duration` field. This has now been fixed so that
+ pauses in action chains can be achieved with the default duration.
+
+- Log level formatted to expected Marionette input
+
+ A regression in 0.22.0 caused the log level to be improperly
+ formatted when using Firefox pre-releases. This is now fixed so
+ that the requested log level is correctly interpreted by Marionette.
+
+- `temporary` field on addon installation made optional
+
+ A regression in 0.22.0 caused the `temporary` field for POST
+ `/session/{session id}/moz/addon/install` to be mandatory. This has
+ now been fixed so that an addon is installed permanently by default.
+
+- SHA1s in version information uses limited number of characters
+
+ The SHA1 used in `--version` when building geckodriver from a
+ git repository is now limited to 12 characters, as it is when
+ building from an hg checkout. This ensures reproducible builds.
+
+## 0.22.0 (2018-09-15)
+
+This release marks an important milestone on the path towards
+a stable release of geckodriver. Large portions of geckodriver
+and the [webdriver] library it is based on has been refactored to
+accommodate using [serde] for JSON serialization.
+
+We have also made great strides to improving [WebDriver conformance],
+to the extent that geckodriver is now _almost_ entirely conforming
+to the standard.
+
+### Added
+
+- Support for WebDriver web element-, web frame-, and web window
+ identifiers from Firefox.
+
+- Added support for the non-configurable `setWindowRect` capability
+ from WebDriver.
+
+ This capability informs whether the attached browser supports
+ manipulating the window dimensions and position.
+
+- A new extension capability `moz:geckodriverVersion` is returned
+ upon session creation.
+
+### Changed
+
+- All JSON serialization and deserialisation has moved from
+ rustc_serialize to [serde].
+
+- The HTTP status codes used for [script timeout] and [timeout]
+ errors has changed from Request Timeout (408) to Internal Server
+ Error (500) in order to not break HTTP/1.1 `Keep-Alive` support,
+ as HTTP clients interpret the old status code to mean they should
+ duplicate the request.
+
+- The HTTP/1.1 `Keep-Alive` timeout for persistent connections has
+ been increased to 90 seconds.
+
+- An [invalid session ID] error is now returned when there is no
+ active session.
+
+- An [invalid argument] error is now returned when [Add Cookie]
+ is given invalid parameters.
+
+- The handshake when geckodriver connects to Marionette has been
+ hardened by killing the Firefox process if it fails.
+
+- The handshake read timeout has been reduced to 10 seconds instead
+ of waiting forever.
+
+- The HTTP server geckodriver uses, [hyper], has been upgraded to
+ version 0.12, thanks to [Bastien Orivel].
+
+- geckodriver version number is no longer logged on startup, as
+ the log level is not configured until a session is created.
+
+ The version number is available through `--version`, and now
+ also through a new `moz:geckodriverVersion` field in the matched
+ capabilities.
+
+- [webdriver crate] upgraded to 0.37.0.
+
+### Fixed
+
+- Parsing [timeout object] values has been made WebDriver conforming,
+ by allowing floats as input.
+
+- Implicit downloads of OpenH264 and Widevine plugins has been disabled.
+
+- The commit hash and date displayed when invoking `--version`
+ is now well-formatted when built from an hg repository, thanks to
+ [Jeremy Lempereur].
+
+- Many documentation improvements, now published on
+ <https://firefox-source-docs.mozilla.org/testing/geckodriver/>.
+
+## 0.21.0 (2018-06-15)
+
+Note that with this release of geckodriver the minimum recommended
+Firefox and Selenium versions have changed:
+
+- Firefox 57 (and greater)
+- Selenium 3.11 (and greater)
+
+### Added
+
+- Support for the chrome element identifier from Firefox.
+
+- The `unhandledPromptBehavior` capability now accepts `accept and
+ notify`, `dismiss and notify`, and `ignore` options.
+
+ Note that the unhandled prompt handler is not fully supported in
+ Firefox at the time of writing.
+
+### Changed
+
+- Firefox will now be started with the `-foreground` and `-no-remote`
+ flags if they have not already been specified by the user in
+ [`moz:firefoxOptions`].
+
+ `-foreground` will ensure the application window gets focus when
+ Firefox is started, and `-no-remote` will prevent remote commands
+ to this instance of Firefox and also ensure we always start a new
+ instance.
+
+- WebDriver commands that do not have a return value now correctly
+ return `{value: null}` instead of an empty dictionary.
+
+- The HTTP server now accepts `Keep-Alive` connections.
+
+- Firefox remote protocol command mappings updated.
+
+ All Marionette commands changed to make use of the `WebDriver:`
+ prefixes introduced with Firefox 56.
+
+- Overhaul of Firefox preferences.
+
+ Already deprecated preferences in Firefox versions earlier than
+ 57 got removed.
+
+- [webdriver crate] upgraded to 0.36.0.
+
+### Fixed
+
+- Force use of IPv4 network stack.
+
+ On certain system configurations, where `localhost` resolves to
+ an IPv6 address, geckodriver would attempt to connect to Firefox
+ on the wrong IP stack, causing the connection attempt to time out
+ after 60 seconds. We now ensure that geckodriver uses IPv4
+ consistently to both connect to Firefox and for allocating a free
+ port.
+
+- geckodriver failed to locate the correct Firefox binary if it was
+ found under a _firefox_ or _firefox-bin_ directory, depending on
+ the system, because it thought the parent directory was the
+ executable.
+
+- On Unix systems (macOS, Linux), geckodriver falsely reported
+ non-executable files as valid binaries.
+
+- When stdout and stderr is redirected by geckodriver, a bug prevented
+ the redirections from taking effect.
+
+## 0.20.1 (2018-04-06)
+
+### Fixed
+
+- Avoid attempting to kill Firefox process that has stopped.
+
+ With the change to allow Firefox enough time to shut down in
+ 0.20.0, geckodriver started unconditionally killing the process
+ to reap its exit status. This caused geckodriver to inaccurately
+ report a successful Firefox shutdown as a failure.
+
+ The regression should not have caused any functional problems, but
+ the termination cause and the exit status are now reported correctly.
+
+## 0.20.0 (2018-03-08)
+
+### Added
+
+- New `--jsdebugger` flag to open the [Browser Toolbox] when Firefox
+ launches. This is useful for debugging Marionette internals.
+
+- Introduced the temporary, boolean capability
+ `moz:useNonSpecCompliantPointerOrigin` to disable the WebDriver
+ conforming behavior of calculating the Pointer Origin.
+
+### Changed
+
+- HTTP status code for the [`StaleElementReference`] error changed
+ from 400 (Bad Request) to 404 (Not Found).
+
+- Backtraces from geckodriver no longer substitute for missing
+ Marionette stacktraces.
+
+- [webdriver crate] upgraded to 0.35.0.
+
+### Fixed
+
+- The Firefox process is now given ample time to shut down, allowing
+ enough time for the Firefox shutdown hang monitor to kick in.
+
+ Firefox has an integrated background monitor that observes
+ long-running threads during shutdown. These threads will be
+ killed after 63 seconds in the event of a hang. To allow Firefox
+ to shut down these threads on its own, geckodriver has to wait
+ that time and some additional seconds.
+
+- Grapheme clusters are now accepted as input for keyboard input
+ to actions.
+
+ Input to the `value` field of the `keyDown` and `keyUp` action
+ primitives used to only accept single characters, which means
+ geckodriver would error when a valid grapheme cluster was sent in,
+ for example with the tamil nadu character U+0BA8 U+0BBF.
+
+ Thanks to Greg Fraley for fixing this bug.
+
+- Improved error messages for malformed capability values.
+
+## 0.19.1 (2017-10-30)
+
+### Changed
+
+- Search suggestions in the location bar turned off as not to
+ trigger network connections
+
+- Block addons incompatible with E10s
+
+### Fixed
+
+- Marionette stacktraces are now correctly propagated
+
+- Some error messages have been clarified
+
+### Removed
+
+- Removed obsolete `socksUsername` and `socksPassword` proxy
+ configuration keys because neither were picked up or recognised
+
+## 0.19.0 (2017-09-16)
+
+Note that with geckodriver 0.19.0 the following versions are recommended:
+
+- Firefox 55.0 (and greater)
+- Selenium 3.5 (and greater)
+
+### Added
+
+- Added endpoint:
+ - POST `/session/{session id}/window/minimize` for the [Minimize Window]
+ command
+
+- Added preference `extensions.shield-recipe-client.api_url` to disable
+ shield studies which could unexpectedly change the behavior of Firefox
+
+- Introduced the temporary, boolean capability `moz:webdriverClick` to
+ enable the WebDriver conforming behavior of the [Element Click] command
+
+- Added crashreporter environment variables to better control the browser
+ in case of crashes
+
+- Added preference `dom.file.createInChild` set to true to allow file
+ object creation in content processes
+
+### Changed
+
+- Log all used application arguments and not only `-marionette`
+
+- Early abort connection attempts to Marionette if the Firefox process
+ closed unexpectetly
+
+- Removed deprecated `socksProxyVersion` in favor of `socksVersion`
+
+- Removed `ftpProxyPort`, `httpProxyPort`, `sslProxyPort`, and
+ `socksProxyPort` because _ports_ have to be set for `ftpProxy`,
+ `httpProxy`, `sslProxy`, and `socksProxy` using ":<PORT>"
+
+- The `proxyType` `noproxy` has been replaced with `direct` in accordance
+ with recent WebDriver specification changes
+
+- The [`WindowRectParameters`] have been updated to return signed 32-bit
+ integers in accordance with the CSS and WebDriver specifications, and
+ to be more liberal with the input types
+
+- Mapped the [`FullscreenWindow`] to the correct Marionette command
+
+- To make sure no browser process is left behind when the [`NewSession`]
+ fails, the process is closed immediately now
+
+- `/moz/addon/install` command accepts an `addon` parameter, in lieu of
+ `path`, containing an addon as a Base64 string (fixed by [Jason Juang])
+
+- [webdriver crate] upgraded to version 0.31.0
+
+- [mozrunner crate] upgraded to version 0.5.0
+
+### Removed
+
+- Removed the following obsolete preferences for Firefox:
+ - `browser.safebrowsing.enabled`
+ - `browser.safebrowsing.forbiddenURIs.enabled`
+ - `marionette.defaultPrefs.port`
+ - `marionette.logging`
+
+## 0.18.0 (2017-07-10)
+
+### Changed
+
+- [`RectResponse`] permits returning floats for `width` and `height`
+ fields
+
+- New type [`CookieResponse`] for the [`GetNamedCookie`] command returns
+ a single cookie, as opposed to an array of a single cookie
+
+- To pick up a prepared profile from the filesystem, it is now possible
+ to pass `["-profile", "/path/to/profile"]` in the `args` array on
+ [`moz:firefoxOptions`]
+
+- geckodriver now recommends Firefox 53 and greater
+
+- Version information (`--version`) contains the hash from from the
+ commit used to build geckodriver
+
+- geckodriver version logged on startup
+
+- [webdriver crate] upgraded to version 0.27.0
+
+- [mozrunner crate] upgraded to version 0.4.1
+
+### Fixed
+
+- The [`SetTimeouts`] command maps to the Marionette `setTimeouts`
+ command, which makes geckodriver compatible with Firefox 56 and greater
+
+- Linux x86 (i686-unknown-linux-musl) builds are fixed
+
+## 0.17.0 (2017-06-09)
+
+### Added
+
+- Added endpoints:
+ - POST `/session/{session id}/window/fullscreen` to invoke the window
+ manager-specific `full screen` operation
+ - POST `/session/{session id}/moz/addon/install` to install an extension
+ (Gecko only)
+ - POST `/session/{session id}/moz/addon/uninstall` to uninstall an
+ extension (Gecko only)
+
+### Changed
+
+- Increasing the length of the `network.http.phishy-userpass-length`
+ preference will cause Firefox to not prompt when navigating to a
+ website with a username or password in the URL
+
+- Library dependencies upgraded to mozrunner 0.4 and mozprofile 0.3
+ to allow overriding of preferences via capabilities if those have been
+ already set in the profile
+
+- Library dependencies upgraded to mozversion 0.1.2 to only use the
+ normalized path of the Firefox binary for version checks but not to
+ actually start the browser, which broke several components in Firefox
+ on Windows
+
+### Fixed
+
+- The [SetWindowRect] command now returns the [WindowRectResponse]
+ when it is done
+
+- Use ASCII versions of array symbols to properly display them in the
+ Windows command prompt
+
+- Use [`SessionNotCreated`] error instead of [`UnknownError`] if there
+ is no current session
+
+## 0.16.1 (2017-04-26)
+
+### Fixed
+
+- Read Firefox version number from stdout when failing
+ to look for the application .ini file (fixes [Selenium
+ #3884](https://github.com/SeleniumHQ/selenium/issues/3884))
+
+- Session is now ended when closing the last Firefox window (fixes
+ [#613](https://github.com/mozilla/geckodriver/issues/613))
+
+## 0.16.0 (2017-04-21)
+
+Note that geckodriver v0.16.0 is only compatible with Selenium 3.4
+and greater.
+
+### Added
+
+- Support for WebDriver-conforming [New Session] negotiation, with
+ `desiredCapabilities`/`requiredCapabilities` negotiation as fallback
+
+- Added two new endpoints:
+ - GET `/session/{session id}/window/rect` for [Get Window Rect]
+ - POST `/session/{session id}/window/rect` for [Set Window Rect]
+
+- Align errors with the [WebDriver errors]:
+ - Introduces new errors [`ElementClickIntercepted`],
+ [`ElementNotInteractable`], [`InvalidCoordinates`], [`NoSuchCookie`],
+ [`UnableToCaptureScreen`], and [`UnknownCommand`]
+ - Removes `ElementNotVisible` and `InvalidElementCoordinates` errors
+
+### Removed
+
+- Removed following list of unused endpoints:
+ - GET `/session/{session id}/alert_text`
+ - POST `/session/{session id}/alert_text`
+ - POST `/session/{session id}/accept_alert`
+ - POST `/session/{session id}/dismiss_alert`
+ - GET `/session/{session id}/window_handle`
+ - DELETE `/session/{session id}/window_handle`
+ - POST `/session/{session id}/execute_async`
+ - POST `/session/{session id}/execute`
+
+### Changed
+
+- [`SendKeysParameters`], which is used for the [Element Send Keys] and
+ [Send Alert Text] commands, has been updated to take a string `text`
+ field
+
+- [`CookieResponse`] and [`CloseWindowResponse`] fixed to be properly
+ wrapped in a `value` field, like other responses
+
+- Allow negative numbers for `x` and `y` fields in `pointerMove` action
+
+- Disable Flash and the plugin container in Firefox by
+ default, which should help mitigate the “Plugin Container
+ for Firefox has stopped working” problems [many users were
+ reporting](https://github.com/mozilla/geckodriver/issues/225) when
+ deleting a session
+
+- Preferences passed in a profile now take precedence over
+ set of default preferences defined by geckodriver (fixed by
+ [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 overridden
+
+- Remove default preference that disables unsafe CPOW checks
+
+- WebDriver library updated to 0.25.2
+
+### Fixed
+
+- Fix for the “corrupt deflate stream” exception that
+ sometimes occurred when trying to write an empty profile by
+ [@kirhgoph](https://github.com/kirhgoph)
+
+- Recognise `sslProxy` and `sslProxyPort` entries in the proxy
+ configuration object (fixed by [Jason Juang])
+
+- Fix “`httpProxyPort` was not an integer” error (fixed by [Jason
+ Juang])
+
+- Fix broken unmarshaling of _Get Timeouts_ response format from Firefox
+ 52 and earlier (fixed by [Jason Juang])
+
+- Allow preferences in [`moz:firefoxOptions`] to be both positive- and
+ negative integers (fixed by [Jason Juang])
+
+- Allow IPv6 hostnames in the proxy configuration object
+
+- i686-unknown-linux-musl (Linux 32-bit) build fixed
+
+- Log messages from other Rust modules are now ignored
+
+- Improved log messages to the HTTPD
+
+## 0.15.0 (2017-03-08)
+
+### Added
+
+- Added routing and parsing for the [Get Timeouts] command
+
+### Changed
+
+- All HTTP responses are now wrapped in `{value: …}` objects per the
+ WebDriver specification; this may likely require you to update your
+ client library
+
+- Pointer move action’s `element` key changed to `origin`, which
+ lets pointer actions originate within the context of the viewport,
+ the pointer’s current position, or from an element
+
+- Now uses about:blank as the new tab document; this was previously
+ disabled due to [bug 1333736](https://bugzil.la/1333736) in Marionette
+
+- WebDriver library updated to 0.23.0
+
+### Fixed
+
+- Aligned the data structure accepted by the [Set Timeouts] command with
+ the WebDriver specification
+
+## 0.14.0 (2017-01-31)
+
+### Changed
+
+- Firefox process is now terminated and session ended when the last
+ window is closed
+
+- WebDriver library updated to version 0.20.0
+
+### Fixed
+
+- Stacktraces are now included when the error originates from within
+ the Rust stack
+
+- HTTPD now returns correct response headers for `Content-Type` and
+ `Cache-Control` thanks to [Mike Pennisi]
+
+## 0.13.0 (2017-01-06)
+
+### Changed
+
+- When navigating to a document with an insecure- or otherwise invalid
+ TLS certificate, an [insecure certificate] error will be returned
+
+- On macOS, deducing Firefox’ location on the system will look for
+ _firefox-bin_ on the system path (`PATH` environmental variable) before
+ looking in the applications folder
+
+- Window position coordinates are allowed to be negative numbers, to
+ cater for maximised window positioning on Windows
+
+- WebDriver library updated to version 0.18.0
+
+### Fixed
+
+- Check for single-character key codes in action sequences now counts
+ characters instead of bytes
+
+## 0.12.0 (2017-01-03)
+
+### Added
+
+- Added [Take Element Screenshot] command
+
+- Added new [Status] command
+
+- Added routing for the [Get Timeouts] command, but it is not yet
+ implemented in Marionette, and will return an _unsupported operation_
+ error until it is
+
+- Implemented routing for [new actions API](Actions), but it too is not
+ yet fully implemented in Marionette
+
+### Changed
+
+- [Synced Firefox
+ preferences](https://github.com/mozilla/geckodriver/commit/2bfdc3ec8151c427a6a75a6ba3ad203459540495)
+ with those used in Mozilla automation
+
+- Default log level for debug builds of Firefox, which used to be `DEBUG`,
+ changed to `INFO`-level
+
+- WebDriver library dependency upgraded to 0.17.1
+
+- Using _session not created_ error when failing to start session
+
+- geckodriver will exit with exit code 69 to indicate that the port
+ is unavailable
+
+### Fixed
+
+- Improved logging when starting Firefox
+
+- Reverted to synchronous logging, which should address cases of
+ inconsistent output when failing to bind to port
+
+- Clarified in README that geckodriver is not supported on Windows XP
+
+- Added documentation of supported capabilities to [README]
+
+- Included capabilities example in the [README]
+
+## 0.11.1 (2016-10-10)
+
+### Fixed
+
+- Version number in binary now reflects the release version
+
+## 0.11.0 (2016-10-10)
+
+### Added
+
+- Introduced continuous integration builds for Linux- and Windows 32-bit
+ binaries
+
+- Added commands for setting- and getting the window position
+
+- Added new extension commands for finding an element’s anonymous
+ children and querying its attributes; accessible through the
+ `/session/{sessionId}/moz/xbl/{elementId}/anonymous_children`
+ to return all anonymous children and
+ `/session/{sessionId}/moz/xbl/{elementId}/anonymous_by_attribute` to
+ return an anonymous element by a name and attribute query
+
+- Introduced a [`moz:firefoxOptions`] capability to customise a Firefox
+ session:
+
+ - The `binary`, `args`, and `profile` entries on this dictionary
+ is equivalent to the old `firefox_binary`, `firefox_args`, and
+ `firefox_profile` capabilities, which have now all been removed
+
+ - The `log` capability takes a dictionary such as `{log: "trace"}`
+ to enable trace level verbosity in Gecko
+
+ - The `prefs` capability lets you define Firefox preferences through
+ capabilities
+
+- Re-introduced the `--webdriver-port` argument as a hidden alias to
+ `--port`
+
+### Changed
+
+- `firefox_binary`, `firefox_args`, and `firefox_profile` capabilities
+ removed in favour of the [`moz:firefoxOptions`] dictionary detailed above
+ and in the [README]
+
+- Removed `--no-e10s` flag, and geckodriver will from now rely on the
+ Firefox default multiprocessing settings (override using preferences)
+
+- Disable pop-up blocker in the default profile by @juangj
+
+- Changed Rust compiler version to 1.12 (beta)
+ temporarily because of [trouble linking Musl
+ binaries](https://github.com/rust-lang/rust/issues/34978)
+
+- Replaced _env_logger_ logging facility with the _slog_ package,
+ causing the `RUST_LOG` environment variable to no longer have any affect
+
+- Updated the WebDriver Rust library to version 0.15
+
+### Fixed
+
+- Corrected link to repository in Cargo metadata
+
+- Verbosity shorthand flag `-v[v]` now works again, following the
+ replacement of the argument parsing library in the previous release
+
+- When the HTTPD fails to start, errors are propagated to the user
+
+- Disabled the additional welcome URL
+ (`startup.homepage_welcome_url.additional`) so that officially branded
+ Firefox builds do not start with two open tabs in fresh profiles
+
+- Disabled homepage override URL redirection on milestone upgrades,
+ which means a tab with an upgrade notice is not displayed when launching
+ a new Firefox version
+
+## 0.10.0 (2016-08-02)
+
+### Changed
+
+- Use multi-process Firefox (e10s) by default, added flag `--no-e10s`
+ to disable it and removed `--e10s` flag
+
+- Disable autofilling of forms by default by [Sven Jost]
+
+- Replace _argparse_ with _clap_ for arguments parsing
+
+### Fixed
+
+- Attempt to deploy a single file from Travis when making a release
+
+- Grammar fix in [README]
+
+## 0.9.0 (2016-06-30)
+
+### Added
+
+- Add ability to use `firefox_binary` capability to define location of
+ Firefox to use
+
+- Automatically detect the default Firefox path if one is not given
+
+- Cross-compile to Windows and ARMv7 (HF) in CI
+
+- Add Musl C library-backed static binaries in CI
+
+- Add `-v`, `-vv`, and `--log LEVEL` flags to increase Gecko verbosity
+
+- Add Get Element Property endpoint
+
+- Add new `--version` flag showing copying information and a link to
+ the repository
+
+### Changed
+
+- Now connects to a Marionette on a random port by default
+
+- Update webdriver-rust library dependency
+
+- Migrated to use Travis to deploy new releases
+
+- Reduced amount of logging
+
+- Introduced a changelog (this)
+
+## 0.8.0 (2016-06-07)
+
+### Added
+
+- Allow specifying array of arguments to the Firefox binary through the
+ `firefox_args` capability
+
+- Pass parameters with [New Session] command
+
+### Changed
+
+- Change product name to _geckodriver_
+
+- Make README more exhaustive
+
+- Quit Firefox when deleting a session
+
+- Update webdriver-rust library
+
+- Update dependencies
+
+### Fixed
+
+- Fix tests
+
+- FIx typo in error message for parsing errors
+
+## 0.7.1 (2016-04-27)
+
+### Added
+
+- Add command line flag for using e10s enabled Firefox by [Kalpesh
+ Krishna]
+
+- Allow providing custom profiles
+
+### Changed
+
+- Allow binding to an IPv6 address by [Jason Juang]
+
+- By default, connect to host-agnostic localhost by [Jason Juang]
+
+- Make `GeckoContextParameters` public
+
+- Update dependencies
+
+### Fixed
+
+- Squash rustc 1.6 warnings by using `std::thread::sleep(dur: Duration)`
+
+## 0.6.2 (2016-01-20)
+
+### Added
+
+- Add LICENSE file from [Joshua Burning]
+
+- Schedule builds in CI on pushes and pull requests
+
+### Changed
+
+- Enable CPOWs in Marionette
+
+## 0.6.0 (2016-01-12)
+
+### Added
+
+- Add Get Page Source endpoint
+
+### Changed
+
+- Handle arrays being sent from Marionette
+
+- Correct build steps in [README]
+
+- Update what properties are read from errors sent by Marionette
+
+- Update dependencies
+
+## 0.5.0 (2015-12-10)
+
+### Changed
+
+- Update argparse dependency to use Cargo
+
+- Update to the latest version of the Marionette wire protocol
+
+- Update to latest webdriver-rust library
+
+- Update dependencies
+
+## 0.4.2 (2015-10-02)
+
+### Changed
+
+- Skip compiling optional items in hyper
+
+## 0.4.1 (2015-10-02)
+
+### Changed
+
+- Update webdriver-rust library
+
+- Update dependencies
+
+## 0.4.0 (2015-09-28)
+
+### Added
+
+- Add command extensions for switching between content- and chrome
+ contexts
+
+- Add more documentation from [Vlad Filippov]
+
+### Changed
+
+- Update Cargo.lock with new dependencies for building
+
+- Update for protocol updates that flatten commands
+
+- Update to new protocol error handling
+
+- Update for Marionette protocol version 3 changes
+
+- Strip any leading and trailing `{}` from the `sessionId` Marionette
+ returns
+
+- Update dependencies
+
+### Fixed
+
+- Fix `GetCSSValue` message to send correct key `propertyName`
+
+- Fix example in documentation from @vladikoff
+
+## 0.3.0 (2015-08-17)
+
+### Added
+
+- Add support for finding elements in subtrees
+
+## 0.2.0 (2015-05-20)
+
+### Added
+
+- Extra debug messages
+
+- Add ability to set WebDriver port
+
+- Add support for getting the active element
+
+- Add support for `GetCookies` and `DeleteCookie`/`DeleteCookies`
+
+- Add preferences that switch off certain features not required for
+ WebDriver tests
+
+### Changed
+
+- Make failing to communicate with Firefox a fatal error that closes
+ the session
+
+- Shut down session only when losing connection
+
+- Better handling of missing command line flags
+
+- Poll for connection every 100ms rather than every 100s
+
+- Switch to string-based error codes
+
+- Switch webdriver-rust library dependency to be pulled from git
+
+- Update dependencies
+
+### Fixed
+
+- Handle null id for switching to frame more correctly
+
+## 0.1.0 (2015-04-09)
+
+### Added
+
+- Add proxy for converting WebDriver HTTP protocol to Marionette protocol
+
+- Add endpoints for modal dialogue support
+
+- Allow connecting to a running Firefox instance
+
+- Add explicit Cargo.lock file
+
+- Start Firefox when we get a [NewSession] command
+
+- Add flag parsing and address parsing
+
+- Add basic error handling
+
+### Changed
+
+- Update for Rust beta
+
+- Switch to new IO libraries
+
+- Pin webdriver-rust commit so we can upgrade rustc versions independently
+
+- Set preferences when starting Firefox
+
+- Improve some error messages
+
+- Re-enable environment variable based logging
+
+### Fixed
+
+- Fix Get Element Rect command to return floats instead of integers
+
+- Fix passing of web elements to Switch To Frame command
+
+- Fix serialisation of script commands
+
+- Fix assorted bugs found by the Selenium test suite
+
+- Fix conversion of Find Element/Find Elements responses from Marionette
+ to WebDriver
+
+- Fixed build by updating Cargo.lock with new dependencies for building
+
+- Squash compile warnings
+
+[README]: https://github.com/mozilla/geckodriver/blob/master/README.md
+[usage documentation]: <https://firefox-source-docs.mozilla.org/testing/geckodriver/Usage.html#Running-Firefox-in-an-container-based-package>
+[Browser Toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox
+[WebDriver conformance]: https://wpt.fyi/results/webdriver/tests?label=experimental
+[`webSocketUrl`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/webSocketUrl
+[`moz:firefoxOptions`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions
+[`moz:debuggerAddress`]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-debuggeraddress
+[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
+[Fission]: https://wiki.mozilla.org/Project_Fission
+[Capabilities]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
+[Flags]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html
+[`--allow-hosts`]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html#code-allow-hosts-var-allow-hosts-var-code
+[`--allow-origins`]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html#code-allow-origins-var-allow-origins-var-code
+[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
+[Rust]: https://rustup.rs/
+[mozilla.org] https://www.mozilla.org/firefox/
+
+[`CloseWindowResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CloseWindowResponse.html
+[`CookieResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CookieResponse.html
+[`DeleteSession`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.DeleteSession
+[`ElementClickIntercepted`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.ElementClickIntercepted
+[`ElementNotInteractable`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.ElementNotInteractable
+[`FullscreenWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.FullscreenWindow
+[`GetNamedCookie`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.GetNamedCookie
+[`GetWindowRect`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.GetWindowRect
+[`InvalidCoordinates`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.InvalidCoordinates
+[`MaximizeWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.MaximizeWindow
+[`MinimizeWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.MinimizeWindow
+[`NewSession`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.NewSession
+[`NoSuchCookie`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.NoSuchCookie
+[`RectResponse`]: https://docs.rs/webdriver/0.27.0/webdriver/response/struct.RectResponse.html
+[`SendKeysParameters`]: https://docs.rs/webdriver/newest/webdriver/command/struct.SendKeysParameters.html
+[`SessionNotCreated`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.SessionNotCreated
+[`SetTimeouts`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.SetTimeouts
+[`SetWindowRect`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.SetWindowRect
+[`StaleElementReference`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.StaleElementReference
+[`UnableToCaptureScreen`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnableToCaptureScreen
+[`UnknownCommand`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnknownCommand
+[`UnknownError`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnknownError
+[`WindowRectParameters`]: https://docs.rs/webdriver/newest/webdriver/command/struct.WindowRectParameters.html
+
+[Add Cookie]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/AddCookie
+[invalid argument]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidArgument
+[invalid session id]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidSessionID
+[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
+[serde]: https://serde.rs/
+[webdriver crate]: https://crates.io/crates/webdriver
+
+[Actions]: https://w3c.github.io/webdriver/webdriver-spec.html#actions
+[Element Click]: https://w3c.github.io/webdriver/webdriver-spec.html#element-click
+[Find Element From Shadow Root]: https://w3c.github.io/webdriver/#dfn-find-element-from-shadow-root
+[Find Elements From Shadow Root]: https://w3c.github.io/webdriver/#dfn-find-elements-from-shadow-root
+[Get Computed Label]: https://w3c.github.io/webdriver/#get-computed-label
+[Get Computed Role]: https://w3c.github.io/webdriver/#get-computed-role
+[Get Element Shadow Root]: https://w3c.github.io/webdriver/#get-element-shadow-root
+[Get Timeouts]: https://w3c.github.io/webdriver/webdriver-spec.html#get-timeouts
+[Get Window Rect]: https://w3c.github.io/webdriver/webdriver-spec.html#get-window-rect
+[insecure certificate]: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-insecure-certificate
+[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
+[Status]: https://w3c.github.io/webdriver/webdriver-spec.html#status
+[Take Element Screenshot]: https://w3c.github.io/webdriver/webdriver-spec.html#take-element-screenshot
+[WebDriver errors]: https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
+
+[Bastien Orivel]: https://github.com/Eijebong
+[David Burns]: https://github.com/AutomatedTester
+[Jason Juang]: https://github.com/juangj
+[Jeremy Lempereur]: https://github.com/o0Ignition0o
+[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
+[Olivier Tilloy]: https://github.com/oSoMoN
diff --git a/testing/geckodriver/CONTRIBUTING.md b/testing/geckodriver/CONTRIBUTING.md
new file mode 100644
index 0000000000..9f3e5df604
--- /dev/null
+++ b/testing/geckodriver/CONTRIBUTING.md
@@ -0,0 +1,2 @@
+Please see our contributor documentation at
+https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers.
diff --git a/testing/geckodriver/Cargo.toml b/testing/geckodriver/Cargo.toml
new file mode 100644
index 0000000000..4f2b60ce1b
--- /dev/null
+++ b/testing/geckodriver/Cargo.toml
@@ -0,0 +1,51 @@
+[package]
+edition = "2018"
+name = "geckodriver"
+version = "0.33.0"
+authors = ["Mozilla"]
+include = [
+ "/.cargo",
+ "/build.rs",
+ "/src"
+ ]
+description = "Proxy for using WebDriver clients to interact with Gecko-based browsers."
+readme = "README.md"
+keywords = [
+ "firefox",
+ "httpd",
+ "mozilla",
+ "w3c",
+ "webdriver",
+ ]
+license = "MPL-2.0"
+repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver"
+
+[dependencies]
+base64 = "0.21"
+chrono = "0.4.6"
+clap = { version = "~3.1", default-features = false, features = ["cargo", "std", "suggestions", "wrap_help"] }
+hyper = "0.14"
+lazy_static = "1.0"
+log = { version = "0.4", features = ["std"] }
+marionette = { path = "./marionette", version="0.4.0" }
+mozdevice = { path = "../mozbase/rust/mozdevice", version="0.5.0" }
+mozprofile = { path = "../mozbase/rust/mozprofile", version="0.9.0" }
+mozrunner = { path = "../mozbase/rust/mozrunner", version="0.15.0" }
+mozversion = { path = "../mozbase/rust/mozversion", version="0.5.0" }
+regex = { version="1.0", default-features = false, features = ["perf", "std"] }
+serde = "1.0"
+serde_derive = "1.0"
+serde_json = "1.0"
+serde_yaml = "0.8"
+tempfile = "3"
+unicode-segmentation = "1.9"
+url = "2.0"
+uuid = { version = "1.0", features = ["v4"] }
+webdriver = { path = "../webdriver", version="0.48.0" }
+zip = { version = "0.6", default-features = false, features = ["deflate"] }
+
+[dev-dependencies]
+tempfile = "3"
+
+[[bin]]
+name = "geckodriver"
diff --git a/testing/geckodriver/ISSUE_TEMPLATE.md b/testing/geckodriver/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000000..2b547946a5
--- /dev/null
+++ b/testing/geckodriver/ISSUE_TEMPLATE.md
@@ -0,0 +1,31 @@
+## System
+
+* Version: <!-- geckodriver version -->
+* Platform: <!-- e.g. Linux/macOS/Windows + version -->
+* Firefox: <!-- from the about dialogue -->
+* Selenium: <!-- client + version -->
+
+
+## Testcase
+
+<!--
+Please provide a minimal HTML document which permits the problem
+to be reproduced.
+-->
+
+
+## Stacktrace
+
+<!--
+Error and stacktrace produced by client.
+-->
+
+
+## Trace-level log
+
+<!--
+See https://searchfox.org/mozilla-central/source/testing/geckodriver/doc/TraceLogs.md
+for how to produce a trace-level log.
+
+For trace logs with more than 20 lines please add its contents as attachment.
+-->
diff --git a/testing/geckodriver/LICENSE b/testing/geckodriver/LICENSE
new file mode 100644
index 0000000000..9c3289a4a7
--- /dev/null
+++ b/testing/geckodriver/LICENSE
@@ -0,0 +1,385 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+
+************************************************************************
+
+************************************************************************
+
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+
+************************************************************************
+
+8. Litigation
+
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at <http://mozilla.org/MPL/2.0/>.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/testing/geckodriver/README.md b/testing/geckodriver/README.md
new file mode 100644
index 0000000000..6329e48736
--- /dev/null
+++ b/testing/geckodriver/README.md
@@ -0,0 +1,85 @@
+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 [Marionette remote protocol] by acting
+as a proxy between the local- and remote ends.
+
+[WebDriver protocol]: https://w3c.github.io/webdriver/#protocol
+[Marionette remote protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/
+[WebDriver]: https://developer.mozilla.org/en-US/docs/Web/WebDriver
+
+
+Downloads
+---------
+
+* [Releases](https://github.com/mozilla/geckodriver/releases/latest)
+* [Change log](https://searchfox.org/mozilla-central/source/testing/geckodriver/CHANGES.md)
+
+
+Documentation
+-------------
+
+* [WebDriver] (work in progress)
+ * [Commands](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands)
+ * [Errors](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors)
+ * [Types](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Types)
+
+* [Cross browser testing](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing)
+
+* [Selenium](https://seleniumhq.github.io/docs/) (work in progress)
+ * [C# API](https://seleniumhq.github.io/selenium/docs/api/dotnet/)
+ * [JavaScript API](https://seleniumhq.github.io/selenium/docs/api/javascript/)
+ * [Java API](https://seleniumhq.github.io/selenium/docs/api/java/)
+ * [Perl API](https://metacpan.org/pod/Selenium::Remote::Driver)
+ * [Python API](https://seleniumhq.github.io/selenium/docs/api/py/)
+ * [Ruby API](https://seleniumhq.github.io/selenium/docs/api/rb/)
+
+* [geckodriver usage](https://firefox-source-docs.mozilla.org/testing/geckodriver/Usage.html)
+ * [Supported platforms](https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html)
+ * [Firefox capabilities](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html)
+ * [Capabilities example](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#capabilities-example)
+ * [Enabling trace logs](https://firefox-source-docs.mozilla.org/testing/geckodriver/TraceLogs.html)
+ * [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 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
+
+Custom release builds
+---------------------
+
+If a binary is not available for your platform, it's possibe to create a custom
+build using the [Rust] toolchain. To do this, checkout the release tag for the
+version of interest and run `cargo build`. Alternatively the latest version may
+be built and installed from `crates.io` using `cargo install geckodriver`.
+
+[Rust]: https://rustup.rs/
+
+Contact
+-------
+
+The mailing list for geckodriver discussion is
+https://groups.google.com/a/mozilla.org/g/dev-webdriver.
+
+There is also an Element channel to talk about using and developing
+geckodriver on `#webdriver:mozilla.org <https://chat.mozilla.org/#/room/#webdriver:mozilla.org>`__
diff --git a/testing/geckodriver/build.rs b/testing/geckodriver/build.rs
new file mode 100644
index 0000000000..eb590476b7
--- /dev/null
+++ b/testing/geckodriver/build.rs
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Writes build information to ${OUT_DIR}/build-info.rs which is included in
+// the program during compilation:
+//
+// ```no_run
+// const COMMIT_HASH: Option<&'static str> = Some("c31a366");
+// const COMMIT_DATE: Option<&'static str> = Some("1988-05-10");
+// ```
+//
+// The values are `None` if running hg failed, e.g. if it is not installed or
+// if we are not in an hg repo.
+
+use std::env;
+use std::ffi::OsStr;
+use std::fs::File;
+use std::io;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+fn main() -> io::Result<()> {
+ let cur_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
+ let build_info = get_build_info(&cur_dir);
+
+ let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
+ let mut fh = File::create(out_dir.join("build-info.rs"))?;
+ writeln!(
+ fh,
+ "const COMMIT_HASH: Option<&'static str> = {:?};",
+ build_info.hash()
+ )?;
+ writeln!(
+ fh,
+ "const COMMIT_DATE: Option<&'static str> = {:?};",
+ build_info.date()
+ )?;
+
+ Ok(())
+}
+
+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")) {
+ Box::new(Git {})
+ } else if let Some(parent) = dir.parent() {
+ get_build_info(parent)
+ } else {
+ eprintln!("unable to detect vcs");
+ Box::new(Noop {})
+ }
+}
+
+trait BuildInfo {
+ fn hash(&self) -> Option<String>;
+ fn date(&self) -> Option<String>;
+}
+
+struct Hg;
+
+impl Hg {
+ fn exec<I, S>(&self, args: I) -> Option<String>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ Command::new("hg")
+ .env("HGPLAIN", "1")
+ .args(args)
+ .output()
+ .ok()
+ .and_then(|r| String::from_utf8(r.stdout).ok())
+ .map(|s| s.trim_end().into())
+ }
+}
+
+impl BuildInfo for Hg {
+ fn hash(&self) -> Option<String> {
+ self.exec(["log", "-r.", "-T{node|short}"])
+ }
+
+ fn date(&self) -> Option<String> {
+ self.exec(["log", "-r.", "-T{date|isodate}"])
+ }
+}
+
+struct Git;
+
+impl Git {
+ fn exec<I, S>(&self, args: I) -> Option<String>
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ Command::new("git")
+ .env("GIT_CONFIG_NOSYSTEM", "1")
+ .args(args)
+ .output()
+ .ok()
+ .and_then(|r| String::from_utf8(r.stdout).ok())
+ .map(|s| s.trim_end().into())
+ }
+
+ fn to_hg_sha(&self, git_sha: String) -> Option<String> {
+ self.exec(["cinnabar", "git2hg", &git_sha])
+ }
+}
+
+impl BuildInfo for Git {
+ fn hash(&self) -> Option<String> {
+ self.exec(["rev-parse", "HEAD"])
+ .and_then(|sha| self.to_hg_sha(sha))
+ .map(|mut s| {
+ s.truncate(12);
+ s
+ })
+ }
+
+ fn date(&self) -> Option<String> {
+ self.exec(["log", "-1", "--date=short", "--pretty=format:%cd"])
+ }
+}
+
+struct Noop;
+
+impl BuildInfo for Noop {
+ fn hash(&self) -> Option<String> {
+ None
+ }
+ fn date(&self) -> Option<String> {
+ None
+ }
+}
diff --git a/testing/geckodriver/doc/ARM.md b/testing/geckodriver/doc/ARM.md
new file mode 100644
index 0000000000..8ae9afae58
--- /dev/null
+++ b/testing/geckodriver/doc/ARM.md
@@ -0,0 +1,50 @@
+# 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:
+
+ ```shell
+ % curl https://sh.rustup.rs -sSf | sh
+ ```
+
+ 2. Install cross-compiler toolchain:
+
+ ```shell
+ % apt install gcc-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross
+ ```
+
+ 3. Create a new shell, or to reuse the existing shell:
+
+ ```shell
+ % source $HOME/.cargo/env
+ ```
+
+ 4. Install rustc target toolchain:
+
+ ```shell
+ % rustup target install armv7-unknown-linux-gnueabihf
+ ```
+
+ 5. Put this in [testing/geckodriver/.cargo/config]:
+
+ ```rust
+ [target.armv7-unknown-linux-gnueabihf]
+ linker = "arm-linux-gnueabihf-gcc"
+ ```
+
+ 6. Build geckodriver from testing/geckodriver:
+
+ ```shell
+ % cd testing/geckodriver
+ % cargo build --release --target armv7-unknown-linux-gnueabihf
+ ```
+
+[central]: https://hg.mozilla.org/mozilla-central/
+[testing/geckodriver/.cargo/config]: https://searchfox.org/mozilla-central/source/testing/geckodriver/.cargo/config
diff --git a/testing/geckodriver/doc/Bugs.md b/testing/geckodriver/doc/Bugs.md
new file mode 100644
index 0000000000..b561bd41b5
--- /dev/null
+++ b/testing/geckodriver/doc/Bugs.md
@@ -0,0 +1,45 @@
+# Reporting bugs
+
+When opening a new issue or commenting on existing issues, please
+make sure discussions are related to concrete technical issues
+with geckodriver or Marionette. Questions or discussions are more
+appropriate for the [mailing list].
+
+For issue reports to be actionable, it must be clear exactly
+what the observed and expected behaviours are, and how to set up
+the state required to observe the erroneous behaviour. The most
+useful thing to provide is a minimal HTML document which permits
+the problem to be reproduced along with a [trace-level log] from
+geckodriver showing the exact wire protocol calls made.
+
+Because of the wide variety and different charateristics of clients
+used with geckodriver, their stacktraces, logs, and code examples are
+typically not very useful as they distract from the actual underlying
+cause. **For this reason, we cannot overstate the importance of
+always providing the [trace-level log] from geckodriver.** Bugs
+relating to a specific client should be filed with that project.
+
+We welcome you to file issues in the [GitHub issue tracker] once you are
+confident it has not already been reported. The [ISSUE_TEMPLATE.md]
+contains a helpful checklist for things we will want to know about
+the affected system, reproduction steps, and logs.
+
+geckodriver development follows a rolling release model as
+we don’t release patches for older versions. It is therefore
+useful to use the tip-of-tree geckodriver binary, or failing this,
+the latest release when verifying the problem. geckodriver is only
+compatible with the current release channel versions of Firefox, and
+it consequently does not help to report bugs that affect outdated
+and unsupported Firefoxen. Please always try to verify the issue
+in the latest Firefox Nightly before you file your bug.
+
+Once we are satisfied the issue raised is of sufficiently actionable
+character, we will continue with triaging it and file a bug where it
+is appropriate. Bugs specific to geckodriver will be filed in the
+[`Testing :: geckodriver`] component in Bugzilla.
+
+[mailing list]: index.rst/#communication
+[trace-level log]: TraceLogs.md
+[GitHub issue tracker]: https://github.com/mozilla/geckodriver/issues
+[ISSUE_TEMPLATE.md]: https://raw.githubusercontent.com/mozilla/geckodriver/master/ISSUE_TEMPLATE.md
+[`Testing :: geckodriver`]: https://bugzilla.mozilla.org/buglist.cgi?component=geckodriver
diff --git a/testing/geckodriver/doc/Building.md b/testing/geckodriver/doc/Building.md
new file mode 100644
index 0000000000..49a5a51200
--- /dev/null
+++ b/testing/geckodriver/doc/Building.md
@@ -0,0 +1,46 @@
+# Building geckodriver
+
+geckodriver is written in [Rust], a systems programming language
+from Mozilla. Crucially, it relies on the [webdriver crate] to
+provide the HTTPD and do most of the heavy lifting of marshalling
+the WebDriver protocol. geckodriver translates WebDriver [commands],
+[responses], and [errors] to the [Marionette protocol], and acts
+as a proxy between [WebDriver] and [Marionette].
+
+To build geckodriver:
+
+```shell
+% ./mach build testing/geckodriver
+```
+
+If you use artifact builds you may build geckodriver using cargo,
+since mach in this case does not have a compile environment:
+
+```shell
+% cd testing/geckodriver
+% cargo build
+…
+Compiling geckodriver v0.21.0 (file:///code/gecko/testing/geckodriver)
+…
+Finished dev [optimized + debuginfo] target(s) in 7.83s
+```
+
+Because all Rust code in central shares the same cargo workspace,
+the binary will be put in the `$(topsrcdir)/target` directory.
+
+You can run your freshly built geckodriver this way:
+
+```shell
+% ./mach geckodriver -- --other --flags
+```
+
+See [Testing](Testing.md) for how to run tests.
+
+[Rust]: https://www.rust-lang.org/
+[webdriver crate]: https://crates.io/crates/webdriver
+[commands]: https://docs.rs/webdriver/newest/webdriver/command/
+[responses]: https://docs.rs/webdriver/newest/webdriver/response/
+[errors]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html
+[Marionette protocol]: /testing/marionette/Protocol.md
+[WebDriver]: https://w3c.github.io/webdriver/
+[Marionette]: /testing/marionette/index.rst
diff --git a/testing/geckodriver/doc/Capabilities.md b/testing/geckodriver/doc/Capabilities.md
new file mode 100644
index 0000000000..c9eb7a5f97
--- /dev/null
+++ b/testing/geckodriver/doc/Capabilities.md
@@ -0,0 +1,98 @@
+# 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).
+
+We additionally have some capabilities that largely are implementation
+concerns that normal users should not care about:
+
+## `moz:debuggerAddress`
+
+A boolean value to indicate if Firefox has to be started with the
+[Remote Protocol] enabled, which is a low-level debugging interface that
+implements a subset of the [Chrome DevTools Protocol] (CDP).
+
+When enabled the returned `moz:debuggerAddress` capability of the `New Session`
+command is the `host:port` combination of a server that supports the following
+HTTP endpoints:
+
+### GET /json/version
+
+The browser version metadata:
+
+```json
+{
+ "Browser": "Firefox/84.0a1",
+ "Protocol-Version": "1.0",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:84.0) Gecko/20100101 Firefox/84.0",
+ "V8-Version": "1.0",
+ "WebKit-Version": "1.0",
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/fe507083-2960-a442-bbd7-7dfe1f111c05"
+}
+```
+
+### GET /json/list
+
+A list of all available websocket targets:
+
+```json
+[ {
+ "description": "",
+ "devtoolsFrontendUrl": null,
+ "faviconUrl": "",
+ "id": "ecbf9028-676a-1b40-8596-a5edc0e2875b",
+ "type": "page",
+ "url": "https://www.mozilla.org/en-US/",
+ "browsingContextId": 29,
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/ecbf9028-676a-1b40-8596-a5edc0e2875b"
+} ]
+```
+
+The contained `webSocketDebuggerUrl` entries can be used to connect to the
+websocket and interact with the browser by using the CDP protocol.
+
+[Remote Protocol]: /remote/index.rst
+[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/
+
+## `moz:useNonSpecCompliantPointerOrigin`
+
+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
+is no longer computed based on the top and left position of the
+referenced element, but on the in-view center point.
+
+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.
+
+## `moz:webdriverClick`
+
+A boolean value to indicate which kind of interactability checks
+to run when performing a click or sending keys to an elements. For
+Firefoxen prior to version 58.0 some legacy code as imported from
+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.
+
+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].
+
+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.
diff --git a/testing/geckodriver/doc/CrashReports.md b/testing/geckodriver/doc/CrashReports.md
new file mode 100644
index 0000000000..576bdda7a9
--- /dev/null
+++ b/testing/geckodriver/doc/CrashReports.md
@@ -0,0 +1,67 @@
+# Analyzing crash data of Firefox
+
+It's not uncommon that under some special platform configurations and while
+running automated tests via Selenium and geckodriver Firefox could crash. In
+those cases it is very helpful to retrieve the generated crash data aka
+minidump files, and report these to us.
+
+## Retrieve the crash data
+
+Because geckodriver creates a temporary user profile for Firefox, it also
+automatically removes all its folders once the tests have been finished. That
+also means that if Firefox crashed the created minidump files are lost. To
+prevent that a custom profile has to be used instead. The following code
+shows an example by using the Python Selenium bindings on Mac OS:
+
+```python
+import tempfile
+
+from selenium import webdriver
+from selenium.webdriver.firefox.options import Options
+
+# Custom profile folder to keep the minidump files
+profile = tempfile.mkdtemp(".selenium")
+print("*** Using profile: {}".format(profile))
+
+# Use the above folder as custom profile
+opts = Options()
+opts.add_argument("-profile")
+opts.add_argument(profile)
+opts.binary = "/Applications/Firefox.app/Contents/MacOS/firefox"
+
+driver = webdriver.Firefox(
+ options=opts,
+ # hard-code the Marionette port so geckodriver can connect
+ service_args=["--marionette-port", "2828"]
+)
+
+# Your test code which crashes Firefox
+```
+
+Executing the test with Selenium now, which triggers the crash of Firefox
+will leave all the files from the user profile around in the above path.
+
+To retrieve the minidump files navigate to that folder and look for a sub
+folder with the name `minidumps`. It should contain at least one series of
+files. One file with the `.dmp` extension and another one with `.extra`.
+Both of those files are needed. If more crash files are present grab them all.
+
+Attach the files as best archived as zip file to the created [geckodriver issue]
+on Github.
+
+[geckodriver issue]: https://github.com/mozilla/geckodriver/issues/new
+
+## Getting details of the crash
+
+More advanced users can upload the generated minidump files themselves and
+receive details information about the crash. Therefore find the [crash reporter]
+folder and copy all the generated minidump files into the `pending` sub directory.
+Make sure that both the `.dmp` and `.extra` files are present.
+
+Once done you can also [view the crash reports].
+
+If you submitted a crash please do not forget to also add the link of the
+crash report to the geckodriver issue.
+
+[crash reporter]: https://support.mozilla.org/kb/mozillacrashreporter#w_viewing-reports-outside-of-firefox
+[view the crash reports]: https://support.mozilla.orgkb/mozillacrashreporter#w_viewing-crash-reports
diff --git a/testing/geckodriver/doc/Flags.md b/testing/geckodriver/doc/Flags.md
new file mode 100644
index 0000000000..3db6e27c06
--- /dev/null
+++ b/testing/geckodriver/doc/Flags.md
@@ -0,0 +1,216 @@
+<!-- markdownlint-disable MD033 -->
+# Flags
+
+## <code>-\\-allow-hosts <var>ALLOW_HOSTS</var>...</code>
+
+Values of the `Host` header to allow for incoming requests.
+
+By default the value of <var>HOST</var> is allowed. If `--allow-hosts`
+is provided, exactly the given values will be permitted. For example
+`--allow-host geckodriver.test webdriver.local` will allow requests
+with `Host` set to `geckodriver.test` or `webdriver.local`.
+
+Requests with `Host` set to an IP address are always allowed.
+
+## <code>-\\-allow-origins <var>ALLOW_ORIGINS</var>...</code>
+
+Values of the `Origin` header to allow for incoming requests.
+
+`Origin` is set by web browsers for all `POST` requests, and most
+other cross-origin requests. By default any request with an `Origin`
+header is rejected to protect against malicious websites trying to
+access geckodriver running on the local machine.
+
+If `--allow-origins` is provided, web services running on the given
+origin will be able to make requests to geckodriver. For example
+`--allow-origins https://webdriver.test:8080` will allow a web-based
+service on the origin with scheme `https`, hostname `webdriver.test`,
+and port `8080` to access the geckodriver instance.
+
+## <code>-\\-android-storage <var>ANDROID_STORAGE</var></code>
+
+**Deprecation warning**: This argument is deprecated and planned to be removed
+with the 0.31.0 release of geckodriver. As such it shouldn't be used with version
+0.30.0 or later anymore. By default the automatic detection will now use the
+external storage location, which is always readable and writeable.
+
+Selects the test data location on the Android device, eg. the Firefox profile.
+By default `auto` is used.
+
+<style type="text/css">
+ table { width: 100%; margin-bottom: 2em; }
+ table, th, td { border: solid gray 1px; }
+ td, th { padding: 10px; text-align: left; vertical-align: middle; }
+ td:nth-child(1), th:nth-child(1) { width: 10em; text-align: center; }
+</style>
+
+<table>
+ <thead>
+ <tr>
+ <th>Value
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td>auto
+ <td>Best suitable location based on whether the device is rooted.<br/>
+ If the device is rooted `internal` is used, otherwise `app`.
+ <tr>
+ <td>app
+ <td><p>Location: `/data/data/%androidPackage%/test_root`</p>
+ Based on the `androidPackage` capability that is passed as part of
+ `moz:firefoxOptions` when creating a new session. Commands that
+ change data in the app's directory are executed using run-as. This requires
+ that the installed app is debuggable.
+ <tr>
+ <td>internal
+ <td><p>Location: `/data/local/tmp/test_root`</p>
+ The device must be rooted since when the app runs, files that are created
+ in the profile, which is owned by the app user, cannot be changed by the
+ shell user. Commands will be executed via `su`.
+ <tr>
+ <td>sdcard
+ <td><p>Location: `$EXTERNAL_STORAGE/Android/data/%androidPackage%/files/test_root`</p>
+ This location is supported by all versions of Android whether if the device
+ is rooted or not.
+</table>
+
+## <code>-b <var>BINARY</var></code> / <code>-\\-binary <var>BINARY</var></code>
+
+Path to the Firefox binary to use. By default geckodriver tries to
+find and use the system installation of Firefox, but that behaviour
+can be changed by using this option. Note that the `binary`
+capability of the `moz:firefoxOptions` object that is passed when
+[creating a new session] will override this option.
+
+On Linux systems it will use the first _firefox_ binary found
+by searching the `PATH` environmental variable, which is roughly
+equivalent to calling [whereis(1)] and extracting the second column:
+
+```shell
+% whereis firefox
+firefox: /usr/bin/firefox /usr/local/firefox
+```
+
+On macOS, the binary is found by looking for the first _firefox-bin_
+binary in the same fashion as on Linux systems. This means it is
+possible to also use `PATH` to control where geckodriver should
+find Firefox on macOS. It will then look for _/Applications/Firefox.app_.
+
+On Windows systems, geckodriver looks for the system Firefox by
+scanning the Windows registry.
+
+[creating a new session]: https://w3c.github.io/webdriver/#new-session
+[whereis(1)]: http://www.manpagez.com/man/1/whereis/
+
+## <code>-\\-connect-existing</code>
+
+Connect geckodriver to an existing Firefox instance. This means
+geckodriver will abstain from the default of starting a new Firefox
+session.
+
+The existing Firefox instance must have [Marionette] enabled.
+To enable the remote protocol in Firefox, you can pass the
+`--marionette` flag. Unless the `marionette.port` preference
+has been user-set, Marionette will listen on port 2828. So when
+using `--connect-existing` it is likely you will also have to use
+`--marionette-port` to set the correct port.
+
+## <code>-\\-host <var>HOST</var></code>
+
+Host to use for the WebDriver server. Defaults to 127.0.0.1.
+
+## <code>-\\-jsdebugger</code>
+
+Attach [browser toolbox] debugger when Firefox starts. This is
+useful for debugging [Marionette] internals.
+
+To be prompted at the start of the test run or between tests,
+you can set the `marionette.debugging.clicktostart` preference to
+`true`.
+
+For reference, below is the list of preferences that enables the
+chrome debugger. These are all set implicitly when the
+argument is passed to geckodriver.
+
+* `devtools.browsertoolbox.panel` -> `jsdebugger`
+
+ Selects the Debugger panel by default.
+
+* `devtools.chrome.enabled` → true
+
+ Enables debugging of chrome code.
+
+* `devtools.debugger.prompt-connection` → false
+
+ Controls the remote connection prompt. Note that this will
+ automatically expose your Firefox instance to localhost.
+
+* `devtools.debugger.remote-enabled` → true
+
+ Allows a remote debugger to connect, which is necessary for
+ debugging chrome code.
+
+[browser toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox
+
+## <code>-\\-log <var>LEVEL</var></code>
+
+Set the Gecko and geckodriver log level. Possible values are `fatal`,
+`error`, `warn`, `info`, `config`, `debug`, and `trace`.
+
+## <code>-\\-log-no-truncate</code>
+
+Disables truncation of long log lines.
+
+## <code>-\\-marionette-host <var>HOST</var></code>
+
+Selects the host for geckodriver’s connection to the [Marionette]
+remote protocol. Defaults to 127.0.0.1.
+
+## <code>-\\-marionette-port <var>PORT</var></code>
+
+Selects the port for geckodriver’s connection to the [Marionette]
+remote protocol.
+
+In the default mode where geckodriver starts and manages the Firefox
+process, it will pick a free port assigned by the system and set the
+`marionette.port` preference in the profile.
+
+When `--connect-existing` is used and the Firefox process is not
+under geckodriver’s control, it will simply connect to <var>PORT</var>.
+
+`--connect-existing`: #connect-existing
+
+## <code>-p <var>PORT</var></code> / <code>-\\-port <var>PORT</var></code>
+
+Port to use for the WebDriver server. Defaults to 4444.
+
+A helpful trick is that it is possible to bind to 0 to get the
+system to atomically assign a free port.
+
+## <code>-\\-profile-root <var>PROFILE_ROOT</var></code>
+
+Path to the directory to use when creating temporary profiles. By
+default this is the system temporary directory. Both geckodriver and
+Firefox must have read-write access to this path.
+
+This setting can be useful when Firefox is sandboxed from the host
+filesystem such that it doesn't share the same system temporary
+directory as geckodriver (e.g. when running Firefox inside a container
+or packaged as a snap).
+
+## <code>-v[v]</code>
+
+Increases the logging verbosity by to debug level when passing
+a single `-v`, or to trace level if `-vv` is passed. This is
+analogous to passing `--log debug` and `--log trace`, respectively.
+
+## <code>-\\-websocket-port<var>PORT</var></code>
+
+Port to use to connect to WebDriver BiDi. Defaults to 9222.
+
+A helpful trick is that it is possible to bind to 0 to get the
+system to atomically assign a free port.
+
+[Marionette]: /testing/marionette/index.rst
diff --git a/testing/geckodriver/doc/Notarization.md b/testing/geckodriver/doc/Notarization.md
new file mode 100644
index 0000000000..ba1ba08d64
--- /dev/null
+++ b/testing/geckodriver/doc/Notarization.md
@@ -0,0 +1,44 @@
+# 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 the geckodriver binary is technically both signed and notarized, the
+actual validation can only be performed by MacOS if the machine that starts
+the geckodriver binary for the very first time is online. Offline validation
+would require shipping geckodriver as a DMG/PKG. You can track the relevant
+progress in [bug 1783943].
+
+Note: geckodriver releases between 0.26.0 and 0.31.0 don't have the
+notarization applied and always require the manual steps below to
+bypass the notarization requirement of the binary during the very first start.
+
+[new notarization requirements]: https://developer.apple.com/news/?id=04102019a
+[bug 1783943]: https://bugzilla.mozilla.org/show_bug.cgi?id=1783943
+
+## Offline mode
+
+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.
diff --git a/testing/geckodriver/doc/Patches.md b/testing/geckodriver/doc/Patches.md
new file mode 100644
index 0000000000..e559da075d
--- /dev/null
+++ b/testing/geckodriver/doc/Patches.md
@@ -0,0 +1,31 @@
+# Submitting patches
+
+You can submit patches by using [Phabricator]. Walk through its documentation
+in how to set it up, and uploading patches for review. Don't worry about which
+person to select for reviewing your code. It will be done automatically.
+
+Please also make sure to follow the [commit creation guidelines].
+
+Once you have contributed a couple of patches, we are happy to sponsor you in
+[becoming a Mozilla committer]. When you have been granted commit access
+level 1, you will have permission to use the [Firefox CI] to trigger your own
+“try runs” to test your changes. You can use the following [try preset] to run
+the most relevant tests:
+
+```shell
+% ./mach try --preset geckodriver
+```
+
+This preset will schedule geckodriver-related tests on various platforms. You can
+reduce the number of tasks by filtering on platforms (e.g. linux) or build type
+(e.g. opt):
+
+```shell
+% ./mach try --preset geckodriver -xq "'linux 'opt"
+```
+
+[Phabricator]: https://moz-conduit.readthedocs.io/en/latest/phabricator-user.html
+[commit creation guidelines]: https://mozilla-version-control-tools.readthedocs.io/en/latest/devguide/contributing.html?highlight=phabricator#submitting-patches-for-review
+[becoming a Mozilla committer]: https://www.mozilla.org/en-US/about/governance/policies/commit/
+[Firefox CI]: https://treeherder.mozilla.org/
+[try preset]: https://firefox-source-docs.mozilla.org/tools/try/presets.html
diff --git a/testing/geckodriver/doc/Profiles.md b/testing/geckodriver/doc/Profiles.md
new file mode 100644
index 0000000000..abad9af730
--- /dev/null
+++ b/testing/geckodriver/doc/Profiles.md
@@ -0,0 +1,103 @@
+# 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]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#args_array_of_strings
+[`profile` capability]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#profile_string
+[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. [remote/components/marionette.js](https://searchfox.org/mozilla-central/source/remote/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 `remote.prefs.recommended` starting with Firefox
+91. For older versions of Firefox, the preference to use was
+`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]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions#prefs_preferences_object
+
+## 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/testing/geckodriver/doc/Releasing.md b/testing/geckodriver/doc/Releasing.md
new file mode 100644
index 0000000000..79574f8a0e
--- /dev/null
+++ b/testing/geckodriver/doc/Releasing.md
@@ -0,0 +1,292 @@
+# Releasing geckodriver
+
+Releasing geckodriver is not as easy as it once used to be when the
+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.
+
+In any case, the steps to release geckodriver are as follows:
+
+[mozilla-central]: https://hg.mozilla.org/mozilla-central/
+[Mozilla’s CI infrastructure]: https://treeherder.mozilla.org/
+
+## Update in-tree dependency crates
+
+geckodriver depends on a number of Rust crates that also live in
+central by using relative paths. Here an excerpt from its `Cargo.toml`:
+
+```ini
+[dependencies]
+…
+marionette = { path = "./marionette" }
+…
+mozdevice = { path = "../mozbase/rust/mozdevice" }
+mozprofile = { path = "../mozbase/rust/mozprofile" }
+mozrunner = { path = "../mozbase/rust/mozrunner" }
+mozversion = { path = "../mozbase/rust/mozversion" }
+…
+webdriver = { path = "../webdriver" }
+```
+
+Because we need to export the geckodriver source code to the old
+GitHub repository when we release, we first need to publish these
+crates in the specified order if they have had any changes in the
+interim since the last 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`
+- `testing/webdriver`
+- `testing/geckodriver/marionette`
+
+For each crate:
+
+1. Change into the crates folder.
+2. Bump the version number in `Cargo.toml` based on [semantic versioning rules],
+ and also update the version dependency for other in-tree crates using the
+ currently modified crate. Note that running `cargo update` will fail if you
+ missed updating a crate's dependency.
+
+3. Use the [cargo-semver-checks] command to validate the version change:
+
+ ```shell
+ % cargo semver-checks check-release
+ ```
+
+4. Update the crate:
+
+ ```shell
+ % cargo update -p <crate name>
+ ```
+
+5. We also publish audit information for the crates based on Mozilla's
+ [audit criteria]. Because we use [wildcard audit entries] make sure that the
+ latest day of publication is still within the `end` date. The related entry
+ of the crate can be found at the top of [audits.toml]. If the date is over,
+ then update its value to at most 1 year in the future.
+
+6. Commit the changes for the modified [Cargo.toml] files, [Cargo.lock] and
+ [audits.toml].
+
+ ```shell
+ % git add Cargo.toml Cargo.lock audits.toml testing
+ % git commit -m "Bug XYZ - [rust-<name>] Release version <version>"
+ ```
+
+[semantic versioning rules]: https://semver.org/
+[cargo-semver-checks]: https://crates.io/crates/cargo-semver-checks
+[audit criteria]: https://mozilla.github.io/cargo-vet/audit-criteria.html
+[wildcard audit entries]: https://mozilla.github.io/cargo-vet/wildcard-audit-entries.html
+[Cargo.toml]: https://searchfox.org/mozilla-central/source/testing/geckodriver/Cargo.toml
+[Cargo.lock]: https://searchfox.org/mozilla-central/source/Cargo.lock
+[audits.toml]: https://searchfox.org/mozilla-central/source/supply-chain/audits.toml
+
+## Update the change log
+
+Notable changes to geckodriver are mentioned in [CHANGES.md]. Many
+users rely on this, so it’s important that you make it **relevant
+to end-users**. For example, we only mention changes that are visible
+to users. The change log is not a complete anthology of commits,
+as these often will not convey the essence of a change to end-users.
+If a feature was added but removed before release, there is no reason
+to list it as a change.
+
+It is good practice to also include relevant information from the
+[webdriver], [marionette], [rust-mozrunner], and [rust-mozdevice] crates,
+since these are the most important dependencies of geckodriver and a lot
+of its functionality is implemented there.
+
+To get a list of all the changes for one of the above crates one of the following
+commands can be used:
+
+```shell
+% hg log -M -r <revision>::central --template "{node|short}\t{desc|firstline}\n" <path>
+% git log --reverse $(git cinnabar hg2git <revision>)..HEAD --pretty="%s" <path>
+```
+
+where `<revision>` is the changeset of the last geckodriver release and `<path>`
+the location of the crate in the repository.
+
+Add the list of changes to the related release bug on Bugzilla, and also check the
+dependency list of the bug for other fixes that are worth mentioning.
+
+We follow the writing style of the existing change log, with
+one section per version (with a release date), with subsections
+‘Added’, ‘Changed’, 'Fixed' 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
+[webdriver]: https://searchfox.org/mozilla-central/source/testing/webdriver
+[marionette]: https://searchfox.org/mozilla-central/source/testing/geckodriver/marionette
+[rust-mozrunner]: https://searchfox.org/mozilla-central/source/testing/mozbase/rust/mozrunner
+[rust-mozdevice]: https://searchfox.org/mozilla-central/source/testing/mozbase/rust/mozdevice
+
+## 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 with that before deciding on the version number.
+
+After you’ve changed the version number, run
+
+```shell
+% ./mach build testing/geckodriver
+```
+
+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 landed, and got merged
+to `mozilla-central`, the changeset id from the merge commit has to picked for
+finalizing the change log. This specific id is needed because Taskcluster creates
+the final signed builds based on that merge.
+
+## 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:
+
+```shell
+% cargo publish
+```
+
+Note that if a crate has an in-tree dependency make sure to first
+change the dependency information.
+
+Do not release the geckodriver crate yet!
+
+Once all crates have been published observe the `/target/package/` folder under
+the root of the mozilla-central repository and remove all the folders related
+to the above published packages (it will save ~1GB disk space).
+
+## Export to GitHub
+
+The canonical GitHub repository is <https://github.com/mozilla/geckodriver.git>
+so make sure you have a local clone of that. It has three branches:
+_master_ which only contains the [README.md]; _old_ which was the
+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 [release commit that bumped the version number](#add-the-changeset-id)
+on mozilla-central:
+
+```shell
+% hg update $RELEASE_REVISION
+```
+
+Or:
+
+```shell
+% git checkout $(git cinnabar hg2git $RELEASE_REVISION)
+```
+
+We will now export the contents of [testing/geckodriver] to a new branch that
+is based on the _release_ branch, which will be used to create a pull request:
+
+```shell
+% cd $SRC/geckodriver
+% git checkout release
+% git pull
+% git checkout -b do_release_X.Y.Z
+% git rm -rf .
+% git clean -fxd
+% cp -rt $SRC/gecko/testing/geckodriver .
+```
+
+Now verify that geckodriver builds correctly by running:
+
+```shell
+% cargo build
+```
+
+[README.md]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md
+[testing/geckodriver]: https://searchfox.org/mozilla-central/source/testing/geckodriver
+
+## Commit local changes
+
+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`.
+
+```shell
+% git add . -- ':!mach_commands.py :!moz.build :!target/*'
+% git commit -S -am "Import of vX.Y.Z" (signed)
+```
+
+or if you cannot use signing use:
+
+```shell
+% git add . -- ':!mach_commands.py :!moz.build :!target/*'
+% git commit -am "Import of vX.Y.Z" (unsigned)
+```
+
+Then push the changes, and create a pull request:
+
+```shell
+% git push origin do_release_X.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 needs to be manually released on github.com. Therefore start to
+[draft a new release], and make the following changes:
+
+1. Specify the "Tag version", and select "Release" as target.
+
+2. Leave the release title empty
+
+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.
+
+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 the Linux platforms.
+
+5. Before announcing the release on GitHub publish the geckodriver crate as well
+ on crates.io by running `cargo publish` from the release branch.
+
+6. Send the release announcement to the [dev-webdriver] mailing list.
+
+[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
+[dev-webdriver]: https://groups.google.com/a/mozilla.org/g/dev-webdriver
+
+Congratulations! You’ve released geckodriver!
diff --git a/testing/geckodriver/doc/Support.md b/testing/geckodriver/doc/Support.md
new file mode 100644
index 0000000000..0c06ee1bc0
--- /dev/null
+++ b/testing/geckodriver/doc/Support.md
@@ -0,0 +1,183 @@
+<!-- markdownlint-disable MD033 -->
+# Supported platforms
+
+The following table shows a mapping between [geckodriver releases],
+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, th { padding: 5px 10px; text-align: center; }
+</style>
+
+<table>
+ <thead>
+ <tr>
+ <th rowspan="2">geckodriver
+ <th rowspan="2">Selenium
+ <th colspan="2">Firefox
+ </tr>
+ <tr>
+ <th>min
+ <th>max
+ </tr>
+ </thead>
+ </thead>
+ <tr>
+ <td>0.33.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>102 ESR
+ <td>n/a
+ <tr>
+ <td>0.32.2
+ <td>≥ 3.11 (3.14 Python)
+ <td>102 ESR
+ <td>n/a
+ <tr>
+ <td>0.32.1
+ <td>≥ 3.11 (3.14 Python)
+ <td>102 ESR
+ <td>n/a
+ <tr>
+ <td>0.32.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>102 ESR
+ <td>n/a
+ <tr>
+ <td>0.31.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>91 ESR
+ <td>n/a
+ <tr>
+ <tr>
+ <td>0.30.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>78 ESR
+ <td>90
+ <tr>
+ <td>0.29.1
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>90
+ <tr>
+ <td>0.29.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>90
+ <tr>
+ <td>0.28.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>90
+ <tr>
+ <td>0.27.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>90
+ <tr>
+ <td>0.26.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>60
+ <td>90
+ <tr>
+ <td>0.25.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>57
+ <td>90
+ <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>79
+ <tr>
+ <td>0.21.0
+ <td>≥ 3.11 (3.14 Python)
+ <td>57
+ <td>79
+ <tr>
+ <td>0.20.1
+ <td>≥ 3.5
+ <td>55
+ <td>62
+ <tr>
+ <td>0.20.0
+ <td>≥ 3.5
+ <td>55
+ <td>62
+ <tr>
+ <td>0.19.1
+ <td>≥ 3.5
+ <td>55
+ <td>62
+ <tr>
+ <td>0.19.0
+ <td>≥ 3.5
+ <td>55
+ <td>62
+ <tr>
+ <td>0.18.0
+ <td>≥ 3.4
+ <td>53
+ <td>62
+ <tr>
+ <td>0.17.0
+ <td>≥ 3.4
+ <td>52
+ <td>62
+</table>
+
+## Clients
+
+[Selenium] users must update to version 3.11 or later to use geckodriver.
+Other clients that follow the [W3C WebDriver specification][WebDriver]
+are also supported.
+
+## Firefoxen
+
+geckodriver is not yet feature complete. This means that it does
+not yet offer full conformance with the [WebDriver] standard
+or complete compatibility with [Selenium]. You can track the
+[implementation status] of the latest [Firefox Nightly] on MDN.
+We also keep track of known [Selenium], [remote protocol], and
+[specification] problems in our [issue tracker].
+
+Support is best in Firefox 57 and greater, although generally the more
+recent the Firefox version, the better the experience as they have
+more bug fixes and features. Some features will only be available
+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.md#android) for more details.
+
+[geckodriver releases]: https://github.com/mozilla/geckodriver/releases
+[Selenium]: https://github.com/seleniumhq/selenium
+[WebDriver]: https://w3c.github.io/webdriver/
+[implementation status]: https://bugzilla.mozilla.org/showdependencytree.cgi?id=721859&hide_resolved=1
+[remote protocol]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Amarionette
+[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/testing/geckodriver/doc/Testing.md b/testing/geckodriver/doc/Testing.md
new file mode 100644
index 0000000000..2f8f5c9ef6
--- /dev/null
+++ b/testing/geckodriver/doc/Testing.md
@@ -0,0 +1,69 @@
+# Testing geckodriver
+
+We verify and test geckodriver in a couple of different ways.
+Since it is an implementation of the WebDriver web standard, we share
+a set of conformance tests with other browser vendors through the
+[Web Platform Tests] (WPT) initiative. This lets us ensure web
+compatibility between _different_ WebDriver implementations for
+different browsers.
+
+In addition to the WPT tests, geckodriver and webdriver have
+unit tests. These are written in Rust, but you must explicitly
+tell mach to build these by adding the following line to your [mozconfig]:
+
+```make
+ac_add_options --enable-rust-tests
+```
+
+Tests can then be run by using the `test` sub command for [cargo] in the
+specific source folder:
+
+```shell
+% cd testing/geckodriver/src
+% cargo test
+```
+
+To run the more extensive WPT tests you can use mach, but first
+make sure you have built Firefox:
+
+```shell
+% ./mach build
+% ./mach wpt testing/web-platform/tests/webdriver
+```
+
+As these are functional integration tests and pop up Firefox windows
+sporadically, a helpful tip is to suppress the window whilst you
+are running them by using Firefox’ [headless mode]:
+
+```shell
+% ./mach wpt --headless testing/web-platform/tests/webdriver
+```
+
+The `--headless` flag is equivalent to setting the `MOZ_HEADLESS`
+output variable. In addition to `MOZ_HEADLESS` there is also
+`MOZ_HEADLESS_WIDTH` and `MOZ_HEADLESS_HEIGHT` for controlling the
+dimensions of the no-op virtual display. This is similar to using
+Xvfb(1) which you may know from the X windowing system, but has
+the additional benefit of also working on macOS and Windows.
+
+As you get in to development of geckodriver and Marionette you will
+increasingly grow to understand our love for [trace-level logs].
+They provide us with the input—the HTTP requests—from the client
+(in WPT’s case from the tests’ use of a custom WebDriver client),
+the translation geckodriver makes to the [Marionette protocol],
+the log output from Marionette, its responses back to geckodriver,
+and finally the output—or the HTTP response—back to the client.
+
+The [trace-level logs] can be surfaced by passing on the `-vv`
+flag to geckodriver through WPT:
+
+```shell
+% ./mach wpt --webdriver-arg=-vv testing/web-platform/tests/webdriver
+```
+
+[Web Platform Tests]: http://web-platform-tests.org/
+[cargo]: http://doc.crates.io/guide.html
+[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.md
+[Marionette protocol]: /testing/marionette/Protocol.md
diff --git a/testing/geckodriver/doc/TraceLogs.md b/testing/geckodriver/doc/TraceLogs.md
new file mode 100644
index 0000000000..fb4e298c44
--- /dev/null
+++ b/testing/geckodriver/doc/TraceLogs.md
@@ -0,0 +1,206 @@
+# Enabling trace logs
+
+geckodriver provides different bands of logs for different audiences.
+The most important log entries are shown to everyone by default,
+and these include which port geckodriver provides the WebDriver
+API on, as well as informative warnings, errors, and fatal exceptions.
+
+The different log bands are, in ascending bandwidth:
+
+1. `fatal` is reserved for exceptional circumstances when geckodriver
+ or Firefox cannot recover. This usually entails that either
+ one or both of the processes will exit.
+
+2. `error` messages are mistakes in the program code which it is
+ possible to recover from.
+
+3. `warn` shows warnings of more informative nature that are not
+ necessarily problems in geckodriver. This could for example happen
+ if you use the legacy `desiredCapabilities`/`requiredCapabilities`
+ objects instead of the new `alwaysMatch`/`firstMatch` structures.
+
+4. `info` (default) contains information about which port geckodriver
+ binds to, but also all messages from the lower-bandwidth levels
+ listed above.
+
+5. `config` additionally shows the negotiated capabilities after
+ matching the `alwaysMatch` capabilities with the sequence of
+ `firstMatch` capabilities.
+
+6. `debug` is reserved for information that is useful when programming.
+
+7. `trace`, where in addition to itself, all previous levels
+ are included. The trace level shows all HTTP requests received
+ by geckodriver, packets sent to and from the remote protocol in
+ Firefox, and responses sent back to your client.
+
+In other words this means that the configured level will coalesce
+entries from all lower bands including itself. If you set the log
+level to `error`, you will get log entries for both `fatal` and `error`.
+Similarly for `trace`, you will get all the logs that are offered.
+
+To help debug a problem with geckodriver or Firefox, the trace-level
+output is vital to understand what is going on. This is why we ask
+that trace logs are included when filing bugs gainst geckodriver.
+It is only under very special circumstances that a trace log is
+not needed, so you will normally find that our first action when
+triaging your issue will be to ask you to include one. Do yourself
+and us a favour and provide a trace-level log right away.
+
+To silence geckodriver altogether you may for example either redirect
+all output to append to some log files:
+
+```shell
+% geckodriver >>geckodriver.log 2>>geckodriver.err.log
+```
+
+Or a black hole somewhere:
+
+```shell
+% geckodriver >/dev/null 2>&1
+```
+
+The log level set for geckodriver is propagated to the Marionette
+logger in Firefox. Marionette is the remote protocol that geckodriver
+uses to implement WebDriver. This means enabling trace logs for
+geckodriver will also implicitly enable them for Marionette.
+
+The log level is set in different ways. Either by using the
+`--log <LEVEL>` option, where `LEVEL` is one of the log levels
+from the list above, or by using the `-v` (for debug) or `-vv`
+(for trace) shorthands. For example, the following command will
+enable trace logs for both geckodriver and Marionette:
+
+```shell
+% geckodriver -vv
+```
+
+The second way of setting the log level is through capabilities.
+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,
+and of course which log level to use.
+
+[`moz:firefoxOptions`]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md#firefox-capabilities
+[README]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md
+
+Each client has its own way of specifying capabilities, and some clients
+include “helpers” for providing browser-specific configuration.
+It is often advisable to use these helpers instead of encoding the
+JSON Object yourself because it can be difficult to get the exact
+details right, but if you choose to, it should look like this:
+
+```json
+{"moz:firefoxOptions": {"log": {"level": "trace"}}}
+```
+
+Note that most known WebDriver clients, such as those provided by
+the Selenium project, do not expose a way to actually _see_ the logs
+unless you redirect the log output to a particular file (using the
+method shown above) or let the client “inherit” geckodriver’s
+output, for example by redirecting the stdout and stderr streams to
+its own. The notable exceptions are the Python and Ruby bindings,
+which surface geckodriver logs in a remarkable easy and efficient way.
+
+See the client-specific documentation below for the most idiomatic
+way to enable trace logs in your language. We want to expand this
+documentation to cover all the best known clients people use with
+geckodriver. If you find your language missing, please consider
+[submitting a patch].
+
+[submitting a patch]: Patches.md
+
+## C-Sharp
+
+The Selenium [C# client] comes with a [`FirefoxOptions`] helper for
+constructing the [`moz:firefoxOptions`] capabilities object:
+
+```csharp
+FirefoxOptions options = new FirefoxOptions();
+options.LogLevel = FirefoxDriverLogLevel.Trace;
+IWebDriver driver = new FirefoxDriver(options);
+```
+
+The log output is directed to stdout.
+
+[C# client]: https://seleniumhq.github.io/selenium/docs/api/dotnet/
+[`FirefoxOptions`]: https://seleniumhq.github.io/selenium/docs/api/dotnet/html/T_OpenQA_Selenium_Firefox_FirefoxOptions.htm
+
+## Java
+
+The Selenium [Java client] also comes with
+a [`org.openqa.selenium.firefox.FirefoxOptions`] helper for
+constructing the [`moz:firefoxOptions`] capabilities object:
+
+```java
+FirefoxOptions options = new FirefoxOptions();
+options.setLogLevel(FirefoxDriverLogLevel.TRACE);
+WebDriver driver = new FirefoxDriver(options);
+```
+
+The log output is directed to stdout.
+
+[Java client]: https://seleniumhq.github.io/selenium/docs/api/java/
+[`org.openqa.selenium.firefox.FirefoxOptions`]: https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/firefox/FirefoxOptions.html
+
+## Javascript (webdriver.io)
+
+With the Selenium [JavaScript client] the capabilities object can directly be
+constructed:
+
+```javascript
+import WebDriver from 'webdriver'
+
+const driver = await WebDriver.newSession({
+ capabilities: {
+ browserName: 'firefox',
+ 'moz:firefoxOptions': {
+ log: { level: 'trace' },
+ }
+ }
+})
+```
+
+The log output is directed to stdout, or if geckodriver runs as a wdio plugin
+then the generated logs are part of the wdio log system.
+
+[JavaScript client]: https://webdriver.io/
+
+## Python
+
+The Selenium [Python client] comes with a
+[`selenium.webdriver.firefox.options.Options`] helper that can
+be used programmatically to construct the [`moz:firefoxOptions`]
+capabilities object:
+
+```python
+from selenium.webdriver import Firefox
+from selenium.webdriver.firefox.options import Options
+
+opts = Options()
+opts.log.level = "trace"
+driver = Firefox(options=opts)
+```
+
+The log output is stored in a file called _geckodriver.log_ in your
+script’s current working directory.
+
+[Python client]: https://selenium-python.readthedocs.io/
+[`selenium.webdriver.firefox.options.Options`]: https://github.com/SeleniumHQ/selenium/blob/master/py/selenium/webdriver/firefox/options.py
+
+## Ruby
+
+The Selenium [Ruby client] comes with an [`Options`] helper to
+generate the correct [`moz:firefoxOptions`] capabilities object:
+
+```ruby
+Selenium::WebDriver.logger.level = :debug
+opts = Selenium::WebDriver::Firefox::Options.new(log_level: :trace)
+driver = Selenium::WebDriver.for :firefox, options: opts
+```
+
+The log output is directed to stdout.
+
+[Ruby client]: https://seleniumhq.github.io/selenium/docs/api/rb/
+[`Options`]: https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/Firefox/Options.html
diff --git a/testing/geckodriver/doc/Usage.md b/testing/geckodriver/doc/Usage.md
new file mode 100644
index 0000000000..ee4ab86f8d
--- /dev/null
+++ b/testing/geckodriver/doc/Usage.md
@@ -0,0 +1,143 @@
+# Usage
+
+geckodriver is an implementation of WebDriver, and WebDriver can
+be used for widely different purposes. How you invoke geckodriver
+largely depends on your use case.
+
+## Running Firefox in a container-based package
+
+When Firefox is packaged inside a container (e.g. [Snap], [Flatpak]), it may
+see a different filesystem to the host. This can affect access to the generated
+profile directory, which may result in a hang when starting Firefox.
+
+This is known to affect launching the default Firefox shipped with Ubuntu 22.04+.
+
+There are several workarounds available for this problem:
+
+- Do not use container-packaged Firefox builds with geckodriver. Instead
+download a Firefox release from <https://download.mozilla.org/?product=firefox-latest&os=linux>
+and a geckodriver release from <https://github.com/mozilla/geckodriver/releases>.
+
+- Use a geckodriver that runs in the same container filesystem as the Firefox
+package. For example on Ubuntu `/snap/bin/geckodriver` will work with the
+default Firefox.
+
+- Set the `--profile-root` command line option to write the profile to a
+directory accessible to both Firefox and geckodriver, for example a non-hidden
+directory under `$HOME`.
+
+[Flatpak]: https://flatpak.org/
+[Snap]: https://ubuntu.com/core/services/guide/snaps-intro
+
+## Selenium
+
+If you are using geckodriver through [Selenium], you must ensure that
+you have version 3.11 or greater. Because geckodriver implements the
+[W3C WebDriver standard][WebDriver] and not the same Selenium wire
+protocol older drivers are using, you may experience incompatibilities
+and migration problems when making the switch from FirefoxDriver to
+geckodriver.
+
+Generally speaking, Selenium 3 enabled geckodriver as the default
+WebDriver implementation for Firefox. With the release of Firefox 47,
+FirefoxDriver had to be discontinued for its lack of support for the
+[new multi-processing architecture in Gecko][e10s].
+
+Selenium client bindings will pick up the _geckodriver_ binary executable
+from your [system’s `PATH` environmental variable][PATH] unless you
+override it by setting the `webdriver.gecko.driver` [Java VM system
+property]:
+
+```java
+System.setProperty("webdriver.gecko.driver", "/home/user/bin");
+```
+
+Or by passing it as a flag to the [java(1)] launcher:
+
+```shell
+% java -Dwebdriver.gecko.driver=/home/user/bin YourApplication
+```
+
+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
+location by exporting or setting the `PATH` variable:
+
+```shell
+% export PATH=$PATH:/home/user/bin
+% whereis geckodriver
+geckodriver: /home/user/bin/geckodriver
+```
+
+On Window systems you can change the system path by right-clicking **My
+Computer** and choosing **Properties**. In the dialogue that appears,
+navigate **Advanced** → **Environmental Variables** → **Path**.
+
+Or in the Windows console window:
+
+```shell
+% set PATH=%PATH%;C:\bin\geckodriver
+```
+
+## Standalone
+
+Since geckodriver is a separate HTTP server that is a complete remote end
+implementation of [WebDriver], it is possible to avoid using the Selenium
+remote server if you have no requirements to distribute processes across
+a matrix of systems.
+
+Given a W3C WebDriver conforming client library (or _local end_) you
+may interact with the geckodriver HTTP server as if you were speaking
+to any Selenium server.
+
+Using [curl(1)]:
+
+```shell
+% geckodriver &
+[1] 16010
+% 1491834109194 geckodriver INFO Listening on 127.0.0.1:4444
+% curl -H 'Content-Type: application/json' -d '{"capabilities": {"alwaysMatch": {"acceptInsecureCerts": true}}}' http://localhost:4444/session
+{"value":{"sessionId":"d4605710-5a4e-4d64-a52a-778bb0c31e00","capabilities":{"acceptInsecureCerts":true,[...]}}}
+% curl -H 'Content-Type: application/json' -d '{"url": "https://mozilla.org"}' http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
+{}
+% curl http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
+{"value":"https://www.mozilla.org/en-US/"
+% curl -X DELETE http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00
+{}
+% fg
+geckodriver
+^C
+```
+
+Using the Python [wdclient] library:
+
+```python
+import webdriver
+
+with webdriver.Session("127.0.0.1", 4444) as session:
+ session.url = "https://mozilla.org"
+ print "The current URL is %s" % session.url
+```
+
+And to run:
+
+```shell
+% geckodriver &
+[1] 16054
+% python example.py
+1491835308354 geckodriver INFO Listening on 127.0.0.1:4444
+The current URL is https://www.mozilla.org/en-US/
+% fg
+geckodriver
+^C
+```
+
+[Selenium]: http://seleniumhq.org/
+[e10s]: https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox
+[PATH]: https://en.wikipedia.org/wiki/PATH_(variable)
+[Java VM system property]: http://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html
+[java(1)]: http://www.manpagez.com/man/1/java/
+[WebDriver]: https://w3c.github.io/webdriver/
+[curl(1)]: http://www.manpagez.com/man/1/curl/
+[wdclient]: https://github.com/web-platform-tests/wpt/tree/master/tools/webdriver
diff --git a/testing/geckodriver/doc/index.rst b/testing/geckodriver/doc/index.rst
new file mode 100644
index 0000000000..863c05810c
--- /dev/null
+++ b/testing/geckodriver/doc/index.rst
@@ -0,0 +1,55 @@
+===========
+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 :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
+.. _change log: https://github.com/mozilla/geckodriver/releases
+.. _Releases: https://github.com/mozilla/geckodriver/releases
+
+
+.. toctree::
+ :maxdepth: 1
+
+ Support.md
+ WebDriver capabilities <https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities>
+ Capabilities.md
+ Usage.md
+ Flags.md
+ Profiles.md
+ Bugs.md
+ TraceLogs.md
+ CrashReports.md
+ Notarization.md
+
+
+For developers
+==============
+.. toctree::
+ :maxdepth: 1
+
+ Building.md
+ Testing.md
+ Patches.md
+ Releasing.md
+ ARM.md
+
+
+Communication
+=============
+
+The mailing list for geckodriver discussion is
+https://groups.google.com/a/mozilla.org/g/dev-webdriver.
+
+If you prefer real-time chat, ask your questions
+on `#webdriver:mozilla.org <https://chat.mozilla.org/#/room/#webdriver:mozilla.org>`__.
diff --git a/testing/geckodriver/mach_commands.py b/testing/geckodriver/mach_commands.py
new file mode 100644
index 0000000000..cb56d3acf2
--- /dev/null
+++ b/testing/geckodriver/mach_commands.py
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import logging
+import os
+
+from mach.decorators import Command, CommandArgument, CommandArgumentGroup
+from mozbuild.base import BinaryNotFoundException
+
+
+@Command(
+ "geckodriver",
+ category="post-build",
+ description="Run the WebDriver implementation for Gecko.",
+)
+@CommandArgument(
+ "--binary", type=str, help="Firefox binary (defaults to the local build)."
+)
+@CommandArgument(
+ "params", nargs="...", help="Flags to be passed through to geckodriver."
+)
+@CommandArgumentGroup("debugging")
+@CommandArgument(
+ "--debug",
+ action="store_true",
+ group="debugging",
+ help="Enable the debugger. Not specifying a --debugger "
+ "option will result in the default debugger "
+ "being used.",
+)
+@CommandArgument(
+ "--debugger",
+ default=None,
+ type=str,
+ group="debugging",
+ help="Name of debugger to use.",
+)
+@CommandArgument(
+ "--debugger-args",
+ default=None,
+ metavar="params",
+ type=str,
+ group="debugging",
+ help="Flags to pass to the debugger itself; split as the Bourne shell would.",
+)
+def run(command_context, binary, params, debug, debugger, debugger_args):
+ try:
+ binpath = command_context.get_binary_path("geckodriver")
+ except BinaryNotFoundException as e:
+ command_context.log(
+ logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}"
+ )
+ command_context.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]
+
+ if params:
+ args.extend(params)
+
+ if binary is None:
+ try:
+ binary = command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(
+ logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}"
+ )
+ command_context.log(
+ logging.INFO, "geckodriver", {"help": e.help()}, "{help}"
+ )
+ return 1
+
+ args.extend(["--binary", binary])
+
+ if debug or debugger or debugger_args:
+ if "INSIDE_EMACS" in os.environ:
+ command_context.log_manager.terminal_handler.setLevel(logging.WARNING)
+
+ import mozdebug
+
+ if not debugger:
+ # No debugger name was provided. Look for the default ones on
+ # current OS.
+ debugger = mozdebug.get_default_debugger_name(
+ mozdebug.DebuggerSearch.KeepLooking
+ )
+
+ if debugger:
+ debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+ if not debuggerInfo:
+ print("Could not find a suitable debugger in your PATH.")
+ return 1
+
+ # Parameters come from the CLI. We need to convert them before
+ # their use.
+ if debugger_args:
+ from mozbuild import shellutil
+
+ try:
+ debugger_args = shellutil.split(debugger_args)
+ except shellutil.MetaCharacterException as e:
+ print(
+ "The --debugger-args you passed require a real shell to parse them."
+ )
+ print("(We can't handle the %r character.)" % e.char)
+ return 1
+
+ # Prepend the debugger args.
+ args = [debuggerInfo.path] + debuggerInfo.args + args
+
+ return command_context.run_process(
+ args=args, ensure_exit_code=False, pass_thru=True
+ )
diff --git a/testing/geckodriver/marionette/Cargo.toml b/testing/geckodriver/marionette/Cargo.toml
new file mode 100644
index 0000000000..2bcfaa37a6
--- /dev/null
+++ b/testing/geckodriver/marionette/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "marionette"
+version = "0.4.0"
+authors = ["Mozilla"]
+description = "Library implementing the client side of Gecko's Marionette remote automation protocol."
+edition = "2018"
+keywords = ["mozilla", "firefox", "marionette", "webdriver"]
+license = "MPL-2.0"
+repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver/marionette"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_repr = "0.1"
diff --git a/testing/geckodriver/marionette/src/common.rs b/testing/geckodriver/marionette/src/common.rs
new file mode 100644
index 0000000000..e819757e73
--- /dev/null
+++ b/testing/geckodriver/marionette/src/common.rs
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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", 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 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}),
+ );
+ }
+
+ #[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/testing/geckodriver/marionette/src/error.rs b/testing/geckodriver/marionette/src/error.rs
new file mode 100644
index 0000000000..6b05410047
--- /dev/null
+++ b/testing/geckodriver/marionette/src/error.rs
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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 From<MarionetteError> for Error {
+ fn from(error: MarionetteError) -> Error {
+ Error::Marionette(error)
+ }
+}
+
+#[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/testing/geckodriver/marionette/src/lib.rs b/testing/geckodriver/marionette/src/lib.rs
new file mode 100644
index 0000000000..80817c5f5b
--- /dev/null
+++ b/testing/geckodriver/marionette/src/lib.rs
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/marionette/src/marionette.rs b/testing/geckodriver/marionette/src/marionette.rs
new file mode 100644
index 0000000000..c06e2d60c7
--- /dev/null
+++ b/testing/geckodriver/marionette/src/marionette.rs
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/marionette/src/message.rs b/testing/geckodriver/marionette/src/message.rs
new file mode 100644
index 0000000000..704d52f67b
--- /dev/null
+++ b/testing/geckodriver/marionette/src/message.rs
@@ -0,0 +1,336 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/marionette/src/result.rs b/testing/geckodriver/marionette/src/result.rs
new file mode 100644
index 0000000000..95817c15f0
--- /dev/null
+++ b/testing/geckodriver/marionette/src/result.rs
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/marionette/src/test.rs b/testing/geckodriver/marionette/src/test.rs
new file mode 100644
index 0000000000..3b20bb0917
--- /dev/null
+++ b/testing/geckodriver/marionette/src/test.rs
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/marionette/src/webdriver.rs b/testing/geckodriver/marionette/src/webdriver.rs
new file mode 100644
index 0000000000..fa1a48927c
--- /dev/null
+++ b/testing/geckodriver/marionette/src/webdriver.rs
@@ -0,0 +1,512 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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 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 {
+ #[serde(rename = "WebDriver:AcceptAlert")]
+ 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:DeleteSession")]
+ DeleteSession,
+ #[serde(rename = "WebDriver:DismissAlert")]
+ DismissAlert,
+ #[serde(rename = "WebDriver:ElementClear")]
+ ElementClear { id: String },
+ #[serde(rename = "WebDriver:ElementClick")]
+ ElementClick { id: String },
+ #[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:FindElementFromShadowRoot")]
+ FindShadowRootElement {
+ #[serde(rename = "shadowRoot")]
+ shadow_root: String,
+ using: Selector,
+ value: String,
+ },
+ #[serde(rename = "WebDriver:FindElementsFromShadowRoot")]
+ FindShadowRootElements {
+ #[serde(rename = "shadowRoot")]
+ shadow_root: 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:GetComputedLabel")]
+ GetComputedLabel { id: String },
+ #[serde(rename = "WebDriver:GetComputedRole")]
+ GetComputedRole { id: String },
+ #[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 { id: String },
+ #[serde(rename = "WebDriver:GetElementTagName")]
+ GetElementTagName { id: String },
+ #[serde(rename = "WebDriver:GetElementText")]
+ GetElementText { id: String },
+ #[serde(rename = "WebDriver:GetPageSource")]
+ GetPageSource,
+ #[serde(rename = "WebDriver:GetShadowRoot")]
+ GetShadowRoot { id: String },
+ #[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 { id: String },
+ #[serde(rename = "WebDriver:IsElementEnabled")]
+ IsEnabled { id: String },
+ #[serde(rename = "WebDriver:IsElementSelected")]
+ IsSelected { id: String },
+ #[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_computed_label_command() {
+ assert_ser_de(
+ &Command::GetComputedLabel { id: "foo".into() },
+ json!({"WebDriver:GetComputedLabel": {"id": "foo"}}),
+ );
+ }
+
+ #[test]
+ fn test_json_get_computed_role_command() {
+ assert_ser_de(
+ &Command::GetComputedRole { id: "foo".into() },
+ json!({"WebDriver:GetComputedRole": {"id": "foo"}}),
+ );
+ }
+
+ #[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"}}),
+ );
+ }
+
+ #[test]
+ fn test_json_find_shadow_root_element() {
+ assert_ser_de(
+ &Command::FindShadowRootElement {
+ shadow_root: "foo".into(),
+ using: Selector::Css,
+ value: "bar".into(),
+ },
+ json!({"WebDriver:FindElementFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
+ );
+ }
+
+ #[test]
+ fn test_json_find_shadow_root_elements() {
+ assert_ser_de(
+ &Command::FindShadowRootElements {
+ shadow_root: "foo".into(),
+ using: Selector::Css,
+ value: "bar".into(),
+ },
+ json!({"WebDriver:FindElementsFromShadowRoot": {"shadowRoot": "foo", "using": "css selector", "value": "bar"}}),
+ );
+ }
+}
diff --git a/testing/geckodriver/moz.build b/testing/geckodriver/moz.build
new file mode 100644
index 0000000000..9cda32b430
--- /dev/null
+++ b/testing/geckodriver/moz.build
@@ -0,0 +1,27 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+RUST_PROGRAMS += ["geckodriver"]
+# Some Rust build scripts compile C/C++ sources, don't error on warnings for them.
+AllowCompilerWarnings()
+
+RUST_TESTS = [
+ "geckodriver",
+ "webdriver",
+ "marionette",
+ # TODO: Move to mozbase/rust/moz.build once those crates can be
+ # tested separately.
+ "mozdevice",
+ "mozprofile",
+ "mozrunner",
+ "mozversion",
+]
+
+with Files("**"):
+ BUG_COMPONENT = ("Testing", "geckodriver")
+
+SPHINX_TREES["/testing/geckodriver"] = "doc"
+
+with Files("doc/**"):
+ SCHEDULES.exclusive = ["docs"]
diff --git a/testing/geckodriver/src/android.rs b/testing/geckodriver/src/android.rs
new file mode 100644
index 0000000000..10293d5c4e
--- /dev/null
+++ b/testing/geckodriver/src/android.rs
@@ -0,0 +1,533 @@
+use crate::capabilities::AndroidOptions;
+use mozdevice::{AndroidStorage, Device, Host, UnixPathBuf};
+use mozprofile::profile::Profile;
+use serde::Serialize;
+use serde_yaml::{Mapping, Value};
+use std::fmt;
+use std::io;
+use std::time;
+use webdriver::error::{ErrorStatus, WebDriverError};
+
+// TODO: avoid port clashes across GeckoView-vehicles.
+// For now, we always use target port 2829, leading to issues like bug 1533704.
+const MARIONETTE_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),
+ 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::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)
+ }
+}
+
+impl From<AndroidError> for WebDriverError {
+ fn from(value: AndroidError) -> WebDriverError {
+ WebDriverError::new(ErrorStatus::UnknownError, value.to_string())
+ }
+}
+
+/// 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)]
+pub struct AndroidHandler {
+ pub config: UnixPathBuf,
+ pub options: AndroidOptions,
+ pub process: AndroidProcess,
+ pub profile: UnixPathBuf,
+ pub test_root: UnixPathBuf,
+
+ // Port forwarding for Marionette: host => target
+ pub marionette_host_port: u16,
+ pub marionette_target_port: u16,
+
+ // Port forwarding for WebSocket connections (WebDriver BiDi and CDP)
+ pub websocket_port: Option<u16>,
+}
+
+impl Drop for AndroidHandler {
+ fn drop(&mut self) {
+ // Try to clean up various settings
+ let clear_command = format!("am clear-debug-app {}", self.process.package);
+ match self
+ .process
+ .device
+ .execute_host_shell_command(&clear_command)
+ {
+ Ok(_) => debug!("Disabled reading from configuration file"),
+ Err(e) => error!("Failed disabling from configuration file: {}", e),
+ }
+
+ match self.process.device.remove(&self.config) {
+ Ok(_) => debug!("Deleted GeckoView configuration file"),
+ Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
+ }
+
+ match self.process.device.remove(&self.test_root) {
+ Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()),
+ Err(e) => error!("Failed deleting test root folder: {}", e),
+ }
+
+ match self
+ .process
+ .device
+ .kill_forward_port(self.marionette_host_port)
+ {
+ Ok(_) => debug!(
+ "Marionette port forward ({} -> {}) stopped",
+ &self.marionette_host_port, &self.marionette_target_port
+ ),
+ Err(e) => error!(
+ "Marionette port forward ({} -> {}) failed to stop: {}",
+ &self.marionette_host_port, &self.marionette_target_port, e
+ ),
+ }
+
+ if let Some(port) = self.websocket_port {
+ match self.process.device.kill_forward_port(port) {
+ Ok(_) => debug!("WebSocket port forward ({0} -> {0}) stopped", &port),
+ Err(e) => error!(
+ "WebSocket port forward ({0} -> {0}) failed to stop: {1}",
+ &port, e
+ ),
+ }
+ }
+ }
+}
+
+impl AndroidHandler {
+ pub fn new(
+ options: &AndroidOptions,
+ marionette_host_port: u16,
+ websocket_port: Option<u16>,
+ ) -> Result<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 host = Host {
+ host: None,
+ port: None,
+ read_timeout: Some(time::Duration::from_millis(5000)),
+ write_timeout: Some(time::Duration::from_millis(5000)),
+ };
+
+ let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?;
+
+ // Set up port forwarding for Marionette.
+ device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?;
+ debug!(
+ "Marionette port forward ({} -> {}) started",
+ marionette_host_port, MARIONETTE_TARGET_PORT
+ );
+
+ if let Some(port) = websocket_port {
+ // Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP).
+ device.forward_port(port, port)?;
+ debug!("WebSocket port forward ({} -> {}) started", port, port);
+ }
+
+ let test_root = match device.storage {
+ AndroidStorage::App => {
+ device.run_as_package = Some(options.package.to_owned());
+ let mut buf = UnixPathBuf::from("/data/data");
+ buf.push(&options.package);
+ buf.push("test_root");
+ buf
+ }
+ AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
+ AndroidStorage::Sdcard => {
+ // We need to push the profile to a location on the device that can also
+ // be read and write by the application, and works for unrooted devices.
+ // The only location that meets this criteria is under:
+ // $EXTERNAL_STORAGE/Android/data/%options.package%/files
+ let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?;
+ let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
+ buf.push("Android/data");
+ buf.push(&options.package);
+ buf.push("files/test_root");
+ buf
+ }
+ };
+
+ debug!(
+ "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}",
+ options,
+ device.storage,
+ test_root.display(),
+ device.run_as_package
+ );
+
+ let mut profile = test_root.clone();
+ profile.push(format!("{}-geckodriver-profile", &options.package));
+
+ // Check if the specified package is installed
+ let response =
+ device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?;
+ let mut packages = response
+ .trim()
+ .split_terminator('\n')
+ .filter(|line| line.starts_with("package:"))
+ .map(|line| line.rsplit(':').next().expect("Package name found"));
+ if !packages.any(|x| x == options.package.as_str()) {
+ return Err(AndroidError::PackageNotFound(options.package.clone()));
+ }
+
+ let config = UnixPathBuf::from(format!(
+ "/data/local/tmp/{}-geckoview-config.yaml",
+ &options.package
+ ));
+
+ // If activity hasn't been specified default to the main activity of the package
+ let activity = match options.activity {
+ Some(ref activity) => activity.clone(),
+ None => {
+ let response = device.execute_host_shell_command(&format!(
+ "cmd package resolve-activity --brief {}",
+ &options.package
+ ))?;
+ let activities = response
+ .split_terminator('\n')
+ .filter(|line| line.starts_with(&options.package))
+ .map(|line| line.rsplit('/').next().unwrap())
+ .collect::<Vec<&str>>();
+ if activities.is_empty() {
+ return Err(AndroidError::ActivityNotFound(options.package.clone()));
+ }
+
+ activities[0].to_owned()
+ }
+ };
+
+ let process = AndroidProcess::new(device, options.package.clone(), activity)?;
+
+ Ok(AndroidHandler {
+ config,
+ process,
+ profile,
+ test_root,
+ marionette_host_port,
+ marionette_target_port: MARIONETTE_TARGET_PORT,
+ options: options.clone(),
+ websocket_port,
+ })
+ }
+
+ pub fn generate_config_file<I, K, V>(
+ &self,
+ args: Option<Vec<String>>,
+ 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, Eq, Debug)]
+ pub struct Config {
+ pub env: Mapping,
+ pub args: Vec<String>,
+ }
+
+ let mut config = Config {
+ args: vec![
+ "--marionette".into(),
+ "--profile".into(),
+ self.profile.display().to_string(),
+ ],
+ env: Mapping::new(),
+ };
+
+ config.args.append(&mut args.unwrap_or_default());
+
+ 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,
+ args: Option<Vec<String>>,
+ env: I,
+ ) -> Result<()>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: ToString,
+ V: ToString,
+ {
+ self.process.device.clear_app_data(&self.process.package)?;
+
+ // These permissions, at least, are required to read profiles in /mnt/sdcard.
+ for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
+ self.process.device.execute_host_shell_command(&format!(
+ "pm grant {} android.permission.{}",
+ &self.process.package, perm
+ ))?;
+ }
+
+ // Make sure to create the test root.
+ self.process.device.create_dir(&self.test_root)?;
+ self.process.device.chmod(&self.test_root, "777", true)?;
+
+ // Replace the profile
+ self.process.device.remove(&self.profile)?;
+ self.process
+ .device
+ .push_dir(&profile.path, &self.profile, 0o777)?;
+
+ let contents = self.generate_config_file(args, 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()
+ );
+ self.process.device.push(reader, &self.config, 0o777)?;
+
+ // Tell GeckoView to read configuration even when `android:debuggable="false"`.
+ self.process.device.execute_host_shell_command(&format!(
+ "am set-debug-app --persistent {}",
+ self.process.package
+ ))?;
+
+ Ok(())
+ }
+
+ pub fn launch(&self) -> Result<()> {
+ // 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()));
+
+ debug!(
+ "Launching {}/{}",
+ self.process.package, self.process.activity
+ );
+ self.process
+ .device
+ .launch(
+ &self.process.package,
+ &self.process.activity,
+ &intent_arguments,
+ )
+ .map_err(|e| {
+ let message = format!(
+ "Could not launch Android {}/{}: {}",
+ self.process.package, self.process.activity, e
+ );
+ mozdevice::DeviceError::Adb(message)
+ })?;
+
+ Ok(())
+ }
+
+ pub fn force_stop(&self) -> Result<()> {
+ debug!(
+ "Force stopping the Android package: {}",
+ &self.process.package
+ );
+ self.process.device.force_stop(&self.process.package)?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ // To successfully run those tests the geckoview_example package needs to
+ // be installed on the device or emulator. After setting up the build
+ // environment (https://mzl.la/3muLv5M), the following mach commands have to
+ // be executed:
+ //
+ // $ ./mach build && ./mach install
+ //
+ // Currently the mozdevice API is not safe for multiple requests at the same
+ // time. It is recommended to run each of the unit tests on its own. Also adb
+ // specific tests cannot be run in CI yet. To check those locally, also run
+ // the ignored tests.
+ //
+ // Use the following command to accomplish that:
+ //
+ // $ cargo test -- --ignored --test-threads=1
+
+ use crate::android::AndroidHandler;
+ use crate::capabilities::AndroidOptions;
+ use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf};
+
+ fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) {
+ let options = AndroidOptions::new(package.to_owned(), storage);
+ let handler = AndroidHandler::new(&options, 4242, None).expect("has valid Android handler");
+
+ assert_eq!(handler.options, options);
+ assert_eq!(handler.process.package, package);
+
+ let expected_config_path = UnixPathBuf::from(format!(
+ "/data/local/tmp/{}-geckoview-config.yaml",
+ &package
+ ));
+ assert_eq!(handler.config, expected_config_path);
+
+ if handler.process.device.storage == AndroidStorage::App {
+ assert_eq!(
+ handler.process.device.run_as_package,
+ Some(package.to_owned())
+ );
+ } else {
+ assert_eq!(handler.process.device.run_as_package, None);
+ }
+
+ let test_root = match handler.process.device.storage {
+ AndroidStorage::App => {
+ let mut buf = UnixPathBuf::from("/data/data");
+ buf.push(&package);
+ buf.push("test_root");
+ buf
+ }
+ AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
+ AndroidStorage::Sdcard => {
+ let response = handler
+ .process
+ .device
+ .execute_host_shell_command("echo $EXTERNAL_STORAGE")
+ .unwrap();
+
+ let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
+ buf.push("Android/data/");
+ buf.push(&package);
+ buf.push("files/test_root");
+ buf
+ }
+ };
+ assert_eq!(handler.test_root, test_root);
+
+ let mut profile = test_root;
+ profile.push(format!("{}-geckodriver-profile", &package));
+ assert_eq!(handler.profile, profile);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_app() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::App);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_auto() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Auto);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_internal() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Internal);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_sdcard() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Sdcard);
+ }
+}
diff --git a/testing/geckodriver/src/browser.rs b/testing/geckodriver/src/browser.rs
new file mode 100644
index 0000000000..a33a755fa2
--- /dev/null
+++ b/testing/geckodriver/src/browser.rs
@@ -0,0 +1,556 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::android::AndroidHandler;
+use crate::capabilities::{FirefoxOptions, ProfileType};
+use crate::logging;
+use crate::prefs;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::{PrefFile, Profile};
+use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
+use std::fs;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use std::time;
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+
+/// A running Gecko instance.
+#[derive(Debug)]
+#[allow(clippy::large_enum_variant)]
+pub(crate) enum Browser {
+ Local(LocalBrowser),
+ Remote(RemoteBrowser),
+
+ /// An existing browser instance not controlled by GeckoDriver
+ Existing(u16),
+}
+
+impl Browser {
+ pub(crate) fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
+ match self {
+ Browser::Local(x) => x.close(wait_for_shutdown),
+ Browser::Remote(x) => x.close(),
+ Browser::Existing(_) => Ok(()),
+ }
+ }
+
+ pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ match self {
+ Browser::Local(x) => x.marionette_port(),
+ Browser::Remote(x) => x.marionette_port(),
+ Browser::Existing(x) => Ok(Some(*x)),
+ }
+ }
+
+ pub(crate) fn update_marionette_port(&mut self, port: u16) {
+ match self {
+ Browser::Local(x) => x.update_marionette_port(port),
+ Browser::Remote(x) => x.update_marionette_port(port),
+ Browser::Existing(x) => {
+ if port != *x {
+ error!(
+ "Cannot re-assign Marionette port when connected to an existing browser"
+ );
+ }
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+/// A local Firefox process, running on this (host) device.
+pub(crate) struct LocalBrowser {
+ marionette_port: u16,
+ prefs_backup: Option<PrefsBackup>,
+ process: FirefoxProcess,
+ profile_path: Option<PathBuf>,
+}
+
+impl LocalBrowser {
+ pub(crate) fn new(
+ options: FirefoxOptions,
+ marionette_port: u16,
+ jsdebugger: bool,
+ profile_root: Option<&Path>,
+ ) -> WebDriverResult<LocalBrowser> {
+ 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 = matches!(options.profile, ProfileType::Path(_));
+
+ let mut profile = match options.profile {
+ ProfileType::Named => None,
+ ProfileType::Path(x) => Some(x),
+ ProfileType::Temporary => Some(Profile::new(profile_root)?),
+ };
+
+ let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile {
+ let profile_path = profile.path.clone();
+ let prefs_backup = set_prefs(
+ marionette_port,
+ profile,
+ is_custom_profile,
+ options.prefs,
+ jsdebugger,
+ )
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+ (Some(profile_path), prefs_backup)
+ } else {
+ warn!("Unable to set geckodriver prefs when using a named profile");
+ (None, None)
+ };
+
+ let mut runner = FirefoxRunner::new(&binary, profile);
+
+ runner.arg("--marionette");
+ if 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 process = match runner.start() {
+ Ok(process) => process,
+ Err(e) => {
+ if let Some(backup) = prefs_backup {
+ backup.restore();
+ }
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to start browser {}: {}", binary.display(), e),
+ ));
+ }
+ };
+
+ Ok(LocalBrowser {
+ marionette_port,
+ prefs_backup,
+ process,
+ profile_path,
+ })
+ }
+
+ fn close(mut self, wait_for_shutdown: bool) -> WebDriverResult<()> {
+ if wait_for_shutdown {
+ // TODO(https://bugzil.la/1443922):
+ // Use toolkit.asyncshutdown.crash_timout pref
+ let duration = time::Duration::from_secs(70);
+ match self.process.wait(duration) {
+ Ok(x) => debug!("Browser process stopped: {}", x),
+ Err(e) => error!("Failed to stop browser process: {}", e),
+ }
+ }
+ self.process.kill()?;
+
+ // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
+ if let Some(prefs_backup) = self.prefs_backup {
+ prefs_backup.restore();
+ };
+
+ Ok(())
+ }
+
+ fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ if self.marionette_port != 0 {
+ return Ok(Some(self.marionette_port));
+ }
+
+ if let Some(profile_path) = self.profile_path.as_ref() {
+ return Ok(read_marionette_port(profile_path));
+ }
+
+ // This should be impossible, but it isn't enforced
+ Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Port not known when using named profile",
+ ))
+ }
+
+ fn update_marionette_port(&mut self, port: u16) {
+ self.marionette_port = port;
+ }
+
+ pub(crate) fn check_status(&mut self) -> Option<String> {
+ match self.process.try_wait() {
+ Ok(Some(status)) => Some(
+ status
+ .code()
+ .map(|c| c.to_string())
+ .unwrap_or_else(|| "signal".into()),
+ ),
+ Ok(None) => None,
+ Err(_) => Some("{unknown}".into()),
+ }
+ }
+}
+
+fn read_marionette_port(profile_path: &Path) -> Option<u16> {
+ let port_file = profile_path.join("MarionetteActivePort");
+ let mut port_str = String::with_capacity(6);
+ let mut file = match fs::File::open(&port_file) {
+ Ok(file) => file,
+ Err(_) => {
+ trace!("Failed to open {}", &port_file.to_string_lossy());
+ return None;
+ }
+ };
+ if let Err(e) = file.read_to_string(&mut port_str) {
+ trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e);
+ return None;
+ };
+ println!("Read port: {}", port_str);
+ let port = port_str.parse::<u16>().ok();
+ if port.is_none() {
+ warn!("Failed fo convert {} to u16", &port_str);
+ }
+ port
+}
+
+#[derive(Debug)]
+/// A remote instance, running on a (target) Android device.
+pub(crate) struct RemoteBrowser {
+ handler: AndroidHandler,
+ marionette_port: u16,
+ prefs_backup: Option<PrefsBackup>,
+}
+
+impl RemoteBrowser {
+ pub(crate) fn new(
+ options: FirefoxOptions,
+ marionette_port: u16,
+ websocket_port: Option<u16>,
+ profile_root: Option<&Path>,
+ ) -> WebDriverResult<RemoteBrowser> {
+ let android_options = options.android.unwrap();
+
+ let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?;
+
+ // Profile management.
+ let (mut profile, is_custom_profile) = match options.profile {
+ ProfileType::Named => {
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Cannot use a named profile on Android",
+ ));
+ }
+ ProfileType::Path(x) => (x, true),
+ ProfileType::Temporary => (Profile::new(profile_root)?, false),
+ };
+
+ let prefs_backup = set_prefs(
+ handler.marionette_target_port,
+ &mut profile,
+ is_custom_profile,
+ options.prefs,
+ false,
+ )
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+
+ handler.prepare(&profile, options.args, options.env.unwrap_or_default())?;
+
+ handler.launch()?;
+
+ Ok(RemoteBrowser {
+ handler,
+ marionette_port,
+ prefs_backup,
+ })
+ }
+
+ fn close(self) -> WebDriverResult<()> {
+ self.handler.force_stop()?;
+
+ // Restoring the prefs if the browser fails to stop perhaps doesn't work anyway
+ if let Some(prefs_backup) = self.prefs_backup {
+ prefs_backup.restore();
+ };
+
+ Ok(())
+ }
+
+ fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> {
+ Ok(Some(self.marionette_port))
+ }
+
+ fn update_marionette_port(&mut self, port: u16) {
+ self.marionette_port = port;
+ }
+}
+
+fn set_prefs(
+ port: u16,
+ profile: &mut Profile,
+ custom_profile: bool,
+ extra_prefs: Vec<(String, Pref)>,
+ js_debugger: bool,
+) -> WebDriverResult<Option<PrefsBackup>> {
+ let prefs = profile.user_prefs().map_err(|_| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Unable to read profile preferences file",
+ )
+ })?;
+
+ let backup_prefs = if custom_profile && prefs.path.exists() {
+ Some(PrefsBackup::new(prefs)?)
+ } else {
+ None
+ };
+
+ for &(name, ref value) in prefs::DEFAULT.iter() {
+ if !custom_profile || !prefs.contains_key(name) {
+ prefs.insert(name.to_string(), (*value).clone());
+ }
+ }
+
+ prefs.insert_slice(&extra_prefs[..]);
+
+ if js_debugger {
+ prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger"));
+ prefs.insert("devtools.debugger.remote-enabled", Pref::new(true));
+ prefs.insert("devtools.chrome.enabled", Pref::new(true));
+ prefs.insert("devtools.debugger.prompt-connection", Pref::new(false));
+ }
+
+ prefs.insert("marionette.port", Pref::new(port));
+ prefs.insert("remote.log.level", logging::max_level().into());
+
+ prefs.write().map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Unable to write Firefox profile: {}", e),
+ )
+ })?;
+ Ok(backup_prefs)
+}
+
+#[derive(Debug)]
+struct PrefsBackup {
+ orig_path: PathBuf,
+ backup_path: PathBuf,
+}
+
+impl PrefsBackup {
+ fn new(prefs: &PrefFile) -> WebDriverResult<PrefsBackup> {
+ let mut prefs_backup_path = prefs.path.clone();
+ let mut counter = 0;
+ while {
+ let ext = if counter > 0 {
+ format!("geckodriver_backup_{}", counter)
+ } else {
+ "geckodriver_backup".to_string()
+ };
+ prefs_backup_path.set_extension(ext);
+ prefs_backup_path.exists()
+ } {
+ counter += 1
+ }
+ debug!("Backing up prefs to {:?}", prefs_backup_path);
+ fs::copy(&prefs.path, &prefs_backup_path)?;
+
+ Ok(PrefsBackup {
+ orig_path: prefs.path.clone(),
+ backup_path: prefs_backup_path,
+ })
+ }
+
+ fn restore(self) {
+ if self.backup_path.exists() {
+ let _ = fs::rename(self.backup_path, self.orig_path);
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::set_prefs;
+ use crate::browser::read_marionette_port;
+ use crate::capabilities::{FirefoxOptions, ProfileType};
+ use base64::prelude::BASE64_STANDARD;
+ use base64::Engine;
+ use mozprofile::preferences::{Pref, PrefValue};
+ use mozprofile::profile::Profile;
+ use serde_json::{Map, Value};
+ use std::fs::File;
+ use std::io::{Read, Write};
+ use std::path::Path;
+ use tempfile::tempdir;
+
+ fn example_profile() -> Value {
+ let mut profile_data = Vec::with_capacity(1024);
+ let mut profile = File::open("src/tests/profile.zip").unwrap();
+ profile.read_to_end(&mut profile_data).unwrap();
+ Value::String(BASE64_STANDARD.encode(&profile_data))
+ }
+
+ // This is not a pretty test, mostly due to the nature of
+ // mozprofile's and MarionetteHandler's APIs, but we have had
+ // several regressions related to remote.log.level.
+ #[test]
+ fn test_remote_log_level() {
+ let mut profile = Profile::new(None).unwrap();
+ set_prefs(2828, &mut profile, false, vec![], false).ok();
+ let user_prefs = profile.user_prefs().unwrap();
+
+ let pref = user_prefs.get("remote.log.level").unwrap();
+ let value = match pref.value {
+ PrefValue::String(ref s) => s,
+ _ => panic!(),
+ };
+ for (i, ch) in value.chars().enumerate() {
+ if i == 0 {
+ assert!(ch.is_uppercase());
+ } else {
+ assert!(ch.is_lowercase());
+ }
+ }
+ }
+
+ #[test]
+ fn test_prefs() {
+ let marionette_settings = Default::default();
+
+ let encoded_profile = example_profile();
+ let mut prefs: Map<String, Value> = Map::new();
+ prefs.insert(
+ "browser.display.background_color".into(),
+ Value::String("#00ff00".into()),
+ );
+
+ let mut firefox_opts = Map::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+ firefox_opts.insert("prefs".into(), Value::Object(prefs));
+
+ let mut caps = Map::new();
+ caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
+
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid profile and prefs");
+
+ let mut profile = match opts.profile {
+ ProfileType::Path(profile) => profile,
+ _ => panic!("Expected ProfileType::Path"),
+ };
+
+ set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences");
+
+ let prefs_set = profile.user_prefs().expect("valid user preferences");
+ println!("{:#?}", prefs_set.prefs);
+
+ assert_eq!(
+ prefs_set.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS"))
+ );
+ assert_eq!(
+ prefs_set.get("browser.display.background_color"),
+ Some(&Pref::new("#00ff00"))
+ );
+ assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828)));
+ }
+
+ #[test]
+ fn test_pref_backup() {
+ let mut profile = Profile::new(None).unwrap();
+
+ // Create some prefs in the profile
+ let initial_prefs = profile.user_prefs().unwrap();
+ initial_prefs.insert("geckodriver.example", Pref::new("example"));
+ initial_prefs.write().unwrap();
+
+ let prefs_path = initial_prefs.path.clone();
+
+ let mut conflicting_backup_path = initial_prefs.path.clone();
+ conflicting_backup_path.set_extension("geckodriver_backup");
+ println!("{:?}", conflicting_backup_path);
+ let mut file = File::create(&conflicting_backup_path).unwrap();
+ file.write_all(b"test").unwrap();
+ assert!(conflicting_backup_path.exists());
+
+ let mut initial_prefs_data = String::new();
+ File::open(&prefs_path)
+ .expect("Initial prefs exist")
+ .read_to_string(&mut initial_prefs_data)
+ .unwrap();
+
+ let backup = set_prefs(2828, &mut profile, true, vec![], false)
+ .unwrap()
+ .unwrap();
+ let user_prefs = profile.user_prefs().unwrap();
+
+ assert!(user_prefs.path.exists());
+ let mut backup_path = user_prefs.path.clone();
+ backup_path.set_extension("geckodriver_backup_1");
+
+ assert!(backup_path.exists());
+
+ // Ensure the actual prefs contain both the existing ones and the ones we added
+ let pref = user_prefs.get("marionette.port").unwrap();
+ assert_eq!(pref.value, PrefValue::Int(2828));
+
+ let pref = user_prefs.get("geckodriver.example").unwrap();
+ assert_eq!(pref.value, PrefValue::String("example".into()));
+
+ // Ensure the backup prefs don't contain the new settings
+ let mut backup_data = String::new();
+ File::open(&backup_path)
+ .expect("Backup prefs exist")
+ .read_to_string(&mut backup_data)
+ .unwrap();
+ assert_eq!(backup_data, initial_prefs_data);
+
+ backup.restore();
+
+ assert!(!backup_path.exists());
+ let mut final_prefs_data = String::new();
+ File::open(&prefs_path)
+ .expect("Initial prefs exist")
+ .read_to_string(&mut final_prefs_data)
+ .unwrap();
+ assert_eq!(final_prefs_data, initial_prefs_data);
+ }
+
+ #[test]
+ fn test_local_read_marionette_port() {
+ fn create_port_file(profile_path: &Path, data: &[u8]) {
+ let port_path = profile_path.join("MarionetteActivePort");
+ let mut file = File::create(&port_path).unwrap();
+ file.write_all(data).unwrap();
+ }
+
+ let profile_dir = tempdir().unwrap();
+ let profile_path = profile_dir.path();
+ assert_eq!(read_marionette_port(profile_path), None);
+ assert_eq!(read_marionette_port(profile_path), None);
+ create_port_file(profile_path, b"");
+ assert_eq!(read_marionette_port(profile_path), None);
+ create_port_file(profile_path, b"1234");
+ assert_eq!(read_marionette_port(profile_path), Some(1234));
+ create_port_file(profile_path, b"1234abc");
+ assert_eq!(read_marionette_port(profile_path), None);
+ }
+}
diff --git a/testing/geckodriver/src/build.rs b/testing/geckodriver/src/build.rs
new file mode 100644
index 0000000000..f77a3363fa
--- /dev/null
+++ b/testing/geckodriver/src/build.rs
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde_json::Value;
+use std::fmt;
+
+include!(concat!(env!("OUT_DIR"), "/build-info.rs"));
+
+pub struct BuildInfo;
+
+impl BuildInfo {
+ pub fn version() -> &'static str {
+ crate_version!()
+ }
+
+ pub fn hash() -> Option<&'static str> {
+ COMMIT_HASH
+ }
+
+ pub fn date() -> Option<&'static str> {
+ COMMIT_DATE
+ }
+}
+
+impl fmt::Display for BuildInfo {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", BuildInfo::version())?;
+ match (BuildInfo::hash(), BuildInfo::date()) {
+ (Some(hash), Some(date)) => write!(f, " ({} {})", hash, date)?,
+ (Some(hash), None) => write!(f, " ({})", hash)?,
+ _ => {}
+ }
+ Ok(())
+ }
+}
+
+impl From<BuildInfo> for Value {
+ fn from(_: BuildInfo) -> Value {
+ Value::String(BuildInfo::version().to_string())
+ }
+}
+
+/// Returns build-time information about geckodriver.
+pub fn build_info() -> BuildInfo {
+ BuildInfo {}
+}
diff --git a/testing/geckodriver/src/capabilities.rs b/testing/geckodriver/src/capabilities.rs
new file mode 100644
index 0000000000..eda75ad62b
--- /dev/null
+++ b/testing/geckodriver/src/capabilities.rs
@@ -0,0 +1,1417 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::command::LogOptions;
+use crate::logging::Level;
+use crate::marionette::MarionetteSettings;
+use base64::prelude::BASE64_STANDARD;
+use base64::Engine;
+use mozdevice::AndroidStorageInput;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::firefox_args::{get_arg_value, parse_args, Arg};
+use mozrunner::runner::platform::firefox_default_path;
+use mozversion::{self, firefox_binary_version, firefox_version, Version};
+use regex::bytes::Regex;
+use serde_json::{Map, Value};
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::ffi::OsString;
+use std::fmt::{self, Display};
+use std::fs;
+use std::io;
+use std::io::BufWriter;
+use std::io::Cursor;
+use std::path::{Path, PathBuf};
+use std::str::{self, FromStr};
+use webdriver::capabilities::{BrowserCapabilities, Capabilities};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+
+#[derive(Clone, Debug)]
+enum VersionError {
+ VersionError(mozversion::Error),
+ MissingBinary,
+}
+
+impl From<mozversion::Error> for VersionError {
+ fn from(err: mozversion::Error) -> VersionError {
+ VersionError::VersionError(err)
+ }
+}
+
+impl Display for VersionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ VersionError::VersionError(ref x) => x.fmt(f),
+ VersionError::MissingBinary => "No binary provided".fmt(f),
+ }
+ }
+}
+
+impl From<VersionError> for WebDriverError {
+ fn from(err: VersionError) -> WebDriverError {
+ WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
+ }
+}
+
+/// Provides matching of `moz:firefoxOptions` and resolutionnized of which Firefox
+/// binary to use.
+///
+/// `FirefoxCapabilities` is constructed with the fallback binary, should
+/// `moz:firefoxOptions` not contain a binary entry. This may either be the
+/// system Firefox installation or an override, for example given to the
+/// `--binary` flag of geckodriver.
+pub struct FirefoxCapabilities<'a> {
+ pub chosen_binary: Option<PathBuf>,
+ fallback_binary: Option<&'a PathBuf>,
+ version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>,
+}
+
+impl<'a> FirefoxCapabilities<'a> {
+ pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
+ FirefoxCapabilities {
+ chosen_binary: None,
+ fallback_binary,
+ version_cache: BTreeMap::new(),
+ }
+ }
+
+ fn set_binary(&mut self, capabilities: &Map<String, Value>) {
+ self.chosen_binary = capabilities
+ .get("moz:firefoxOptions")
+ .and_then(|x| x.get("binary"))
+ .and_then(|x| x.as_str())
+ .map(PathBuf::from)
+ .or_else(|| self.fallback_binary.cloned())
+ .or_else(firefox_default_path);
+ }
+
+ fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> {
+ if let Some(binary) = binary {
+ if let Some(cache_value) = self.version_cache.get(binary) {
+ return cache_value.clone();
+ }
+ let rv = self
+ .version_from_ini(binary)
+ .or_else(|_| self.version_from_binary(binary));
+ if let Ok(ref version) = rv {
+ debug!("Found version {}", version);
+ } else {
+ debug!("Failed to get binary version");
+ }
+ self.version_cache.insert(binary.to_path_buf(), rv.clone());
+ rv
+ } else {
+ Err(VersionError::MissingBinary)
+ }
+ }
+
+ fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from ini files");
+ let version = firefox_version(binary)?;
+ if let Some(version_string) = version.version_string {
+ Version::from_str(&version_string).map_err(|err| err.into())
+ } else {
+ Err(VersionError::VersionError(
+ mozversion::Error::MetadataError("Missing version string".into()),
+ ))
+ }
+ }
+
+ fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from binary");
+ Ok(firefox_binary_version(binary)?)
+ }
+}
+
+impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
+ fn init(&mut self, capabilities: &Capabilities) {
+ self.set_binary(capabilities);
+ }
+
+ fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(Some("firefox".into()))
+ }
+
+ fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ let binary = self.chosen_binary.clone();
+ self.version(binary.as_ref().map(|x| x.as_ref()))
+ .map_err(|err| err.into())
+ .map(|x| Some(x.to_string()))
+ }
+
+ fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(if cfg!(target_os = "windows") {
+ Some("windows".into())
+ } else if cfg!(target_os = "macos") {
+ Some("mac".into())
+ } else if cfg!(target_os = "linux") {
+ Some("linux".into())
+ } else {
+ None
+ })
+ }
+
+ fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn compare_browser_version(
+ &mut self,
+ version: &str,
+ comparison: &str,
+ ) -> WebDriverResult<bool> {
+ Version::from_str(version)
+ .map_err(VersionError::from)?
+ .matches(comparison)
+ .map_err(|err| VersionError::from(err).into())
+ }
+
+ fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> {
+ if !name.starts_with("moz:") {
+ return Ok(());
+ }
+ match name {
+ "moz:firefoxOptions" => {
+ let data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "moz:firefoxOptions is not an object"
+ );
+ for (key, value) in data.iter() {
+ match &**key {
+ "androidActivity"
+ | "androidDeviceSerial"
+ | "androidPackage"
+ | "profile" => {
+ if !value.is_string() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "androidIntentArguments" | "args" => {
+ if !try_opt!(
+ value.as_array(),
+ ErrorStatus::InvalidArgument,
+ format!("{} is not an array", &**key)
+ )
+ .iter()
+ .all(|value| value.is_string())
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} entry is not a string", &**key),
+ ));
+ }
+ }
+ "binary" => {
+ if let Some(binary) = value.as_str() {
+ if !data.contains_key("androidPackage")
+ && self.version(Some(Path::new(binary))).is_err()
+ {
+ 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,
+ "Environment values were not all strings",
+ ));
+ }
+ }
+ "log" => {
+ let log_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "log value is not an object"
+ );
+ for (log_key, log_value) in log_data.iter() {
+ match &**log_key {
+ "level" => {
+ let level = try_opt!(
+ log_value.as_str(),
+ ErrorStatus::InvalidArgument,
+ "log level is not a string"
+ );
+ if Level::from_str(level).is_err() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Not a valid log level: {}", level),
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid log field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "prefs" => {
+ let prefs_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "prefs value is not an object"
+ );
+ 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",
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid moz:firefoxOptions field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "moz:useNonSpecCompliantPointerOrigin" => {
+ warn!("You are using the deprecated vendor specific capability 'moz:useNonSpecCompliantPointerOrigin', which will be removed in Firefox 116.");
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:useNonSpecCompliantPointerOrigin is not a boolean",
+ ));
+ }
+ }
+ "moz:webdriverClick" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:webdriverClick is not a boolean",
+ ));
+ }
+ }
+ "moz:debuggerAddress" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:debuggerAddress is not a boolean",
+ ));
+ }
+ }
+ _ => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Unrecognised option {}", name),
+ ))
+ }
+ }
+ Ok(())
+ }
+
+ fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+}
+
+/// 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,
+ pub storage: AndroidStorageInput,
+}
+
+impl AndroidOptions {
+ pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions {
+ AndroidOptions {
+ package,
+ storage,
+ ..Default::default()
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum ProfileType {
+ Path(Profile),
+ Named,
+ Temporary,
+}
+
+impl Default for ProfileType {
+ fn default() -> Self {
+ ProfileType::Temporary
+ }
+}
+
+/// Rust representation of `moz:firefoxOptions`.
+///
+/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
+/// the encoded profile, the binary arguments, log settings, and additional
+/// preferences to be checked and unmarshaled from the `moz:firefoxOptions`
+/// JSON Object into a Rust representation.
+#[derive(Default, Debug)]
+pub struct FirefoxOptions {
+ pub binary: Option<PathBuf>,
+ pub profile: ProfileType,
+ pub args: Option<Vec<String>>,
+ pub env: Option<Vec<(String, String)>>,
+ pub log: LogOptions,
+ pub prefs: Vec<(String, Pref)>,
+ pub android: Option<AndroidOptions>,
+ pub use_websocket: bool,
+}
+
+impl FirefoxOptions {
+ pub fn new() -> FirefoxOptions {
+ Default::default()
+ }
+
+ pub(crate) fn from_capabilities(
+ binary_path: Option<PathBuf>,
+ settings: &MarionetteSettings,
+ matched: &mut Capabilities,
+ ) -> WebDriverResult<FirefoxOptions> {
+ let mut rv = FirefoxOptions::new();
+ rv.binary = binary_path;
+
+ if let Some(json) = matched.remove("moz:firefoxOptions") {
+ let options = json.as_object().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "'moz:firefoxOptions' \
+ capability is not an object",
+ )
+ })?;
+
+ if options.get("androidPackage").is_some() && options.get("binary").is_some() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidPackage and binary are mutual exclusive",
+ ));
+ }
+
+ rv.android = FirefoxOptions::load_android(settings.android_storage, 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)?;
+ if let Some(profile) =
+ FirefoxOptions::load_profile(settings.profile_root.as_deref(), options)?
+ {
+ rv.profile = ProfileType::Path(profile);
+ }
+ }
+
+ if let Some(args) = rv.args.as_ref() {
+ let os_args = parse_args(args.iter().map(OsString::from).collect::<Vec<_>>().iter());
+
+ if let Some(path) = get_arg_value(os_args.iter(), Arg::Profile) {
+ if let ProfileType::Path(_) = rv.profile {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Can't provide both a --profile argument and a profile",
+ ));
+ }
+ let path_buf = PathBuf::from(path);
+ rv.profile = ProfileType::Path(Profile::new_from_path(&path_buf)?);
+ }
+
+ if get_arg_value(os_args.iter(), Arg::NamedProfile).is_some() {
+ if let ProfileType::Path(_) = rv.profile {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Can't provide both a -P argument and a profile",
+ ));
+ }
+ // See bug 1757720
+ warn!("Firefox was configured to use a named profile (`-P <name>`). \
+ Support for named profiles will be removed in a future geckodriver release. \
+ Please instead use the `--profile <path>` Firefox argument to start with an existing profile");
+ rv.profile = ProfileType::Named;
+ }
+
+ // Block these Firefox command line arguments that should not be settable
+ // via session capabilities.
+ if let Some(arg) = os_args
+ .iter()
+ .filter_map(|(opt_arg, _)| opt_arg.as_ref())
+ .find(|arg| {
+ matches!(
+ arg,
+ Arg::Marionette
+ | Arg::RemoteAllowHosts
+ | Arg::RemoteAllowOrigins
+ | Arg::RemoteDebuggingPort
+ )
+ })
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Argument {} can't be set via capabilities", arg),
+ ));
+ };
+ }
+
+ let has_web_socket_url = matched
+ .get("webSocketUrl")
+ .and_then(|x| x.as_bool())
+ .unwrap_or(false);
+
+ let has_debugger_address = matched
+ .remove("moz:debuggerAddress")
+ .and_then(|x| x.as_bool())
+ .unwrap_or(false);
+
+ // Set a command line provided port for the Remote Agent for now.
+ // It needs to be the same on the host and the Android device.
+ if has_web_socket_url || has_debugger_address {
+ rv.use_websocket = true;
+
+ // Bug 1722863: Setting of command line arguments would be
+ // better suited in the individual Browser implementations.
+ let mut remote_args = Vec::new();
+ remote_args.push("--remote-debugging-port".to_owned());
+ remote_args.push(settings.websocket_port.to_string());
+
+ // Handle additional hosts for WebDriver BiDi WebSocket connections
+ if !settings.allow_hosts.is_empty() {
+ remote_args.push("--remote-allow-hosts".to_owned());
+ remote_args.push(
+ settings
+ .allow_hosts
+ .iter()
+ .map(|host| host.to_string())
+ .collect::<Vec<String>>()
+ .join(","),
+ );
+ }
+
+ // Handle additional origins for WebDriver BiDi WebSocket connections
+ if !settings.allow_origins.is_empty() {
+ remote_args.push("--remote-allow-origins".to_owned());
+ remote_args.push(
+ settings
+ .allow_origins
+ .iter()
+ .map(|origin| origin.to_string())
+ .collect::<Vec<String>>()
+ .join(","),
+ );
+ }
+
+ if let Some(ref mut args) = rv.args {
+ args.append(&mut remote_args);
+ } else {
+ rv.args = Some(remote_args);
+ }
+ }
+
+ Ok(rv)
+ }
+
+ fn load_profile(
+ profile_root: Option<&Path>,
+ options: &Capabilities,
+ ) -> WebDriverResult<Option<Profile>> {
+ if let Some(profile_json) = options.get("profile") {
+ let profile_base64 = profile_json.as_str().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
+ })?;
+ let profile_zip = &*BASE64_STANDARD.decode(profile_base64)?;
+
+ // Create an emtpy profile directory
+ let profile = Profile::new(profile_root)?;
+ unzip_buffer(
+ profile_zip,
+ profile
+ .temp_dir
+ .as_ref()
+ .expect("Profile doesn't have a path")
+ .path(),
+ )?;
+
+ Ok(Some(profile))
+ } else {
+ Ok(None)
+ }
+ }
+
+ 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_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_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_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_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,
+ };
+
+ Ok(LogOptions { level })
+ } else {
+ Ok(Default::default())
+ }
+ }
+
+ 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_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)?));
+ }
+ Ok(rv)
+ } else {
+ Ok(vec![])
+ }
+ }
+
+ pub fn load_android(
+ storage: AndroidStorageInput,
+ 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.clone(), storage);
+
+ 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 => {
+ match package.as_str() {
+ "org.mozilla.firefox"
+ | "org.mozilla.firefox_beta"
+ | "org.mozilla.fenix"
+ | "org.mozilla.fenix.debug"
+ | "org.mozilla.reference.browser" => {
+ Some("org.mozilla.fenix.IntentReceiverActivity".to_string())
+ }
+ "org.mozilla.focus"
+ | "org.mozilla.focus.debug"
+ | "org.mozilla.klar"
+ | "org.mozilla.klar.debug" => {
+ Some("org.mozilla.focus.activity.IntentReceiverActivity".to_string())
+ }
+ // For all other applications fallback to auto-detection.
+ _ => 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 => {
+ // All GeckoView based applications support this view,
+ // and allow to open a blank page in a Gecko window.
+ Some(vec![
+ "-a".to_string(),
+ "android.intent.action.VIEW".to_string(),
+ "-d".to_string(),
+ "about:blank".to_string(),
+ ])
+ }
+ };
+
+ 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)),
+ _ => Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Could not convert pref value to string, boolean, or integer",
+ )),
+ }
+}
+
+fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
+ let reader = Cursor::new(buf);
+ let mut zip = zip::ZipArchive::new(reader)
+ .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?;
+
+ for i in 0..zip.len() {
+ let mut file = zip.by_index(i).map_err(|_| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Processing profile zip file failed",
+ )
+ })?;
+ let unzip_path = {
+ let name = file.name();
+ let is_dir = name.ends_with('/');
+ let rel_path = Path::new(name);
+ let dest_path = dest_dir.join(rel_path);
+
+ {
+ let create_dir = if is_dir {
+ Some(dest_path.as_path())
+ } else {
+ dest_path.parent()
+ };
+ if let Some(dir) = create_dir {
+ if !dir.exists() {
+ debug!("Creating profile directory tree {}", dir.to_string_lossy());
+ fs::create_dir_all(dir)?;
+ }
+ }
+ }
+
+ if is_dir {
+ None
+ } else {
+ Some(dest_path)
+ }
+ };
+
+ if let Some(unzip_path) = unzip_path {
+ debug!("Extracting profile to {}", unzip_path.to_string_lossy());
+ let dest = fs::File::create(unzip_path)?;
+ if file.size() > 0 {
+ let mut writer = BufWriter::new(dest);
+ io::copy(&mut file, &mut writer)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate mozprofile;
+
+ use self::mozprofile::preferences::Pref;
+ use super::*;
+ use serde_json::{json, Map, Value};
+ use std::fs::File;
+ use std::io::Read;
+ use url::{Host, Url};
+ use webdriver::capabilities::Capabilities;
+
+ fn example_profile() -> Value {
+ let mut profile_data = Vec::with_capacity(1024);
+ let mut profile = File::open("src/tests/profile.zip").unwrap();
+ profile.read_to_end(&mut profile_data).unwrap();
+ Value::String(BASE64_STANDARD.encode(&profile_data))
+ }
+
+ fn make_options(
+ firefox_opts: Capabilities,
+ marionette_settings: Option<MarionetteSettings>,
+ ) -> WebDriverResult<FirefoxOptions> {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
+
+ FirefoxOptions::from_capabilities(None, &marionette_settings.unwrap_or_default(), &mut caps)
+ }
+
+ #[test]
+ fn fx_options_default() {
+ let opts: FirefoxOptions = Default::default();
+ 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_empty_caps() {
+ let mut caps = Capabilities::new();
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("valid firefox options");
+ 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 marionette_settings = Default::default();
+
+ let opts = FirefoxOptions::from_capabilities(
+ Some(binary.clone()),
+ &marionette_settings,
+ &mut caps,
+ )
+ .expect("valid firefox options");
+ 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_blocked_firefox_arguments() {
+ let blocked_args = vec![
+ "--marionette",
+ "--remote-allow-hosts",
+ "--remote-allow-origins",
+ "--remote-debugging-port",
+ ];
+
+ for arg in blocked_args {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!([arg]));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_not_set() {
+ let mut caps = Capabilities::new();
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_false() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(false));
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_true() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let settings = MarionetteSettings {
+ websocket_port: 1234,
+ ..Default::default()
+ };
+ let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
+ assert_eq!(iter.next(), Some(&"1234".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_and_allow_hosts() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let mut marionette_settings: MarionetteSettings = Default::default();
+ marionette_settings.allow_hosts = vec![
+ Host::parse("foo").expect("host"),
+ Host::parse("bar").expect("host"),
+ ];
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
+ assert_eq!(iter.next(), Some(&"foo,bar".to_owned()));
+ assert!(!iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_and_allow_origins() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let mut marionette_settings: MarionetteSettings = Default::default();
+ marionette_settings.allow_origins = vec![
+ Url::parse("http://foo/").expect("url"),
+ Url::parse("http://bar/").expect("url"),
+ ];
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
+ assert_eq!(iter.next(), Some(&"http://foo/,http://bar/".to_owned()));
+ assert!(!iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_not_set() {
+ let caps = Capabilities::new();
+
+ let opts = make_options(caps, None).expect("valid firefox options");
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_false() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(false));
+
+ let opts = make_options(caps, None).expect("valid firefox options");
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_true() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(true));
+
+ let settings = MarionetteSettings {
+ websocket_port: 1234,
+ ..Default::default()
+ };
+ let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
+ assert_eq!(iter.next(), Some(&"1234".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_invalid_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), json!(42));
+
+ let marionette_settings = Default::default();
+ FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect_err("Firefox options need to be of type object");
+ }
+
+ #[test]
+ fn fx_options_android_package_and_binary() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo"));
+ firefox_opts.insert("binary".into(), json!("bar"));
+
+ make_options(firefox_opts, None)
+ .expect_err("androidPackage and binary are mutual exclusive");
+ }
+
+ #[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, None).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, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().package, 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, None).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, None).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_default_known_apps() {
+ let packages = vec![
+ "org.mozilla.firefox",
+ "org.mozilla.firefox_beta",
+ "org.mozilla.fenix",
+ "org.mozilla.fenix.debug",
+ "org.mozilla.focus",
+ "org.mozilla.focus.debug",
+ "org.mozilla.klar",
+ "org.mozilla.klar.debug",
+ "org.mozilla.reference.browser",
+ ];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert!(opts
+ .android
+ .unwrap()
+ .activity
+ .unwrap()
+ .contains("IntentReceiverActivity"));
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_default_unknown_apps() {
+ let packages = vec!["org.mozilla.geckoview_example", "com.some.other.app"];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, None);
+ }
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert(
+ "androidPackage".into(),
+ json!("org.mozilla.geckoview_example"),
+ );
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, None);
+ }
+
+ #[test]
+ fn fx_options_android_activity_override() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!("foo"));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, Some("foo".to_string()));
+ }
+
+ #[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, None).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, None).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, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().device_serial,
+ Some("cheese".to_string())
+ );
+ }
+
+ #[test]
+ fn fx_options_android_device_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, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_defaults() {
+ let packages = vec![
+ "org.mozilla.firefox",
+ "org.mozilla.firefox_beta",
+ "org.mozilla.fenix",
+ "org.mozilla.fenix.debug",
+ "org.mozilla.geckoview_example",
+ "org.mozilla.reference.browser",
+ "com.some.other.app",
+ ];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().intent_arguments,
+ Some(vec![
+ "-a".to_string(),
+ "android.intent.action.VIEW".to_string(),
+ "-d".to_string(),
+ "about:blank".to_string(),
+ ])
+ );
+ }
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_override() {
+ 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, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().intent_arguments,
+ Some(vec!["lorem".to_string(), "ipsum".to_string()])
+ );
+ }
+
+ #[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, None).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, None).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, None).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);
+
+ make_options(firefox_opts, None).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, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn test_profile() {
+ let encoded_profile = example_profile();
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ let mut profile = match opts.profile {
+ ProfileType::Path(profile) => profile,
+ _ => panic!("Expected ProfileType::Path"),
+ };
+ let prefs = profile.user_prefs().expect("valid preferences");
+
+ println!("{:#?}", prefs.prefs);
+
+ assert_eq!(
+ prefs.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS"))
+ );
+ }
+
+ #[test]
+ fn fx_options_args_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Path(_)));
+ }
+
+ #[test]
+ fn fx_options_args_named_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["-P", "foo"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Named));
+ }
+
+ #[test]
+ fn fx_options_args_no_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--headless"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Temporary));
+ }
+
+ #[test]
+ fn fx_options_args_profile_and_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
+ firefox_opts.insert("profile".into(), json!("foo"));
+
+ make_options(firefox_opts, None).expect_err("Invalid args");
+ }
+
+ #[test]
+ fn fx_options_args_p_and_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["-P"]));
+ firefox_opts.insert("profile".into(), json!("foo"));
+
+ make_options(firefox_opts, None).expect_err("Invalid args");
+ }
+}
diff --git a/testing/geckodriver/src/command.rs b/testing/geckodriver/src/command.rs
new file mode 100644
index 0000000000..c92eabf6f3
--- /dev/null
+++ b/testing/geckodriver/src/command.rs
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::logging;
+use base64::prelude::BASE64_STANDARD;
+use base64::Engine;
+use hyper::Method;
+use serde::de::{self, Deserialize, Deserializer};
+use serde_json::{self, Value};
+use std::env;
+use std::fs::File;
+use std::io::prelude::*;
+use uuid::Uuid;
+use webdriver::command::{WebDriverCommand, WebDriverExtensionCommand};
+use webdriver::error::WebDriverResult;
+use webdriver::httpapi::WebDriverExtensionRoute;
+use webdriver::Parameters;
+
+pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> {
+ vec![
+ (
+ Method::GET,
+ "/session/{sessionId}/moz/context",
+ GeckoExtensionRoute::GetContext,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/context",
+ GeckoExtensionRoute::SetContext,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/addon/install",
+ GeckoExtensionRoute::InstallAddon,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/addon/uninstall",
+ GeckoExtensionRoute::UninstallAddon,
+ ),
+ (
+ Method::GET,
+ "/session/{sessionId}/moz/screenshot/full",
+ GeckoExtensionRoute::TakeFullScreenshot,
+ ),
+ ]
+}
+
+#[derive(Clone, PartialEq, Eq)]
+pub enum GeckoExtensionRoute {
+ GetContext,
+ SetContext,
+ InstallAddon,
+ UninstallAddon,
+ TakeFullScreenshot,
+}
+
+impl WebDriverExtensionRoute for GeckoExtensionRoute {
+ type Command = GeckoExtensionCommand;
+
+ fn command(
+ &self,
+ _params: &Parameters,
+ body_data: &Value,
+ ) -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> {
+ use self::GeckoExtensionRoute::*;
+
+ let command = match *self {
+ GetContext => GeckoExtensionCommand::GetContext,
+ SetContext => {
+ GeckoExtensionCommand::SetContext(serde_json::from_value(body_data.clone())?)
+ }
+ InstallAddon => {
+ GeckoExtensionCommand::InstallAddon(serde_json::from_value(body_data.clone())?)
+ }
+ UninstallAddon => {
+ GeckoExtensionCommand::UninstallAddon(serde_json::from_value(body_data.clone())?)
+ }
+ TakeFullScreenshot => GeckoExtensionCommand::TakeFullScreenshot,
+ };
+
+ Ok(WebDriverCommand::Extension(command))
+ }
+}
+
+#[derive(Clone)]
+pub enum GeckoExtensionCommand {
+ GetContext,
+ SetContext(GeckoContextParameters),
+ InstallAddon(AddonInstallParameters),
+ UninstallAddon(AddonUninstallParameters),
+ TakeFullScreenshot,
+}
+
+impl WebDriverExtensionCommand for GeckoExtensionCommand {
+ fn parameters_json(&self) -> Option<Value> {
+ use self::GeckoExtensionCommand::*;
+ match self {
+ GetContext => None,
+ InstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
+ SetContext(x) => Some(serde_json::to_value(x).unwrap()),
+ UninstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
+ TakeFullScreenshot => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
+pub struct AddonInstallParameters {
+ pub path: String,
+ pub temporary: Option<bool>,
+}
+
+impl<'de> Deserialize<'de> for AddonInstallParameters {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Debug, Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct Base64 {
+ addon: String,
+ temporary: Option<bool>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct Path {
+ path: String,
+ temporary: Option<bool>,
+ }
+
+ #[derive(Debug, Deserialize)]
+ #[serde(untagged)]
+ enum Helper {
+ Base64(Base64),
+ Path(Path),
+ }
+
+ let params = match Helper::deserialize(deserializer)? {
+ Helper::Path(ref mut data) => AddonInstallParameters {
+ path: data.path.clone(),
+ temporary: data.temporary,
+ },
+ Helper::Base64(ref mut data) => {
+ let content = BASE64_STANDARD
+ .decode(&data.addon)
+ .map_err(de::Error::custom)?;
+
+ let path = env::temp_dir()
+ .as_path()
+ .join(format!("addon-{}.xpi", Uuid::new_v4()));
+ let mut xpi_file = File::create(&path).map_err(de::Error::custom)?;
+ xpi_file
+ .write(content.as_slice())
+ .map_err(de::Error::custom)?;
+
+ let path = match path.to_str() {
+ Some(path) => path.to_string(),
+ None => return Err(de::Error::custom("could not write addon to file")),
+ };
+
+ AddonInstallParameters {
+ path,
+ temporary: data.temporary,
+ }
+ }
+ };
+
+ Ok(params)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct AddonUninstallParameters {
+ pub id: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum GeckoContext {
+ Content,
+ Chrome,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct GeckoContextParameters {
+ pub context: GeckoContext,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct XblLocatorParameters {
+ pub name: String,
+ pub value: String,
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct LogOptions {
+ pub level: Option<logging::Level>,
+}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use super::*;
+ use crate::test::assert_de;
+
+ #[test]
+ 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_with_path_and_temporary() {
+ let params = AddonInstallParameters {
+ path: "/path/to.xpi".to_string(),
+ temporary: Some(true),
+ };
+ assert_de(&params, json!({"path": "/path/to.xpi", "temporary": true}));
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path() {
+ let params = AddonInstallParameters {
+ path: "/path/to.xpi".to_string(),
+ temporary: None,
+ };
+ assert_de(&params, json!({"path": "/path/to.xpi"}));
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path_invalid_type() {
+ 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 = 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 = 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();
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).unwrap();
+ assert_eq!(contents, "hello");
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon_only() {
+ 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();
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).unwrap();
+ assert_eq!(contents, "hello");
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon_invalid_type() {
+ 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 = 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 = 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 = 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_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 params = AddonUninstallParameters {
+ id: "foo".to_string(),
+ };
+ assert_de(&params, json!({"id": "foo"}));
+ }
+
+ #[test]
+ fn test_json_addon_uninstall_parameters_id_invalid_type() {
+ let json = json!({"id": true});
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_gecko_context_parameters_content() {
+ let params = GeckoContextParameters {
+ context: GeckoContext::Content,
+ };
+ assert_de(&params, json!({"context": "content"}));
+ }
+
+ #[test]
+ fn test_json_gecko_context_parameters_chrome() {
+ let params = GeckoContextParameters {
+ context: GeckoContext::Chrome,
+ };
+ assert_de(&params, json!({"context": "chrome"}));
+ }
+
+ #[test]
+ 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());
+ }
+}
diff --git a/testing/geckodriver/src/logging.rs b/testing/geckodriver/src/logging.rs
new file mode 100644
index 0000000000..073da956ed
--- /dev/null
+++ b/testing/geckodriver/src/logging.rs
@@ -0,0 +1,403 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! Gecko-esque logger implementation for the [`log`] crate.
+//!
+//! The [`log`] crate provides a single logging API that abstracts over the
+//! actual logging implementation. This module uses the logging API
+//! to provide a log implementation that shares many aesthetical traits with
+//! [Log.sys.mjs] from Gecko.
+//!
+//! Using the [`error!`], [`warn!`], [`info!`], [`debug!`], and
+//! [`trace!`] macros from `log` will output a timestamp field, followed by the
+//! log level, and then the message. The fields are separated by a tab
+//! character, making the output suitable for further text processing with
+//! `awk(1)`.
+//!
+//! This module shares the same API as `log`, except it provides additional
+//! entry functions [`init`] and [`init_with_level`] and additional log levels
+//! `Level::Fatal` and `Level::Config`. Converting these into the
+//! [`log::Level`] is lossy so that `Level::Fatal` becomes `log::Level::Error`
+//! and `Level::Config` becomes `log::Level::Debug`.
+//!
+//! [`log`]: https://docs.rs/log/newest/log/
+//! [Log.sys.mjs]: https://searchfox.org/mozilla-central/source/toolkit/modules/Log.sys.mjs
+//! [`error!`]: https://docs.rs/log/newest/log/macro.error.html
+//! [`warn!`]: https://docs.rs/log/newest/log/macro.warn.html
+//! [`info!`]: https://docs.rs/log/newest/log/macro.info.html
+//! [`debug!`]: https://docs.rs/log/newest/log/macro.debug.html
+//! [`trace!`]: https://docs.rs/log/newest/log/macro.trace.html
+//! [`init`]: fn.init.html
+//! [`init_with_level`]: fn.init_with_level.html
+
+use std::fmt;
+use std::io;
+use std::io::Write;
+use std::str;
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
+use unicode_segmentation::UnicodeSegmentation;
+
+use mozprofile::preferences::Pref;
+
+static LOG_TRUNCATE: AtomicBool = AtomicBool::new(true);
+static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0);
+
+const MAX_STRING_LENGTH: usize = 250;
+
+const LOGGED_TARGETS: &[&str] = &[
+ "geckodriver",
+ "mozdevice",
+ "mozprofile",
+ "mozrunner",
+ "mozversion",
+ "webdriver",
+];
+
+/// Logger levels from [Log.sys.mjs].
+///
+/// [Log.sys.mjs]: https://searchfox.org/mozilla-central/source/toolkit/modules/Log.sys.mjs
+#[repr(usize)]
+#[derive(Clone, Copy, Eq, Debug, Hash, PartialEq)]
+pub enum Level {
+ Fatal = 70,
+ Error = 60,
+ Warn = 50,
+ Info = 40,
+ Config = 30,
+ Debug = 20,
+ Trace = 10,
+}
+
+impl From<usize> for Level {
+ fn from(n: usize) -> Level {
+ use self::Level::*;
+ match n {
+ 70 => Fatal,
+ 60 => Error,
+ 50 => Warn,
+ 40 => Info,
+ 30 => Config,
+ 20 => Debug,
+ 10 => Trace,
+ _ => Info,
+ }
+ }
+}
+
+impl fmt::Display for Level {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use self::Level::*;
+ let s = match *self {
+ Fatal => "FATAL",
+ Error => "ERROR",
+ Warn => "WARN",
+ Info => "INFO",
+ Config => "CONFIG",
+ Debug => "DEBUG",
+ Trace => "TRACE",
+ };
+ write!(f, "{}", s)
+ }
+}
+
+impl str::FromStr for Level {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Level, ()> {
+ use self::Level::*;
+ match s.to_lowercase().as_ref() {
+ "fatal" => Ok(Fatal),
+ "error" => Ok(Error),
+ "warn" => Ok(Warn),
+ "info" => Ok(Info),
+ "config" => Ok(Config),
+ "debug" => Ok(Debug),
+ "trace" => Ok(Trace),
+ _ => Err(()),
+ }
+ }
+}
+
+impl From<Level> for log::Level {
+ fn from(level: Level) -> log::Level {
+ use self::Level::*;
+ match level {
+ Fatal | Error => log::Level::Error,
+ Warn => log::Level::Warn,
+ Info => log::Level::Info,
+ Config | Debug => log::Level::Debug,
+ Trace => log::Level::Trace,
+ }
+ }
+}
+
+impl From<Level> for Pref {
+ fn from(level: Level) -> Pref {
+ use self::Level::*;
+ Pref::new(match level {
+ Fatal => "Fatal",
+ Error => "Error",
+ Warn => "Warn",
+ Info => "Info",
+ Config => "Config",
+ Debug => "Debug",
+ Trace => "Trace",
+ })
+ }
+}
+
+impl From<log::Level> for Level {
+ fn from(log_level: log::Level) -> Level {
+ use log::Level::*;
+ match log_level {
+ Error => Level::Error,
+ Warn => Level::Warn,
+ Info => Level::Info,
+ Debug => Level::Debug,
+ Trace => Level::Trace,
+ }
+ }
+}
+
+struct Logger;
+
+impl log::Log for Logger {
+ fn enabled(&self, meta: &log::Metadata) -> bool {
+ LOGGED_TARGETS.iter().any(|&x| meta.target().starts_with(x))
+ && meta.level() <= log::max_level()
+ }
+
+ fn log(&self, record: &log::Record) {
+ if self.enabled(record.metadata()) {
+ if let Some((s1, s2)) = truncate_message(record.args()) {
+ println!(
+ "{}\t{}\t{}\t{} ... {}",
+ format_ts(chrono::Local::now()),
+ record.target(),
+ record.level(),
+ s1,
+ s2
+ );
+ } else {
+ println!(
+ "{}\t{}\t{}\t{}",
+ format_ts(chrono::Local::now()),
+ record.target(),
+ record.level(),
+ record.args()
+ )
+ }
+ }
+ }
+
+ fn flush(&self) {
+ io::stdout().flush().unwrap();
+ }
+}
+
+/// Initialises the logging subsystem with the default log level.
+pub fn init(truncate: bool) -> Result<(), log::SetLoggerError> {
+ init_with_level(Level::Info, truncate)
+}
+
+/// Initialises the logging subsystem.
+pub fn init_with_level(level: Level, truncate: bool) -> Result<(), log::SetLoggerError> {
+ let logger = Logger {};
+ set_max_level(level);
+ set_truncate(truncate);
+ log::set_boxed_logger(Box::new(logger))?;
+ Ok(())
+}
+
+/// Returns the current maximum log level.
+pub fn max_level() -> Level {
+ MAX_LOG_LEVEL.load(Ordering::Relaxed).into()
+}
+
+/// Sets the global maximum log level.
+pub fn set_max_level(level: Level) {
+ MAX_LOG_LEVEL.store(level as usize, Ordering::SeqCst);
+
+ let slevel: log::Level = level.into();
+ log::set_max_level(slevel.to_level_filter())
+}
+
+/// Sets the global maximum log level.
+pub fn set_truncate(truncate: bool) {
+ LOG_TRUNCATE.store(truncate, Ordering::SeqCst);
+}
+
+/// Returns the truncation flag.
+pub fn truncate() -> bool {
+ LOG_TRUNCATE.load(Ordering::Relaxed)
+}
+
+/// Produces a 13-digit Unix Epoch timestamp similar to Gecko.
+fn format_ts(ts: chrono::DateTime<chrono::Local>) -> String {
+ format!("{}{:03}", ts.timestamp(), ts.timestamp_subsec_millis())
+}
+
+/// Truncate a log message if it's too long
+fn truncate_message(args: &fmt::Arguments) -> Option<(String, String)> {
+ // Don't truncate the message if requested.
+ if !truncate() {
+ return None;
+ }
+
+ let message = format!("{}", args);
+ let chars = message.graphemes(true).collect::<Vec<&str>>();
+
+ if chars.len() > MAX_STRING_LENGTH {
+ let middle: usize = MAX_STRING_LENGTH / 2;
+ let s1 = chars[0..middle].concat();
+ let s2 = chars[chars.len() - middle..].concat();
+ Some((s1, s2))
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use std::str::FromStr;
+ use std::sync::Mutex;
+
+ use mozprofile::preferences::{Pref, PrefValue};
+
+ lazy_static! {
+ static ref LEVEL_MUTEX: Mutex<()> = Mutex::new(());
+ }
+
+ #[test]
+ fn test_level_repr() {
+ assert_eq!(Level::Fatal as usize, 70);
+ assert_eq!(Level::Error as usize, 60);
+ assert_eq!(Level::Warn as usize, 50);
+ assert_eq!(Level::Info as usize, 40);
+ assert_eq!(Level::Config as usize, 30);
+ assert_eq!(Level::Debug as usize, 20);
+ assert_eq!(Level::Trace as usize, 10);
+ }
+
+ #[test]
+ fn test_level_from_log() {
+ assert_eq!(Level::from(log::Level::Error), Level::Error);
+ assert_eq!(Level::from(log::Level::Warn), Level::Warn);
+ assert_eq!(Level::from(log::Level::Info), Level::Info);
+ assert_eq!(Level::from(log::Level::Debug), Level::Debug);
+ assert_eq!(Level::from(log::Level::Trace), Level::Trace);
+ }
+
+ #[test]
+ fn test_level_into_log() {
+ assert_eq!(Into::<log::Level>::into(Level::Fatal), log::Level::Error);
+ assert_eq!(Into::<log::Level>::into(Level::Error), log::Level::Error);
+ assert_eq!(Into::<log::Level>::into(Level::Warn), log::Level::Warn);
+ assert_eq!(Into::<log::Level>::into(Level::Info), log::Level::Info);
+ assert_eq!(Into::<log::Level>::into(Level::Config), log::Level::Debug);
+ assert_eq!(Into::<log::Level>::into(Level::Debug), log::Level::Debug);
+ assert_eq!(Into::<log::Level>::into(Level::Trace), log::Level::Trace);
+ }
+
+ #[test]
+ fn test_level_into_pref() {
+ let tests = [
+ (Level::Fatal, "Fatal"),
+ (Level::Error, "Error"),
+ (Level::Warn, "Warn"),
+ (Level::Info, "Info"),
+ (Level::Config, "Config"),
+ (Level::Debug, "Debug"),
+ (Level::Trace, "Trace"),
+ ];
+
+ for &(lvl, s) in tests.iter() {
+ let expected = Pref {
+ value: PrefValue::String(s.to_string()),
+ sticky: false,
+ };
+ assert_eq!(Into::<Pref>::into(lvl), expected);
+ }
+ }
+
+ #[test]
+ fn test_level_from_str() {
+ assert_eq!(Level::from_str("fatal"), Ok(Level::Fatal));
+ assert_eq!(Level::from_str("error"), Ok(Level::Error));
+ assert_eq!(Level::from_str("warn"), Ok(Level::Warn));
+ assert_eq!(Level::from_str("info"), Ok(Level::Info));
+ assert_eq!(Level::from_str("config"), Ok(Level::Config));
+ assert_eq!(Level::from_str("debug"), Ok(Level::Debug));
+ assert_eq!(Level::from_str("trace"), Ok(Level::Trace));
+
+ assert_eq!(Level::from_str("INFO"), Ok(Level::Info));
+
+ assert!(Level::from_str("foo").is_err());
+ }
+
+ #[test]
+ fn test_level_to_str() {
+ assert_eq!(Level::Fatal.to_string(), "FATAL");
+ assert_eq!(Level::Error.to_string(), "ERROR");
+ assert_eq!(Level::Warn.to_string(), "WARN");
+ assert_eq!(Level::Info.to_string(), "INFO");
+ assert_eq!(Level::Config.to_string(), "CONFIG");
+ assert_eq!(Level::Debug.to_string(), "DEBUG");
+ assert_eq!(Level::Trace.to_string(), "TRACE");
+ }
+
+ #[test]
+ fn test_max_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ set_max_level(Level::Info);
+ assert_eq!(max_level(), Level::Info);
+ }
+
+ #[test]
+ fn test_set_max_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ set_max_level(Level::Error);
+ assert_eq!(max_level(), Level::Error);
+ set_max_level(Level::Fatal);
+ assert_eq!(max_level(), Level::Fatal);
+ }
+
+ #[test]
+ fn test_init_with_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ init_with_level(Level::Debug, false).unwrap();
+ assert_eq!(max_level(), Level::Debug);
+ assert!(init_with_level(Level::Warn, false).is_err());
+ }
+
+ #[test]
+ fn test_format_ts() {
+ let ts = chrono::Local::now();
+ let s = format_ts(ts);
+ assert_eq!(s.len(), 13);
+ }
+
+ #[test]
+ fn test_truncate() {
+ let short_message = (0..MAX_STRING_LENGTH).map(|_| "x").collect::<String>();
+ // A message up to MAX_STRING_LENGTH is not truncated
+ assert_eq!(truncate_message(&format_args!("{}", short_message)), None);
+
+ let long_message = (0..MAX_STRING_LENGTH + 1).map(|_| "x").collect::<String>();
+ let part = (0..MAX_STRING_LENGTH / 2).map(|_| "x").collect::<String>();
+
+ // A message longer than MAX_STRING_LENGTH is not truncated if requested
+ set_truncate(false);
+ assert_eq!(truncate_message(&format_args!("{}", long_message)), None);
+
+ // A message longer than MAX_STRING_LENGTH is truncated if requested
+ set_truncate(true);
+ assert_eq!(
+ truncate_message(&format_args!("{}", long_message)),
+ Some((part.to_owned(), part))
+ );
+ }
+}
diff --git a/testing/geckodriver/src/main.rs b/testing/geckodriver/src/main.rs
new file mode 100644
index 0000000000..64df65a0d0
--- /dev/null
+++ b/testing/geckodriver/src/main.rs
@@ -0,0 +1,549 @@
+#![forbid(unsafe_code)]
+
+extern crate chrono;
+#[macro_use]
+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;
+extern crate regex;
+extern crate serde;
+#[macro_use]
+extern crate serde_derive;
+extern crate serde_json;
+extern crate serde_yaml;
+extern crate tempfile;
+extern crate url;
+extern crate uuid;
+extern crate webdriver;
+extern crate zip;
+
+#[macro_use]
+extern crate log;
+
+use std::env;
+use std::fmt;
+use std::io;
+use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
+use std::path::PathBuf;
+use std::result;
+use std::str::FromStr;
+
+use clap::{AppSettings, Arg, Command};
+
+macro_rules! try_opt {
+ ($expr:expr, $err_type:expr, $err_msg:expr) => {{
+ match $expr {
+ Some(x) => x,
+ None => return Err(WebDriverError::new($err_type, $err_msg)),
+ }
+ }};
+}
+
+mod android;
+mod browser;
+mod build;
+mod capabilities;
+mod command;
+mod logging;
+mod marionette;
+mod prefs;
+
+#[cfg(test)]
+pub mod test;
+
+use crate::command::extension_routes;
+use crate::logging::Level;
+use crate::marionette::{MarionetteHandler, MarionetteSettings};
+use mozdevice::AndroidStorageInput;
+use url::{Host, Url};
+
+const EXIT_SUCCESS: i32 = 0;
+const EXIT_USAGE: i32 = 64;
+const EXIT_UNAVAILABLE: i32 = 69;
+
+enum FatalError {
+ Parsing(clap::Error),
+ Usage(String),
+ Server(io::Error),
+}
+
+impl FatalError {
+ fn exit_code(&self) -> i32 {
+ use FatalError::*;
+ match *self {
+ Parsing(_) | Usage(_) => EXIT_USAGE,
+ Server(_) => EXIT_UNAVAILABLE,
+ }
+ }
+
+ fn help_included(&self) -> bool {
+ matches!(*self, FatalError::Parsing(_))
+ }
+}
+
+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),
+ };
+ 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>;
+
+#[allow(clippy::large_enum_variant)]
+enum Operation {
+ Help,
+ Version,
+ Server {
+ log_level: Option<Level>,
+ log_truncate: bool,
+ address: SocketAddr,
+ allow_hosts: Vec<Host>,
+ allow_origins: Vec<Url>,
+ settings: MarionetteSettings,
+ deprecated_storage_arg: bool,
+ },
+}
+
+/// Get a socket address from the provided host and port
+///
+/// # Arguments
+/// * `webdriver_host` - The hostname on which the server will listen
+/// * `webdriver_port` - The port on which the server will listen
+///
+/// When the host and port resolve to multiple addresses, prefer
+/// IPv4 addresses vs IPv6.
+fn server_address(webdriver_host: &str, webdriver_port: u16) -> ProgramResult<SocketAddr> {
+ let mut socket_addrs = match format!("{}:{}", webdriver_host, webdriver_port).to_socket_addrs()
+ {
+ Ok(addrs) => addrs.collect::<Vec<_>>(),
+ Err(e) => usage!("{}: {}:{}", e, webdriver_host, webdriver_port),
+ };
+ if socket_addrs.is_empty() {
+ usage!(
+ "Unable to resolve host: {}:{}",
+ webdriver_host,
+ webdriver_port
+ )
+ }
+ // Prefer ipv4 address
+ socket_addrs.sort_by(|a, b| {
+ let a_val = i32::from(!a.ip().is_ipv4());
+ let b_val = i32::from(!b.ip().is_ipv4());
+ a_val.partial_cmp(&b_val).expect("Comparison failed")
+ });
+ Ok(socket_addrs.remove(0))
+}
+
+/// Parse a given string into a Host
+fn parse_hostname(webdriver_host: &str) -> Result<Host, url::ParseError> {
+ let host_str = if let Ok(ip_addr) = IpAddr::from_str(webdriver_host) {
+ // In this case we have an IP address as the host
+ if ip_addr.is_ipv6() {
+ // Convert to quoted form
+ format!("[{}]", &webdriver_host)
+ } else {
+ webdriver_host.into()
+ }
+ } else {
+ webdriver_host.into()
+ };
+
+ Host::parse(&host_str)
+}
+
+/// Get a list of default hostnames to allow
+///
+/// This only covers domain names, not IP addresses, since IP adresses
+/// are always accepted.
+fn get_default_allowed_hosts(ip: IpAddr) -> Vec<Result<Host, url::ParseError>> {
+ let localhost_is_loopback = ("localhost".to_string(), 80)
+ .to_socket_addrs()
+ .map(|addr_iter| {
+ addr_iter
+ .map(|addr| addr.ip())
+ .filter(|ip| ip.is_loopback())
+ })
+ .iter()
+ .len()
+ > 0;
+ if ip.is_loopback() && localhost_is_loopback {
+ vec![Host::parse("localhost")]
+ } else {
+ vec![]
+ }
+}
+
+fn get_allowed_hosts(
+ host: Host,
+ allow_hosts: Option<clap::Values>,
+) -> Result<Vec<Host>, url::ParseError> {
+ allow_hosts
+ .map(|hosts| hosts.map(Host::parse).collect::<Vec<_>>())
+ .unwrap_or_else(|| match host {
+ Host::Domain(_) => {
+ vec![Ok(host.clone())]
+ }
+ Host::Ipv4(ip) => get_default_allowed_hosts(IpAddr::V4(ip)),
+ Host::Ipv6(ip) => get_default_allowed_hosts(IpAddr::V6(ip)),
+ })
+ .into_iter()
+ .collect::<Result<Vec<Host>, url::ParseError>>()
+}
+
+fn get_allowed_origins(allow_origins: Option<clap::Values>) -> Result<Vec<Url>, url::ParseError> {
+ allow_origins
+ .map(|origins| {
+ origins
+ .map(Url::parse)
+ .collect::<Result<Vec<Url>, url::ParseError>>()
+ })
+ .unwrap_or_else(|| Ok(vec![]))
+}
+
+fn parse_args(cmd: &mut Command) -> ProgramResult<Operation> {
+ let args = cmd.try_get_matches_from_mut(env::args())?;
+
+ if args.is_present("help") {
+ return Ok(Operation::Help);
+ } else if args.is_present("version") {
+ return Ok(Operation::Version);
+ }
+
+ let log_level = if args.is_present("log_level") {
+ Level::from_str(args.value_of("log_level").unwrap()).ok()
+ } else {
+ Some(match args.occurrences_of("verbosity") {
+ 0 => Level::Info,
+ 1 => Level::Debug,
+ _ => Level::Trace,
+ })
+ };
+
+ let webdriver_host = args.value_of("webdriver_host").unwrap();
+ let webdriver_port = {
+ let s = args.value_of("webdriver_port").unwrap();
+ match u16::from_str(s) {
+ Ok(n) => n,
+ Err(e) => usage!("invalid --port: {}: {}", e, s),
+ }
+ };
+
+ let android_storage = args
+ .value_of_t::<AndroidStorageInput>("android_storage")
+ .unwrap_or(AndroidStorageInput::Auto);
+
+ let binary = args.value_of("binary").map(PathBuf::from);
+
+ let profile_root = args.value_of("profile_root").map(PathBuf::from);
+
+ // Try to create a temporary directory on startup to check that the directory exists and is writable
+ {
+ let tmp_dir = if let Some(ref tmp_root) = profile_root {
+ tempfile::tempdir_in(tmp_root)
+ } else {
+ tempfile::tempdir()
+ };
+ if tmp_dir.is_err() {
+ usage!("Unable to write to temporary directory; consider --profile-root with a writeable directory")
+ }
+ }
+
+ let marionette_host = args.value_of("marionette_host").unwrap();
+ let marionette_port = match args.value_of("marionette_port") {
+ Some(s) => match u16::from_str(s) {
+ Ok(n) => Some(n),
+ Err(e) => usage!("invalid --marionette-port: {}", e),
+ },
+ None => None,
+ };
+
+ // For Android the port on the device must be the same as the one on the
+ // host. For now default to 9222, which is the default for --remote-debugging-port.
+ let websocket_port = match args.value_of("websocket_port") {
+ Some(s) => match u16::from_str(s) {
+ Ok(n) => n,
+ Err(e) => usage!("invalid --websocket-port: {}", e),
+ },
+ None => 9222,
+ };
+
+ let host = match parse_hostname(webdriver_host) {
+ Ok(name) => name,
+ Err(e) => usage!("invalid --host {}: {}", webdriver_host, e),
+ };
+
+ let allow_hosts = match get_allowed_hosts(host, args.values_of("allow_hosts")) {
+ Ok(hosts) => hosts,
+ Err(e) => usage!("invalid --allow-hosts {}", e),
+ };
+
+ let allow_origins = match get_allowed_origins(args.values_of("allow_origins")) {
+ Ok(origins) => origins,
+ Err(e) => usage!("invalid --allow-origins {}", e),
+ };
+
+ let address = server_address(webdriver_host, webdriver_port)?;
+
+ let settings = MarionetteSettings {
+ binary,
+ profile_root,
+ connect_existing: args.is_present("connect_existing"),
+ host: marionette_host.into(),
+ port: marionette_port,
+ websocket_port,
+ allow_hosts: allow_hosts.clone(),
+ allow_origins: allow_origins.clone(),
+ jsdebugger: args.is_present("jsdebugger"),
+ android_storage,
+ };
+ Ok(Operation::Server {
+ log_level,
+ log_truncate: !args.is_present("log_no_truncate"),
+ allow_hosts,
+ allow_origins,
+ address,
+ settings,
+ deprecated_storage_arg: args.is_present("android_storage"),
+ })
+}
+
+fn inner_main(cmd: &mut Command) -> ProgramResult<()> {
+ match parse_args(cmd)? {
+ Operation::Help => print_help(cmd),
+ Operation::Version => print_version(),
+
+ Operation::Server {
+ log_level,
+ log_truncate,
+ address,
+ allow_hosts,
+ allow_origins,
+ settings,
+ deprecated_storage_arg,
+ } => {
+ if let Some(ref level) = log_level {
+ logging::init_with_level(*level, log_truncate).unwrap();
+ } else {
+ logging::init(log_truncate).unwrap();
+ }
+
+ if deprecated_storage_arg {
+ warn!("--android-storage argument is deprecated and will be removed soon.");
+ };
+
+ let handler = MarionetteHandler::new(settings);
+ let listening = webdriver::server::start(
+ address,
+ allow_hosts,
+ allow_origins,
+ handler,
+ extension_routes(),
+ )?;
+ info!("Listening on {}", listening.socket);
+ }
+ }
+
+ Ok(())
+}
+
+fn main() {
+ use std::process::exit;
+
+ let mut cmd = make_command();
+
+ // use std::process:Termination when it graduates
+ exit(match inner_main(&mut cmd) {
+ Ok(_) => EXIT_SUCCESS,
+
+ Err(e) => {
+ eprintln!("{}: {}", get_program_name(), e);
+ if !e.help_included() {
+ print_help(&mut cmd);
+ }
+
+ e.exit_code()
+ }
+ });
+}
+
+fn make_command<'a>() -> Command<'a> {
+ Command::new(format!("geckodriver {}", build::build_info()))
+ .setting(AppSettings::NoAutoHelp)
+ .setting(AppSettings::NoAutoVersion)
+ .about("WebDriver implementation for Firefox")
+ .arg(
+ Arg::new("webdriver_host")
+ .long("host")
+ .takes_value(true)
+ .value_name("HOST")
+ .default_value("127.0.0.1")
+ .help("Host IP to use for WebDriver server"),
+ )
+ .arg(
+ Arg::new("webdriver_port")
+ .short('p')
+ .long("port")
+ .takes_value(true)
+ .value_name("PORT")
+ .default_value("4444")
+ .help("Port to use for WebDriver server"),
+ )
+ .arg(
+ Arg::new("binary")
+ .short('b')
+ .long("binary")
+ .takes_value(true)
+ .value_name("BINARY")
+ .help("Path to the Firefox binary"),
+ )
+ .arg(
+ Arg::new("marionette_host")
+ .long("marionette-host")
+ .takes_value(true)
+ .value_name("HOST")
+ .default_value("127.0.0.1")
+ .help("Host to use to connect to Gecko"),
+ )
+ .arg(
+ Arg::new("marionette_port")
+ .long("marionette-port")
+ .takes_value(true)
+ .value_name("PORT")
+ .help("Port to use to connect to Gecko [default: system-allocated port]"),
+ )
+ .arg(
+ Arg::new("websocket_port")
+ .long("websocket-port")
+ .takes_value(true)
+ .value_name("PORT")
+ .conflicts_with("connect_existing")
+ .help("Port to use to connect to WebDriver BiDi [default: 9222]"),
+ )
+ .arg(
+ Arg::new("connect_existing")
+ .long("connect-existing")
+ .requires("marionette_port")
+ .help("Connect to an existing Firefox instance"),
+ )
+ .arg(
+ Arg::new("jsdebugger")
+ .long("jsdebugger")
+ .help("Attach browser toolbox debugger for Firefox"),
+ )
+ .arg(
+ Arg::new("verbosity")
+ .multiple_occurrences(true)
+ .conflicts_with("log_level")
+ .short('v')
+ .help("Log level verbosity (-v for debug and -vv for trace level)"),
+ )
+ .arg(
+ Arg::new("log_level")
+ .long("log")
+ .takes_value(true)
+ .value_name("LEVEL")
+ .possible_values(["fatal", "error", "warn", "info", "config", "debug", "trace"])
+ .help("Set Gecko log level"),
+ )
+ .arg(
+ Arg::new("log_no_truncate")
+ .long("log-no-truncate")
+ .help("Disable truncation of long log lines"),
+ )
+ .arg(
+ Arg::new("help")
+ .short('h')
+ .long("help")
+ .help("Prints this message"),
+ )
+ .arg(
+ Arg::new("version")
+ .short('V')
+ .long("version")
+ .help("Prints version and copying information"),
+ )
+ .arg(
+ Arg::new("profile_root")
+ .long("profile-root")
+ .takes_value(true)
+ .value_name("PROFILE_ROOT")
+ .help("Directory in which to create profiles. Defaults to the system temporary directory."),
+ )
+ .arg(
+ Arg::new("android_storage")
+ .long("android-storage")
+ .possible_values(["auto", "app", "internal", "sdcard"])
+ .value_name("ANDROID_STORAGE")
+ .help("Selects storage location to be used for test data (deprecated)."),
+ )
+ .arg(
+ Arg::new("allow_hosts")
+ .long("allow-hosts")
+ .takes_value(true)
+ .multiple_values(true)
+ .value_name("ALLOW_HOSTS")
+ .help("List of hostnames to allow. By default the value of --host is allowed, and in addition if that's a well known local address, other variations on well known local addresses are allowed. If --allow-hosts is provided only exactly those hosts are allowed."),
+ )
+ .arg(
+ Arg::new("allow_origins")
+ .long("allow-origins")
+ .takes_value(true)
+ .multiple_values(true)
+ .value_name("ALLOW_ORIGINS")
+ .help("List of request origins to allow. These must be formatted as scheme://host:port. By default any request with an origin header is rejected. If --allow-origins is provided then only exactly those origins are allowed."),
+ )
+}
+
+fn get_program_name() -> String {
+ env::args().next().unwrap()
+}
+
+fn print_help(cmd: &mut Command) {
+ cmd.print_help().ok();
+ println!();
+}
+
+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/testing/geckodriver/src/marionette.rs b/testing/geckodriver/src/marionette.rs
new file mode 100644
index 0000000000..836382b7db
--- /dev/null
+++ b/testing/geckodriver/src/marionette.rs
@@ -0,0 +1,1623 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::browser::{Browser, LocalBrowser, RemoteBrowser};
+use crate::build;
+use crate::capabilities::{FirefoxCapabilities, FirefoxOptions, ProfileType};
+use crate::command::{
+ AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters,
+ GeckoExtensionCommand, GeckoExtensionRoute,
+};
+use crate::logging;
+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, 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 mozdevice::AndroidStorageInput;
+use serde::de::{self, Deserialize, Deserializer};
+use serde::ser::{Serialize, Serializer};
+use serde_json::{self, Map, Value};
+use std::io::prelude::*;
+use std::io::Error as IoError;
+use std::io::ErrorKind;
+use std::io::Result as IoResult;
+use std::net::{Shutdown, TcpListener, TcpStream};
+use std::path::PathBuf;
+use std::sync::Mutex;
+use std::thread;
+use std::time;
+use url::{Host, Url};
+use webdriver::capabilities::BrowserCapabilities;
+use webdriver::command::WebDriverCommand::{
+ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
+ ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
+ FindElement, FindElementElement, FindElementElements, FindElements, FindShadowRootElement,
+ FindShadowRootElements, FullscreenWindow, Get, GetActiveElement, GetAlertText, GetCSSValue,
+ GetComputedLabel, GetComputedRole, GetCookies, GetCurrentUrl, GetElementAttribute,
+ GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie,
+ GetPageSource, GetShadowRoot, 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, Date, FrameId, LocatorStrategy, ShadowRoot, WebElement, ELEMENT_KEY, FRAME_KEY,
+ SHADOW_KEY, WINDOW_KEY,
+};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+use webdriver::response::{
+ CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse,
+ NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse,
+};
+use webdriver::server::{Session, WebDriverHandler};
+use webdriver::{capabilities::CapabilitiesMatching, server::SessionTeardownKind};
+
+#[derive(Debug, PartialEq, Deserialize)]
+struct MarionetteHandshake {
+ #[serde(rename = "marionetteProtocol")]
+ protocol: u16,
+ #[serde(rename = "applicationType")]
+ application_type: String,
+}
+
+#[derive(Default)]
+pub(crate) struct MarionetteSettings {
+ pub(crate) binary: Option<PathBuf>,
+ pub(crate) profile_root: Option<PathBuf>,
+ pub(crate) connect_existing: bool,
+ pub(crate) host: String,
+ pub(crate) port: Option<u16>,
+ pub(crate) websocket_port: u16,
+ pub(crate) allow_hosts: Vec<Host>,
+ pub(crate) allow_origins: Vec<Url>,
+
+ /// Brings up the Browser Toolbox when starting Firefox,
+ /// letting you debug internals.
+ pub(crate) jsdebugger: bool,
+
+ pub(crate) android_storage: AndroidStorageInput,
+}
+
+#[derive(Default)]
+pub(crate) struct MarionetteHandler {
+ connection: Mutex<Option<MarionetteConnection>>,
+ settings: MarionetteSettings,
+}
+
+impl MarionetteHandler {
+ pub(crate) fn new(settings: MarionetteSettings) -> MarionetteHandler {
+ MarionetteHandler {
+ connection: Mutex::new(None),
+ settings,
+ }
+ }
+
+ fn create_connection(
+ &self,
+ session_id: Option<String>,
+ new_session_parameters: &NewSessionParameters,
+ ) -> WebDriverResult<MarionetteConnection> {
+ let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
+ let (capabilities, options) = {
+ let mut capabilities = new_session_parameters
+ .match_browser(&mut fx_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.clone(),
+ &self.settings,
+ &mut capabilities,
+ )?;
+ (capabilities, options)
+ };
+
+ if let Some(l) = options.log.level {
+ logging::set_max_level(l);
+ }
+
+ let marionette_host = self.settings.host.to_owned();
+ let marionette_port = match self.settings.port {
+ Some(port) => port,
+ None => {
+ // If we're launching Firefox Desktop version 95 or later, and there's no port
+ // specified, we can pass 0 as the port and later read it back from
+ // the profile.
+ let can_use_profile: bool = options.android.is_none()
+ && options.profile != ProfileType::Named
+ && !self.settings.connect_existing
+ && fx_capabilities
+ .browser_version(&capabilities)
+ .map(|opt_v| {
+ opt_v
+ .map(|v| {
+ fx_capabilities
+ .compare_browser_version(&v, ">=95")
+ .unwrap_or(false)
+ })
+ .unwrap_or(false)
+ })
+ .unwrap_or(false);
+ if can_use_profile {
+ 0
+ } else {
+ get_free_port(&marionette_host)?
+ }
+ }
+ };
+
+ let websocket_port = if options.use_websocket {
+ Some(self.settings.websocket_port)
+ } else {
+ None
+ };
+
+ let browser = if options.android.is_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",
+ ));
+ }
+ Browser::Remote(RemoteBrowser::new(
+ options,
+ marionette_port,
+ websocket_port,
+ self.settings.profile_root.as_deref(),
+ )?)
+ } else if !self.settings.connect_existing {
+ Browser::Local(LocalBrowser::new(
+ options,
+ marionette_port,
+ self.settings.jsdebugger,
+ self.settings.profile_root.as_deref(),
+ )?)
+ } else {
+ Browser::Existing(marionette_port)
+ };
+ let session = MarionetteSession::new(session_id, capabilities);
+ MarionetteConnection::new(marionette_host, browser, session)
+ }
+
+ fn close_connection(&mut self, wait_for_shutdown: bool) {
+ if let Ok(connection) = self.connection.get_mut() {
+ if let Some(conn) = connection.take() {
+ if let Err(e) = conn.close(wait_for_shutdown) {
+ error!("Failed to close browser connection: {}", e)
+ }
+ }
+ }
+ }
+}
+
+impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
+ fn handle_command(
+ &mut self,
+ _: &Option<Session>,
+ msg: WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<WebDriverResponse> {
+ // First handle the status message which doesn't actually require a marionette
+ // connection or message
+ if let Status = msg.command {
+ let (ready, message) = self
+ .connection
+ .get_mut()
+ .map(|ref connection| {
+ connection
+ .as_ref()
+ .map(|_| (false, "Session already started"))
+ .unwrap_or((true, ""))
+ })
+ .unwrap_or((false, "geckodriver internal error"));
+ let mut value = Map::new();
+ value.insert("ready".to_string(), Value::Bool(ready));
+ value.insert("message".to_string(), Value::String(message.into()));
+ return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object(
+ value,
+ ))));
+ }
+
+ match self.connection.lock() {
+ Ok(mut connection) => {
+ if connection.is_none() {
+ if let NewSession(ref capabilities) = msg.command {
+ let conn = self.create_connection(msg.session_id.clone(), capabilities)?;
+ *connection = Some(conn);
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidSessionId,
+ "Tried to run command without establishing a connection",
+ ));
+ }
+ }
+ let conn = connection.as_mut().expect("Missing connection");
+ conn.send_command(&msg).map_err(|mut err| {
+ // Shutdown the browser if no session can
+ // be established due to errors.
+ if let NewSession(_) = msg.command {
+ err.delete_session = true;
+ }
+ err
+ })
+ }
+ Err(_) => Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to aquire Marionette connection",
+ )),
+ }
+ }
+
+ fn teardown_session(&mut self, kind: SessionTeardownKind) {
+ let wait_for_shutdown = match kind {
+ SessionTeardownKind::Deleted => true,
+ SessionTeardownKind::NotDeleted => false,
+ };
+ self.close_connection(wait_for_shutdown);
+ }
+}
+
+impl Drop for MarionetteHandler {
+ fn drop(&mut self) {
+ self.close_connection(false);
+ }
+}
+
+struct MarionetteSession {
+ session_id: String,
+ capabilities: Map<String, Value>,
+ command_id: MessageId,
+}
+
+impl MarionetteSession {
+ fn new(session_id: Option<String>, capabilities: Map<String, Value>) -> MarionetteSession {
+ let initital_id = session_id.unwrap_or_default();
+ MarionetteSession {
+ session_id: initital_id,
+ capabilities,
+ command_id: 0,
+ }
+ }
+
+ fn update(
+ &mut self,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: &MarionetteResponse,
+ ) -> WebDriverResult<()> {
+ if let NewSession(_) = msg.command {
+ let session_id = try_opt!(
+ try_opt!(
+ resp.result.get("sessionId"),
+ ErrorStatus::SessionNotCreated,
+ "Unable to get session id"
+ )
+ .as_str(),
+ ErrorStatus::SessionNotCreated,
+ "Unable to convert session id to string"
+ );
+ self.session_id = session_id.to_string();
+ };
+ Ok(())
+ }
+
+ /// Converts a Marionette JSON response into a `WebElement`.
+ ///
+ /// Note that it currently coerces all chrome elements, web frames, and web
+ /// windows also into web elements. This will change at a later point.
+ fn to_web_element(&self, json_data: &Value) -> WebDriverResult<WebElement> {
+ let data = try_opt!(
+ json_data.as_object(),
+ ErrorStatus::UnknownError,
+ "Failed to convert data to an object"
+ );
+
+ let element = data.get(ELEMENT_KEY);
+ let frame = data.get(FRAME_KEY);
+ let window = data.get(WINDOW_KEY);
+
+ let value = try_opt!(
+ element.or(frame).or(window),
+ ErrorStatus::UnknownError,
+ "Failed to extract web element from Marionette response"
+ );
+ let id = try_opt!(
+ value.as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to convert web element reference value to string"
+ )
+ .to_string();
+ Ok(WebElement(id))
+ }
+
+ /// Converts a Marionette JSON response into a `ShadowRoot`.
+ fn to_shadow_root(&self, json_data: &Value) -> WebDriverResult<ShadowRoot> {
+ let data = try_opt!(
+ json_data.as_object(),
+ ErrorStatus::UnknownError,
+ "Failed to convert data to an object"
+ );
+
+ let shadow_root = data.get(SHADOW_KEY);
+
+ let value = try_opt!(
+ shadow_root,
+ ErrorStatus::UnknownError,
+ "Failed to extract shadow root from Marionette response"
+ );
+ let id = try_opt!(
+ value.as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to convert shadow root reference value to string"
+ )
+ .to_string();
+ Ok(ShadowRoot(id))
+ }
+
+ fn next_command_id(&mut self) -> MessageId {
+ self.command_id += 1;
+ self.command_id
+ }
+
+ fn response(
+ &mut self,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: MarionetteResponse,
+ ) -> WebDriverResult<WebDriverResponse> {
+ use self::GeckoExtensionCommand::*;
+
+ if resp.id != self.command_id {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!(
+ "Marionette responses arrived out of sequence, expected {}, got {}",
+ self.command_id, resp.id
+ ),
+ ));
+ }
+
+ if let Some(error) = resp.error {
+ return Err(error.into());
+ }
+
+ self.update(msg, &resp)?;
+
+ Ok(match msg.command {
+ // Everything that doesn't have a response value
+ Get(_)
+ | GoBack
+ | GoForward
+ | Refresh
+ | SetTimeouts(_)
+ | SwitchToWindow(_)
+ | SwitchToFrame(_)
+ | SwitchToParentFrame
+ | AddCookie(_)
+ | DeleteCookies
+ | DeleteCookie(_)
+ | DismissAlert
+ | AcceptAlert
+ | SendAlertText(_)
+ | ElementClick(_)
+ | ElementClear(_)
+ | ElementSendKeys(_, _)
+ | PerformActions(_)
+ | ReleaseActions => WebDriverResponse::Void,
+ // Things that simply return the contents of the marionette "value" property
+ GetCurrentUrl
+ | GetTitle
+ | GetPageSource
+ | GetWindowHandle
+ | IsDisplayed(_)
+ | IsSelected(_)
+ | GetElementAttribute(_, _)
+ | GetElementProperty(_, _)
+ | GetCSSValue(_, _)
+ | GetElementText(_)
+ | GetElementTagName(_)
+ | GetComputedLabel(_)
+ | GetComputedRole(_)
+ | IsEnabled(_)
+ | ExecuteScript(_)
+ | ExecuteAsyncScript(_)
+ | GetAlertText
+ | TakeScreenshot
+ | Print(_)
+ | TakeElementScreenshot(_) => {
+ WebDriverResponse::Generic(resp.into_value_response(true)?)
+ }
+ GetTimeouts => {
+ let script = match try_opt!(
+ resp.result.get("script"),
+ 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"
+ ),
+ };
+ let page_load = try_opt!(
+ try_opt!(
+ resp.result.get("pageLoad"),
+ ErrorStatus::UnknownError,
+ "Missing field: pageLoad"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret page load duration as u64"
+ );
+ let implicit = try_opt!(
+ try_opt!(
+ resp.result.get("implicit"),
+ ErrorStatus::UnknownError,
+ "Missing field: implicit"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret implicit search duration as u64"
+ );
+
+ WebDriverResponse::Timeouts(TimeoutsResponse {
+ script,
+ page_load,
+ implicit,
+ })
+ }
+ Status => panic!("Got status command that should already have been handled"),
+ 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(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret handle as string"
+ )
+ .into();
+ let typ: String = try_opt!(
+ try_opt!(
+ resp.result.get("type"),
+ ErrorStatus::UnknownError,
+ "Failed to find type field"
+ )
+ .as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret type as string"
+ )
+ .into();
+
+ WebDriverResponse::NewWindow(NewWindowResponse { handle, typ })
+ }
+ CloseWindow => {
+ let data = try_opt!(
+ resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array"
+ );
+ let handles = data
+ .iter()
+ .map(|x| {
+ Ok(try_opt!(
+ x.as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret window handle as string"
+ )
+ .to_owned())
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ WebDriverResponse::CloseWindow(CloseWindowResponse(handles))
+ }
+ GetElementRect(_) => {
+ let x = try_opt!(
+ try_opt!(
+ resp.result.get("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as float"
+ );
+
+ let y = try_opt!(
+ try_opt!(
+ resp.result.get("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as float"
+ );
+
+ let width = try_opt!(
+ try_opt!(
+ resp.result.get("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float"
+ );
+
+ let height = try_opt!(
+ try_opt!(
+ resp.result.get("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float"
+ );
+
+ let rect = ElementRectResponse {
+ x,
+ y,
+ width,
+ height,
+ };
+ WebDriverResponse::ElementRect(rect)
+ }
+ FullscreenWindow | MinimizeWindow | MaximizeWindow | GetWindowRect
+ | SetWindowRect(_) => {
+ let width = try_opt!(
+ try_opt!(
+ resp.result.get("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as positive integer"
+ );
+
+ let height = try_opt!(
+ try_opt!(
+ resp.result.get("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find heigenht field"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret height as positive integer"
+ );
+
+ let x = try_opt!(
+ try_opt!(
+ resp.result.get("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field"
+ )
+ .as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as integer"
+ );
+
+ let y = try_opt!(
+ try_opt!(
+ resp.result.get("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field"
+ )
+ .as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as integer"
+ );
+
+ let rect = WindowRectResponse {
+ x: x as i32,
+ y: y as i32,
+ width: width as i32,
+ height: height as i32,
+ };
+ WebDriverResponse::WindowRect(rect)
+ }
+ GetCookies => {
+ let cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
+ WebDriverResponse::Cookies(CookiesResponse(cookies))
+ }
+ GetNamedCookie(ref name) => {
+ let mut cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
+ cookies.retain(|x| x.name == *name);
+ let cookie = try_opt!(
+ cookies.pop(),
+ ErrorStatus::NoSuchCookie,
+ format!("No cookie with name {}", name)
+ );
+ WebDriverResponse::Cookie(CookieResponse(cookie))
+ }
+ FindElement(_) | FindElementElement(_, _) | FindShadowRootElement(_, _) => {
+ let element = self.to_web_element(try_opt!(
+ resp.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ ))?;
+ WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
+ }
+ FindElements(_) | FindElementElements(_, _) | FindShadowRootElements(_, _) => {
+ let element_vec = try_opt!(
+ resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array"
+ );
+ let elements = element_vec
+ .iter()
+ .map(|x| self.to_web_element(x))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ // TODO(Henrik): How to remove unwrap?
+ WebDriverResponse::Generic(ValueResponse(Value::Array(
+ elements
+ .iter()
+ .map(|x| serde_json::to_value(x).unwrap())
+ .collect(),
+ )))
+ }
+ GetShadowRoot(_) => {
+ let shadow_root = self.to_shadow_root(try_opt!(
+ resp.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ ))?;
+ WebDriverResponse::Generic(ValueResponse(serde_json::to_value(shadow_root)?))
+ }
+ GetActiveElement => {
+ let element = self.to_web_element(try_opt!(
+ resp.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ ))?;
+ WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
+ }
+ NewSession(_) => {
+ let session_id = try_opt!(
+ try_opt!(
+ resp.result.get("sessionId"),
+ ErrorStatus::InvalidSessionId,
+ "Failed to find sessionId field"
+ )
+ .as_str(),
+ ErrorStatus::InvalidSessionId,
+ "sessionId is not a string"
+ );
+
+ let mut capabilities = try_opt!(
+ try_opt!(
+ resp.result.get("capabilities"),
+ ErrorStatus::UnknownError,
+ "Failed to find capabilities field"
+ )
+ .as_object(),
+ ErrorStatus::UnknownError,
+ "capabilities field is not an object"
+ )
+ .clone();
+
+ capabilities.insert("moz:geckodriverVersion".into(), build::build_info().into());
+
+ WebDriverResponse::NewSession(NewSessionResponse::new(
+ session_id.to_string(),
+ Value::Object(capabilities),
+ ))
+ }
+ DeleteSession => WebDriverResponse::DeleteSession,
+ Extension(ref extension) => match extension {
+ GetContext => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ SetContext(_) => WebDriverResponse::Void,
+ InstallAddon(_) => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ UninstallAddon(_) => WebDriverResponse::Void,
+ TakeFullScreenshot => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ },
+ })
+ }
+}
+
+fn try_convert_to_marionette_message(
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ browser: &Browser,
+) -> 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 => match browser {
+ Browser::Local(_) | Browser::Remote(_) => Some(Command::Marionette(
+ marionette_rs::marionette::Command::DeleteSession {
+ flags: vec![AppStatus::eForceQuit],
+ },
+ )),
+ Browser::Existing(_) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::DeleteSession,
+ )),
+ },
+ DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)),
+ ElementClear(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClear {
+ id: e.clone().to_string(),
+ },
+ )),
+ ElementClick(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClick {
+ id: e.clone().to_string(),
+ },
+ )),
+ 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,
+ },
+ ))
+ }
+ 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,
+ },
+ ))
+ }
+ 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,
+ },
+ ))
+ }
+ FindShadowRootElement(ref s, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindShadowRootElement {
+ shadow_root: s.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value,
+ },
+ ))
+ }
+ FindShadowRootElements(ref s, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindShadowRootElements {
+ shadow_root: s.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value,
+ },
+ ))
+ }
+ 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)),
+ GetComputedLabel(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetComputedLabel {
+ id: e.clone().to_string(),
+ },
+ )),
+ GetComputedRole(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetComputedRole {
+ id: e.clone().to_string(),
+ },
+ )),
+ 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 e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementRect {
+ id: e.clone().to_string(),
+ },
+ )),
+ GetElementTagName(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementTagName {
+ id: e.clone().to_string(),
+ },
+ )),
+ GetElementText(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementText {
+ id: e.clone().to_string(),
+ },
+ )),
+ GetPageSource => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetPageSource,
+ )),
+ GetShadowRoot(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetShadowRoot {
+ id: e.clone().to_string(),
+ },
+ )),
+ 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 e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::IsDisplayed {
+ id: e.clone().to_string(),
+ },
+ )),
+ IsEnabled(ref e) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsEnabled {
+ id: e.clone().to_string(),
+ })),
+ IsSelected(ref e) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsSelected {
+ id: e.clone().to_string(),
+ })),
+ 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(TakeFullScreenshot) => {
+ let screenshot = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: true,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeFullScreenshot(screenshot),
+ ))
+ }
+ _ => None,
+ })
+}
+
+#[derive(Debug, PartialEq)]
+struct MarionetteCommand {
+ id: MessageId,
+ name: String,
+ params: Map<String, Value>,
+}
+
+impl Serialize for MarionetteCommand {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let data = (&0, &self.id, &self.name, &self.params);
+ data.serialize(serializer)
+ }
+}
+
+impl MarionetteCommand {
+ 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: MessageId,
+ capabilities: &Map<String, Value>,
+ browser: &Browser,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<String> {
+ use self::GeckoExtensionCommand::*;
+
+ if let Some(cmd) = try_convert_to_marionette_message(msg, browser)? {
+ 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 mut data = Map::new();
+ for (k, v) in capabilities.iter() {
+ data.insert(k.to_string(), serde_json::to_value(v)?);
+ }
+
+ (Some("WebDriver:NewSession"), 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())),
+ _ => (None, None),
+ },
+ _ => (None, None),
+ };
+
+ let name = try_opt!(
+ opt_name,
+ ErrorStatus::UnsupportedOperation,
+ "Operation not supported"
+ );
+ let parameters = opt_parameters.unwrap_or_else(|| Ok(Map::new()))?;
+
+ let req = MarionetteCommand::new(id, name.into(), parameters);
+ MarionetteCommand::encode_msg(req)
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+struct MarionetteResponse {
+ id: MessageId,
+ error: Option<MarionetteError>,
+ result: Value,
+}
+
+impl<'de> Deserialize<'de> for MarionetteResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ struct ResponseWrapper {
+ msg_type: u64,
+ id: MessageId,
+ error: Option<MarionetteError>,
+ result: Value,
+ }
+
+ let wrapper: ResponseWrapper = Deserialize::deserialize(deserializer)?;
+
+ if wrapper.msg_type != 1 {
+ return Err(de::Error::custom(
+ "Expected '1' in first element of response",
+ ));
+ };
+
+ Ok(MarionetteResponse {
+ id: wrapper.id,
+ error: wrapper.error,
+ result: wrapper.result,
+ })
+ }
+}
+
+impl MarionetteResponse {
+ 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"
+ )
+ } else {
+ &self.result
+ };
+
+ Ok(ValueResponse(value.clone()))
+ }
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+struct MarionetteError {
+ #[serde(rename = "error")]
+ code: String,
+ message: String,
+ stacktrace: Option<String>,
+}
+
+impl From<MarionetteError> for WebDriverError {
+ fn from(error: MarionetteError) -> WebDriverError {
+ let status = ErrorStatus::from(error.code);
+ let message = error.message;
+
+ if let Some(stack) = error.stacktrace {
+ WebDriverError::new_with_stack(status, message, stack)
+ } else {
+ WebDriverError::new(status, message)
+ }
+ }
+}
+
+fn get_free_port(host: &str) -> IoResult<u16> {
+ TcpListener::bind((host, 0))
+ .and_then(|stream| stream.local_addr())
+ .map(|x| x.port())
+}
+
+struct MarionetteConnection {
+ browser: Browser,
+ session: MarionetteSession,
+ stream: TcpStream,
+}
+
+impl MarionetteConnection {
+ fn new(
+ host: String,
+ mut browser: Browser,
+ session: MarionetteSession,
+ ) -> WebDriverResult<MarionetteConnection> {
+ let stream = match MarionetteConnection::connect(&host, &mut browser) {
+ Ok(stream) => stream,
+ Err(e) => {
+ if let Err(e) = browser.close(true) {
+ error!("Failed to stop browser: {:?}", e);
+ }
+ return Err(e);
+ }
+ };
+ Ok(MarionetteConnection {
+ browser,
+ session,
+ stream,
+ })
+ }
+
+ fn connect(host: &str, browser: &mut Browser) -> WebDriverResult<TcpStream> {
+ let timeout = time::Duration::from_secs(60);
+ let poll_interval = time::Duration::from_millis(100);
+ let now = time::Instant::now();
+
+ debug!(
+ "Waiting {}s to connect to browser on {}",
+ timeout.as_secs(),
+ host,
+ );
+
+ loop {
+ // immediately abort connection attempts if process disappears
+ if let Browser::Local(browser) = browser {
+ if let Some(status) = browser.check_status() {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Process unexpectedly closed with status {}", status),
+ ));
+ }
+ }
+
+ let last_err;
+
+ if let Some(port) = browser.marionette_port()? {
+ match MarionetteConnection::try_connect(host, port) {
+ Ok(stream) => {
+ debug!("Connection to Marionette established on {}:{}.", host, port);
+ browser.update_marionette_port(port);
+ return Ok(stream);
+ }
+ Err(e) => {
+ let err_str = e.to_string();
+ last_err = Some(err_str);
+ }
+ }
+ } else {
+ last_err = Some("Failed to read marionette port".into());
+ }
+ if now.elapsed() < timeout {
+ trace!("Retrying in {:?}", poll_interval);
+ thread::sleep(poll_interval);
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::Timeout,
+ last_err.unwrap_or_else(|| "Unknown error".into()),
+ ));
+ }
+ }
+ }
+
+ fn try_connect(host: &str, port: u16) -> WebDriverResult<TcpStream> {
+ let mut stream = TcpStream::connect((host, port))?;
+ MarionetteConnection::handshake(&mut stream)?;
+ Ok(stream)
+ }
+
+ 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. Don't
+ // make it shorter as 1000ms to not fail on slow connections.
+ stream
+ .set_read_timeout(Some(time::Duration::from_millis(1000)))
+ .ok();
+ let data = MarionetteConnection::read_resp(stream);
+ stream.set_read_timeout(timeout).ok();
+
+ data
+ }
+ _ => 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)?;
+
+ if data.application_type != "gecko" {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Unrecognized application type {}", data.application_type),
+ ));
+ }
+
+ if data.protocol != 3 {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!(
+ "Unsupported Marionette protocol version {}, required 3",
+ data.protocol
+ ),
+ ));
+ }
+
+ Ok(data)
+ }
+
+ fn close(self, wait_for_shutdown: bool) -> WebDriverResult<()> {
+ self.stream.shutdown(Shutdown::Both)?;
+ self.browser.close(wait_for_shutdown)?;
+ Ok(())
+ }
+
+ fn send_command(
+ &mut self,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<WebDriverResponse> {
+ let id = self.session.next_command_id();
+ let enc_cmd = MarionetteCommand::from_webdriver_message(
+ id,
+ &self.session.capabilities,
+ &self.browser,
+ 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, data: String) -> WebDriverResult<String> {
+ if self.stream.write(data.as_bytes()).is_err() {
+ let mut err = WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to write request to stream",
+ );
+ err.delete_session = true;
+ return Err(err);
+ }
+
+ match MarionetteConnection::read_resp(&mut self.stream) {
+ Ok(resp) => Ok(resp),
+ Err(_) => {
+ let mut err = WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to decode response from marionette",
+ );
+ err.delete_session = true;
+ Err(err)
+ }
+ }
+ }
+
+ fn read_resp(stream: &mut TcpStream) -> IoResult<String> {
+ let mut bytes = 0usize;
+
+ loop {
+ let buf = &mut [0u8];
+ let num_read = stream.read(buf)?;
+ let byte = match num_read {
+ 0 => {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ "EOF reading marionette message",
+ ))
+ }
+ 1 => buf[0],
+ _ => panic!("Expected one byte got more"),
+ } as char;
+ match byte {
+ '0'..='9' => {
+ bytes *= 10;
+ bytes += byte as usize - '0' as usize;
+ }
+ ':' => break,
+ _ => {}
+ }
+ }
+
+ let buf = &mut [0u8; 8192];
+ let mut payload = Vec::with_capacity(bytes);
+ let mut total_read = 0;
+ while total_read < bytes {
+ let num_read = stream.read(buf)?;
+ if num_read == 0 {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ "EOF reading marionette message",
+ ));
+ }
+ total_read += num_read;
+ for x in &buf[..num_read] {
+ payload.push(*x);
+ }
+ }
+
+ // TODO(jgraham): Need to handle the error here
+ Ok(String::from_utf8(payload).unwrap())
+ }
+}
+
+trait ToMarionette<T> {
+ fn to_marionette(&self) -> WebDriverResult<T>;
+}
+
+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)?,
+ );
+ }
+ Ok(data)
+ }
+}
+
+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()));
+ Ok(data)
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for GeckoContextParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ let mut data = Map::new();
+ data.insert(
+ "value".to_owned(),
+ serde_json::to_value(self.context.clone())?,
+ );
+ Ok(data)
+ }
+}
+
+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 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())
+ }
+}
+
+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<MarionetteDate> for Date {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteDate> {
+ Ok(MarionetteDate(self.0))
+ }
+}
+
+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())
+ }
+}
+
+impl ToMarionette<MarionetteUrl> for GetParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteUrl> {
+ Ok(MarionetteUrl {
+ url: self.url.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteScript> for JavascriptCommandParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteScript> {
+ Ok(MarionetteScript {
+ script: self.script.clone(),
+ args: self.args.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<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),
+ }
+ }
+}
+
+impl ToMarionette<MarionetteNewWindow> for NewWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteNewWindow> {
+ Ok(MarionetteNewWindow {
+ type_hint: self.type_hint.clone(),
+ })
+ }
+}
+
+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<MarionetteFrame> for SwitchToFrameParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> {
+ Ok(match &self.id {
+ Some(x) => match x {
+ FrameId::Short(n) => MarionetteFrame::Index(*n),
+ FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()),
+ },
+ None => MarionetteFrame::Parent,
+ })
+ }
+}
+
+impl ToMarionette<Window> for SwitchToWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<Window> {
+ Ok(Window {
+ handle: self.handle.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<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,
+ })
+ }
+}
diff --git a/testing/geckodriver/src/prefs.rs b/testing/geckodriver/src/prefs.rs
new file mode 100644
index 0000000000..4407cbccaa
--- /dev/null
+++ b/testing/geckodriver/src/prefs.rs
@@ -0,0 +1,150 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use mozprofile::preferences::Pref;
+
+// ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A GECKODRIVER PEER!
+//
+// Please refer to INSTRUCTIONS TO ADD A NEW PREFERENCE in
+// remote/shared/RecommendedPreferences.sys.mjs
+//
+// Note: geckodriver is used out-of-tree with various builds of Firefox.
+// Removing a preference from this file will cause regressions,
+// so please be careful and get review from a Testing :: geckodriver peer
+// before you make any changes to this file.
+lazy_static! {
+ pub static ref DEFAULT: Vec<(&'static str, Pref)> = vec![
+ // Make sure Shield doesn't hit the network.
+ ("app.normandy.api_url", Pref::new("")),
+
+ // Disable Firefox old build background check
+ ("app.update.checkInstallTime", Pref::new(false)),
+
+ // Disable automatically upgrading Firefox
+ //
+ // Note: Possible update tests could reset or flip the value to allow
+ // updates to be downloaded and applied.
+ ("app.update.disabledForTesting", Pref::new(true)),
+
+ // Enable the dump function, which sends messages to the system
+ // console
+ ("browser.dom.window.dump.enabled", Pref::new(true)),
+ ("devtools.console.stdout.chrome", Pref::new(true)),
+
+ // Do not restore the last open set of tabs if the browser crashed
+ ("browser.sessionstore.resume_from_crash", Pref::new(false)),
+
+ // Skip check for default browser on startup
+ ("browser.shell.checkDefaultBrowser", Pref::new(false)),
+
+ // Do not redirect user when a milestone upgrade of Firefox
+ // is detected
+ ("browser.startup.homepage_override.mstone", Pref::new("ignore")),
+
+ // Start with a blank page (about:blank)
+ ("browser.startup.page", Pref::new(0)),
+
+ // Disable page translations, causing timeouts for wdspec tests in early
+ // beta. See Bug 1836093.
+ ("browser.translations.enable", Pref::new(false)),
+
+ // Disable the UI tour
+ ("browser.uitour.enabled", Pref::new(false)),
+
+ // Do not warn on quitting Firefox
+ ("browser.warnOnQuit", Pref::new(false)),
+
+ // Defensively disable data reporting systems
+ ("datareporting.healthreport.documentServerURI", Pref::new("http://%(server)s/dummy/healthreport/")),
+ ("datareporting.healthreport.logging.consoleEnabled", Pref::new(false)),
+ ("datareporting.healthreport.service.enabled", Pref::new(false)),
+ ("datareporting.healthreport.service.firstRun", Pref::new(false)),
+ ("datareporting.healthreport.uploadEnabled", Pref::new(false)),
+
+ // Do not show datareporting policy notifications which can
+ // interfere with tests
+ ("datareporting.policy.dataSubmissionEnabled", Pref::new(false)),
+ ("datareporting.policy.dataSubmissionPolicyBypassNotification", Pref::new(true)),
+
+ // Disable the ProcessHangMonitor
+ ("dom.ipc.reportProcessHangs", Pref::new(false)),
+
+ // Only load extensions from the application and user profile
+ // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ ("extensions.autoDisableScopes", Pref::new(0)),
+ ("extensions.enabledScopes", Pref::new(5)),
+
+ // Disable intalling any distribution extensions or add-ons
+ ("extensions.installDistroAddons", Pref::new(false)),
+
+ // Turn off extension updates so they do not bother tests
+ ("extensions.update.enabled", Pref::new(false)),
+ ("extensions.update.notifyUser", Pref::new(false)),
+
+ // Allow the application to have focus even it runs in the
+ // background
+ ("focusmanager.testmode", Pref::new(true)),
+
+ // Disable useragent updates
+ ("general.useragent.updates.enabled", Pref::new(false)),
+
+ // Always use network provider for geolocation tests so we bypass
+ // the macOS dialog raised by the corelocation provider
+ ("geo.provider.testing", Pref::new(true)),
+
+ // Do not scan wi-fi
+ ("geo.wifi.scan", Pref::new(false)),
+
+ // 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)),
+
+ // Disable download and usage of OpenH264, and Widevine plugins
+ ("media.gmp-manager.updateEnabled", Pref::new(false)),
+
+ // Disable the GFX sanity window
+ ("media.sanity-test.disabled", Pref::new(true)),
+
+ // Do not automatically switch between offline and online
+ ("network.manage-offline-status", Pref::new(false)),
+
+ // Make sure SNTP requests do not hit the network
+ ("network.sntp.pools", Pref::new("%(server)s")),
+
+ // Don't do network connections for mitm priming
+ ("security.certerrors.mitm.priming.enabled", Pref::new(false)),
+
+ // Ensure remote settings do not hit the network
+ ("services.settings.server", Pref::new("data:,#remote-settings-dummy/v1")),
+
+ // Disable first run pages
+ ("startup.homepage_welcome_url", Pref::new("about:blank")),
+ ("startup.homepage_welcome_url.additional", Pref::new("")),
+
+ // asrouter expects a plain object or null
+ ("browser.newtabpage.activity-stream.asrouter.providers.cfr", Pref::new("null")),
+ // TODO: Remove once minimum supported Firefox release is 93.
+ ("browser.newtabpage.activity-stream.asrouter.providers.cfr-fxa", Pref::new("null")),
+ ("browser.newtabpage.activity-stream.asrouter.providers.snippets", Pref::new("null")),
+ ("browser.newtabpage.activity-stream.asrouter.providers.message-groups", Pref::new("null")),
+ ("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", Pref::new("null")),
+ ("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", Pref::new("null")),
+ ("browser.newtabpage.activity-stream.feeds.system.topstories", Pref::new(false)),
+ ("browser.newtabpage.activity-stream.feeds.snippets", Pref::new(false)),
+ ("browser.newtabpage.activity-stream.tippyTop.service.endpoint", Pref::new("")),
+ ("browser.newtabpage.activity-stream.discoverystream.config", Pref::new("[]")),
+
+ // For Activity Stream firstrun page, use an empty string to avoid fetching.
+ ("browser.newtabpage.activity-stream.fxaccounts.endpoint", Pref::new("")),
+
+ // Prevent starting into safe mode after application crashes
+ ("toolkit.startup.max_resumed_crashes", Pref::new(-1)),
+
+ // Disable webapp updates.
+ ("browser.webapps.checkForUpdates", Pref::new(0)),
+ ];
+}
diff --git a/testing/geckodriver/src/test.rs b/testing/geckodriver/src/test.rs
new file mode 100644
index 0000000000..e664aadf08
--- /dev/null
+++ b/testing/geckodriver/src/test.rs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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/testing/geckodriver/src/tests/profile.zip b/testing/geckodriver/src/tests/profile.zip
new file mode 100644
index 0000000000..286b118183
--- /dev/null
+++ b/testing/geckodriver/src/tests/profile.zip
Binary files differ