summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver/marionette
diff options
context:
space:
mode:
Diffstat (limited to 'testing/geckodriver/marionette')
-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.rs459
9 files changed, 1574 insertions, 0 deletions
diff --git a/testing/geckodriver/marionette/Cargo.toml b/testing/geckodriver/marionette/Cargo.toml
new file mode 100644
index 0000000000..6e4c4acc88
--- /dev/null
+++ b/testing/geckodriver/marionette/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "marionette"
+version = "0.2.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..a795701198
--- /dev/null
+++ b/testing/geckodriver/marionette/src/webdriver.rs
@@ -0,0 +1,459 @@
+/* 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 LegacyWebElement {
+ pub id: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Locator {
+ pub using: Selector,
+ pub value: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Selector {
+ #[serde(rename = "css selector")]
+ Css,
+ #[serde(rename = "link text")]
+ LinkText,
+ #[serde(rename = "partial link text")]
+ PartialLinkText,
+ #[serde(rename = "tag name")]
+ TagName,
+ #[serde(rename = "xpath")]
+ XPath,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct NewWindow {
+ #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+ pub type_hint: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct WindowRect {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub x: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub y: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub width: Option<i32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub height: Option<i32>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Keys {
+ pub text: String,
+ pub value: Vec<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(default, rename_all = "camelCase")]
+pub struct PrintParameters {
+ pub orientation: PrintOrientation,
+ pub scale: f64,
+ pub background: bool,
+ pub page: PrintPage,
+ pub margin: PrintMargins,
+ pub page_ranges: Vec<String>,
+ pub shrink_to_fit: bool,
+}
+
+impl Default for PrintParameters {
+ fn default() -> Self {
+ PrintParameters {
+ orientation: PrintOrientation::default(),
+ scale: 1.0,
+ background: false,
+ page: PrintPage::default(),
+ margin: PrintMargins::default(),
+ page_ranges: Vec::new(),
+ shrink_to_fit: true,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum PrintOrientation {
+ Landscape,
+ Portrait,
+}
+
+impl Default for PrintOrientation {
+ fn default() -> Self {
+ PrintOrientation::Portrait
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct PrintPage {
+ pub width: f64,
+ pub height: f64,
+}
+
+impl Default for PrintPage {
+ fn default() -> Self {
+ PrintPage {
+ width: 21.59,
+ height: 27.94,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct PrintMargins {
+ pub top: f64,
+ pub bottom: f64,
+ pub left: f64,
+ pub right: f64,
+}
+
+impl Default for PrintMargins {
+ fn default() -> Self {
+ PrintMargins {
+ top: 1.0,
+ bottom: 1.0,
+ left: 1.0,
+ right: 1.0,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct ScreenshotOptions {
+ pub id: Option<String>,
+ pub highlights: Vec<Option<String>>,
+ pub full: bool,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Script {
+ pub script: String,
+ pub args: Option<Vec<Value>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Command {
+ #[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(LegacyWebElement),
+ #[serde(rename = "WebDriver:ElementClick")]
+ ElementClick(LegacyWebElement),
+ #[serde(rename = "WebDriver:ElementSendKeys")]
+ ElementSendKeys {
+ id: String,
+ text: String,
+ value: Vec<String>,
+ },
+ #[serde(rename = "WebDriver:ExecuteAsyncScript")]
+ ExecuteAsyncScript(Script),
+ #[serde(rename = "WebDriver:ExecuteScript")]
+ ExecuteScript(Script),
+ #[serde(rename = "WebDriver:FindElement")]
+ FindElement(Locator),
+ #[serde(rename = "WebDriver:FindElements")]
+ FindElements(Locator),
+ #[serde(rename = "WebDriver:FindElement")]
+ FindElementElement {
+ element: String,
+ using: Selector,
+ value: String,
+ },
+ #[serde(rename = "WebDriver:FindElements")]
+ FindElementElements {
+ element: String,
+ using: Selector,
+ value: String,
+ },
+ #[serde(rename = "WebDriver:FullscreenWindow")]
+ FullscreenWindow,
+ #[serde(rename = "WebDriver:Navigate")]
+ Get(Url),
+ #[serde(rename = "WebDriver:GetActiveElement")]
+ GetActiveElement,
+ #[serde(rename = "WebDriver:GetAlertText")]
+ GetAlertText,
+ #[serde(rename = "WebDriver:GetCookies")]
+ GetCookies,
+ #[serde(rename = "WebDriver:GetElementCSSValue")]
+ GetCSSValue {
+ id: String,
+ #[serde(rename = "propertyName")]
+ property: String,
+ },
+ #[serde(rename = "WebDriver:GetCurrentURL")]
+ GetCurrentUrl,
+ #[serde(rename = "WebDriver:GetElementAttribute")]
+ GetElementAttribute { id: String, name: String },
+ #[serde(rename = "WebDriver:GetElementProperty")]
+ GetElementProperty { id: String, name: String },
+ #[serde(rename = "WebDriver:GetElementRect")]
+ GetElementRect(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetElementTagName")]
+ GetElementTagName(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetElementText")]
+ GetElementText(LegacyWebElement),
+ #[serde(rename = "WebDriver:GetPageSource")]
+ GetPageSource,
+ #[serde(rename = "WebDriver: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(LegacyWebElement),
+ #[serde(rename = "WebDriver:IsElementEnabled")]
+ IsEnabled(LegacyWebElement),
+ #[serde(rename = "WebDriver:IsElementSelected")]
+ IsSelected(LegacyWebElement),
+ #[serde(rename = "WebDriver:MaximizeWindow")]
+ MaximizeWindow,
+ #[serde(rename = "WebDriver:MinimizeWindow")]
+ MinimizeWindow,
+ #[serde(rename = "WebDriver:NewWindow")]
+ NewWindow(NewWindow),
+ #[serde(rename = "WebDriver:Print")]
+ Print(PrintParameters),
+ #[serde(rename = "WebDriver:Refresh")]
+ Refresh,
+ #[serde(rename = "WebDriver:ReleaseActions")]
+ ReleaseActions,
+ #[serde(rename = "WebDriver:SendAlertText")]
+ SendAlertText(Keys),
+ #[serde(rename = "WebDriver:SetTimeouts")]
+ SetTimeouts(Timeouts),
+ #[serde(rename = "WebDriver:SetWindowRect")]
+ SetWindowRect(WindowRect),
+ #[serde(rename = "WebDriver:SwitchToFrame")]
+ SwitchToFrame(Frame),
+ #[serde(rename = "WebDriver:SwitchToParentFrame")]
+ SwitchToParentFrame,
+ #[serde(rename = "WebDriver:SwitchToWindow")]
+ SwitchToWindow(Window),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeElementScreenshot(ScreenshotOptions),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeFullScreenshot(ScreenshotOptions),
+ #[serde(rename = "WebDriver:TakeScreenshot")]
+ TakeScreenshot(ScreenshotOptions),
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::common::Date;
+ use crate::test::{assert_ser, assert_ser_de};
+ use serde_json::json;
+
+ #[test]
+ fn test_json_screenshot() {
+ let data = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: false,
+ };
+ let json = json!({"full":false,"highlights":[],"id":null});
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_selector_css() {
+ assert_ser_de(&Selector::Css, json!("css selector"));
+ }
+
+ #[test]
+ fn test_json_selector_link_text() {
+ assert_ser_de(&Selector::LinkText, json!("link text"));
+ }
+
+ #[test]
+ fn test_json_selector_partial_link_text() {
+ assert_ser_de(&Selector::PartialLinkText, json!("partial link text"));
+ }
+
+ #[test]
+ fn test_json_selector_tag_name() {
+ assert_ser_de(&Selector::TagName, json!("tag name"));
+ }
+
+ #[test]
+ fn test_json_selector_xpath() {
+ assert_ser_de(&Selector::XPath, json!("xpath"));
+ }
+
+ #[test]
+ fn test_json_selector_invalid() {
+ assert!(serde_json::from_value::<Selector>(json!("foo")).is_err());
+ }
+
+ #[test]
+ fn test_json_locator() {
+ let json = json!({
+ "using": "partial link text",
+ "value": "link text",
+ });
+ let data = Locator {
+ using: Selector::PartialLinkText,
+ value: "link text".into(),
+ };
+
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_keys() {
+ let data = Keys {
+ text: "Foo".into(),
+ value: vec!["F".into(), "o".into(), "o".into()],
+ };
+ let json = json!({"text": "Foo", "value": ["F", "o", "o"]});
+ assert_ser_de(&data, json);
+ }
+
+ #[test]
+ fn test_json_new_window() {
+ let data = NewWindow {
+ type_hint: Some("foo".into()),
+ };
+ assert_ser_de(&data, json!({ "type": "foo" }));
+ }
+
+ #[test]
+ fn test_json_window_rect() {
+ let data = WindowRect {
+ x: Some(123),
+ y: None,
+ width: None,
+ height: None,
+ };
+ assert_ser_de(&data, json!({"x": 123}));
+ }
+
+ #[test]
+ fn test_command_with_params() {
+ let locator = Locator {
+ using: Selector::Css,
+ value: "value".into(),
+ };
+ let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}});
+ assert_ser_de(&Command::FindElement(locator), json);
+ }
+
+ #[test]
+ fn test_command_with_wrapper_params() {
+ let cookie = Cookie {
+ name: "hello".into(),
+ value: "world".into(),
+ path: None,
+ domain: None,
+ secure: false,
+ http_only: false,
+ expiry: Some(Date(1564488092)),
+ same_site: None,
+ };
+ let json = json!({"WebDriver:AddCookie": {"cookie": {"name": "hello", "value": "world", "secure": false, "httpOnly": false, "expiry": 1564488092}}});
+ assert_ser_de(&Command::AddCookie(cookie), json);
+ }
+
+ #[test]
+ fn test_empty_commands() {
+ assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts"));
+ }
+
+ #[test]
+ fn test_json_command_invalid() {
+ assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
+ }
+
+ #[test]
+ fn test_json_delete_cookie_command() {
+ let json = json!({"WebDriver:DeleteCookie": {"name": "foo"}});
+ assert_ser_de(&Command::DeleteCookie("foo".into()), json);
+ }
+
+ #[test]
+ fn test_json_new_window_command() {
+ let data = NewWindow {
+ type_hint: Some("foo".into()),
+ };
+ let json = json!({"WebDriver:NewWindow": {"type": "foo"}});
+ assert_ser_de(&Command::NewWindow(data), json);
+ }
+
+ #[test]
+ fn test_json_new_window_command_with_none_value() {
+ let data = NewWindow { type_hint: None };
+ let json = json!({"WebDriver:NewWindow": {}});
+ assert_ser_de(&Command::NewWindow(data), json);
+ }
+
+ #[test]
+ fn test_json_command_as_struct() {
+ assert_ser(
+ &Command::FindElementElement {
+ element: "foo".into(),
+ using: Selector::XPath,
+ value: "bar".into(),
+ },
+ json!({"WebDriver:FindElement": {"element": "foo", "using": "xpath", "value": "bar" }}),
+ );
+ }
+
+ #[test]
+ fn test_json_get_css_value() {
+ assert_ser_de(
+ &Command::GetCSSValue {
+ id: "foo".into(),
+ property: "bar".into(),
+ },
+ json!({"WebDriver:GetElementCSSValue": {"id": "foo", "propertyName": "bar"}}),
+ );
+ }
+}