diff options
Diffstat (limited to 'testing/geckodriver/marionette')
-rw-r--r-- | testing/geckodriver/marionette/Cargo.toml | 14 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/common.rs | 240 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/error.rs | 184 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/lib.rs | 14 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/marionette.rs | 69 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/message.rs | 336 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/result.rs | 223 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/test.rs | 35 | ||||
-rw-r--r-- | testing/geckodriver/marionette/src/webdriver.rs | 459 |
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"}}), + ); + } +} |