зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1560836 - Create marionette subcrate in geckodriver. r=ato
Differential Revision: https://phabricator.services.mozilla.com/D37375 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
e95055a0fe
Коммит
e8f6e00b4c
|
@ -37,6 +37,10 @@ exclude = [
|
|||
"media/mp4parse-rust/mp4parse_capi",
|
||||
"media/mp4parse-rust/mp4parse_fallible",
|
||||
"xpcom/rust/gkrust_utils",
|
||||
|
||||
# Exclude temporarily till it is developed independently
|
||||
# TODO(nupur): Remove after this is completed
|
||||
"testing/geckodriver/marionette",
|
||||
]
|
||||
|
||||
# Explicitly specify what our profiles use. The opt-level setting here is
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
[workspace]
|
||||
[package]
|
||||
name = "marionette"
|
||||
version = "0.1.0"
|
||||
authors = ["Mozilla"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
|
@ -0,0 +1,12 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BoolValue {
|
||||
value: bool,
|
||||
}
|
||||
|
||||
impl BoolValue {
|
||||
pub fn new(val: bool) -> Self {
|
||||
BoolValue { value: val }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
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", skip_serializing)]
|
||||
pub kind: ErrorKind,
|
||||
#[serde(default = "empty_string")]
|
||||
pub message: String,
|
||||
#[serde(rename = "stacktrace", default = "empty_string")]
|
||||
pub stack: String,
|
||||
}
|
||||
|
||||
fn empty_string() -> String {
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
impl Into<Error> for MarionetteError {
|
||||
fn into(self) -> Error {
|
||||
Error::Marionette(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub enum ErrorKind {
|
||||
#[serde(rename = "element click intercepted")]
|
||||
ElementClickIntercepted,
|
||||
#[serde(rename = "element not accessible")]
|
||||
ElementNotAccessible,
|
||||
#[serde(rename = "element not interactable")]
|
||||
ElementNotInteractable,
|
||||
#[serde(rename = "insecure certificate")]
|
||||
InsecureCertificate,
|
||||
#[serde(rename = "invalid argument")]
|
||||
InvalidArgument,
|
||||
#[serde(rename = "invalid cookie")]
|
||||
InvalidCookieDomain,
|
||||
#[serde(rename = "invalid element state")]
|
||||
InvalidElementState,
|
||||
#[serde(rename = "invalid selector")]
|
||||
InvalidSelector,
|
||||
#[serde(rename = "invalid session id")]
|
||||
InvalidSessionId,
|
||||
#[serde(rename = "javascript error")]
|
||||
JavaScript,
|
||||
#[serde(rename = "move target out of bounds")]
|
||||
MoveTargetOutOfBounds,
|
||||
#[serde(rename = "no such alert")]
|
||||
NoSuchAlert,
|
||||
#[serde(rename = "no such element")]
|
||||
NoSuchElement,
|
||||
#[serde(rename = "no such frame")]
|
||||
NoSuchFrame,
|
||||
#[serde(rename = "no such window")]
|
||||
NoSuchWindow,
|
||||
#[serde(rename = "script timeout")]
|
||||
ScriptTimeout,
|
||||
#[serde(rename = "session not created")]
|
||||
SessionNotCreated,
|
||||
#[serde(rename = "stale element reference")]
|
||||
StaleElementReference,
|
||||
#[serde(rename = "timeout")]
|
||||
Timeout,
|
||||
#[serde(rename = "unable to set cookie")]
|
||||
UnableToSetCookie,
|
||||
#[serde(rename = "unexpected alert open")]
|
||||
UnexpectedAlertOpen,
|
||||
#[serde(rename = "unknown command")]
|
||||
UnknownCommand,
|
||||
#[serde(rename = "unknown error")]
|
||||
Unknown,
|
||||
#[serde(rename = "unsupported operation")]
|
||||
UnsupportedOperation,
|
||||
#[serde(rename = "webdriver error")]
|
||||
WebDriver,
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
use ErrorKind::*;
|
||||
match self {
|
||||
ElementClickIntercepted => "element click intercepted",
|
||||
ElementNotAccessible => "element not accessible",
|
||||
ElementNotInteractable => "element not interactable",
|
||||
InsecureCertificate => "insecure certificate",
|
||||
InvalidArgument => "invalid argument",
|
||||
InvalidCookieDomain => "invalid cookie",
|
||||
InvalidElementState => "invalid element state",
|
||||
InvalidSelector => "invalid selector",
|
||||
InvalidSessionId => "invalid session id",
|
||||
JavaScript => "javascript error",
|
||||
MoveTargetOutOfBounds => "move target out of bounds",
|
||||
NoSuchAlert => "no such alert",
|
||||
NoSuchElement => "no such element",
|
||||
NoSuchFrame => "no such frame",
|
||||
NoSuchWindow => "no such window",
|
||||
ScriptTimeout => "script timeout",
|
||||
SessionNotCreated => "session not created",
|
||||
StaleElementReference => "stale eelement referencee",
|
||||
Timeout => "timeout",
|
||||
UnableToSetCookie => "unable to set cookie",
|
||||
UnexpectedAlertOpen => "unexpected alert open",
|
||||
UnknownCommand => "unknown command",
|
||||
Unknown => "unknown error",
|
||||
UnsupportedOperation => "unsupported operation",
|
||||
WebDriver => "webdriver error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::assert_ser;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_error_serialize() {
|
||||
let err = MarionetteError {
|
||||
kind: ErrorKind::Timeout,
|
||||
message: "".into(),
|
||||
stack: "".into(),
|
||||
};
|
||||
assert_ser(&err, json!({"message": "", "stacktrace": ""}));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
pub mod error;
|
||||
|
||||
pub mod common;
|
||||
pub mod marionette;
|
||||
pub mod message;
|
||||
pub mod webdriver;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
|
@ -0,0 +1,46 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::BoolValue;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Command {
|
||||
#[serde(rename = "Marionette:AcceptConnections")]
|
||||
AcceptConnections(BoolValue),
|
||||
#[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_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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,333 @@
|
|||
use serde::de::{self, SeqAccess, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{Map, Value};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::marionette;
|
||||
use crate::webdriver;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
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,
|
||||
}
|
||||
|
||||
type MessageId = u32;
|
||||
type Payload = Map<String, Value>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct Request(MessageId, 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)]
|
||||
enum Response {
|
||||
Result { id: MessageId, result: Payload },
|
||||
Error { id: MessageId, error: Error },
|
||||
}
|
||||
|
||||
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.description(), &error).serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
#[serde(untagged)]
|
||||
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<ErrorKind> = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
|
||||
|
||||
let response = if let Some(kind) = maybe_error {
|
||||
// intermediary transformation to Error by inserting {"error": <kind>}
|
||||
// so it can be treated as a MarionetteError
|
||||
let mut details: Map<String, Value> = seq
|
||||
.next_element()?
|
||||
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
|
||||
details.insert("error".to_string(), serde_json::to_value(kind).unwrap());
|
||||
|
||||
let error: Error = serde_json::from_value(Value::Object(details)).unwrap();
|
||||
Response::Error { id, error }
|
||||
} else {
|
||||
let result: Payload = 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::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 }]);
|
||||
// TODO: payload is currently untyped
|
||||
let mut result = Map::new();
|
||||
result.insert("value".into(), Value::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, "no such element", {"message": "", "stacktrace": ""}]);
|
||||
let error: Error = MarionetteError {
|
||||
kind: ErrorKind::NoSuchElement,
|
||||
message: "".into(),
|
||||
stack: "".into(),
|
||||
}
|
||||
.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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[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 enum Command {
|
||||
#[serde(rename = "WebDriver:FindElement")]
|
||||
FindElement(Locator),
|
||||
#[serde(rename = "WebDriver:GetTimeouts")]
|
||||
GetTimeouts,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::assert_ser_de;
|
||||
use serde_json::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_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_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());
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче