/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::actions::ActionSequence; use crate::capabilities::{ BrowserCapabilities, Capabilities, CapabilitiesMatching, LegacyNewSessionParameters, SpecNewSessionParameters, }; use crate::common::{ CredentialParameters, Date, FrameId, LocatorStrategy, ShadowRoot, WebElement, MAX_SAFE_INTEGER, }; use crate::error::{ErrorStatus, WebDriverError, WebDriverResult}; use crate::httpapi::{Route, VoidWebDriverExtensionRoute, WebDriverExtensionRoute}; use crate::Parameters; use serde::de::{self, Deserialize, Deserializer}; use serde_json::Value; #[derive(Debug, PartialEq)] pub enum WebDriverCommand { NewSession(NewSessionParameters), DeleteSession, Get(GetParameters), GetCurrentUrl, GoBack, GoForward, Refresh, GetTitle, GetPageSource, GetWindowHandle, GetWindowHandles, NewWindow(NewWindowParameters), CloseWindow, GetWindowRect, SetWindowRect(WindowRectParameters), MinimizeWindow, MaximizeWindow, FullscreenWindow, SwitchToWindow(SwitchToWindowParameters), SwitchToFrame(SwitchToFrameParameters), SwitchToParentFrame, FindElement(LocatorParameters), FindElements(LocatorParameters), FindElementElement(WebElement, LocatorParameters), FindElementElements(WebElement, LocatorParameters), FindShadowRootElement(ShadowRoot, LocatorParameters), FindShadowRootElements(ShadowRoot, LocatorParameters), GetActiveElement, GetComputedLabel(WebElement), GetComputedRole(WebElement), GetShadowRoot(WebElement), IsDisplayed(WebElement), IsSelected(WebElement), GetElementAttribute(WebElement, String), GetElementProperty(WebElement, String), GetCSSValue(WebElement, String), GetElementText(WebElement), GetElementTagName(WebElement), GetElementRect(WebElement), IsEnabled(WebElement), ExecuteScript(JavascriptCommandParameters), ExecuteAsyncScript(JavascriptCommandParameters), GetCookies, GetNamedCookie(String), AddCookie(AddCookieParameters), DeleteCookies, DeleteCookie(String), GetTimeouts, SetTimeouts(TimeoutsParameters), ElementClick(WebElement), ElementClear(WebElement), ElementSendKeys(WebElement, SendKeysParameters), PerformActions(ActionsParameters), ReleaseActions, DismissAlert, AcceptAlert, GetAlertText, SendAlertText(SendKeysParameters), TakeScreenshot, TakeElementScreenshot(WebElement), Print(PrintParameters), Status, Extension(T), WebAuthnAddVirtualAuthenticator(AuthenticatorParameters), WebAuthnRemoveVirtualAuthenticator, WebAuthnAddCredential(CredentialParameters), WebAuthnGetCredentials, WebAuthnRemoveCredential, WebAuthnRemoveAllCredentials, WebAuthnSetUserVerified(UserVerificationParameters), } pub trait WebDriverExtensionCommand: Clone + Send { fn parameters_json(&self) -> Option; } #[derive(Clone, Debug)] pub struct VoidWebDriverExtensionCommand; impl WebDriverExtensionCommand for VoidWebDriverExtensionCommand { fn parameters_json(&self) -> Option { panic!("No extensions implemented"); } } #[derive(Debug, PartialEq)] pub struct WebDriverMessage { pub session_id: Option, pub command: WebDriverCommand, } impl WebDriverMessage { pub fn new( session_id: Option, command: WebDriverCommand, ) -> WebDriverMessage { WebDriverMessage { session_id, command, } } pub fn from_http( match_type: Route, params: &Parameters, raw_body: &str, requires_body: bool, ) -> WebDriverResult> { let session_id = WebDriverMessage::::get_session_id(params); let body_data = WebDriverMessage::::decode_body(raw_body, requires_body)?; let command = match match_type { Route::NewSession => WebDriverCommand::NewSession(serde_json::from_str(raw_body)?), Route::DeleteSession => WebDriverCommand::DeleteSession, Route::Get => WebDriverCommand::Get(serde_json::from_str(raw_body)?), Route::GetCurrentUrl => WebDriverCommand::GetCurrentUrl, Route::GoBack => WebDriverCommand::GoBack, Route::GoForward => WebDriverCommand::GoForward, Route::Refresh => WebDriverCommand::Refresh, Route::GetTitle => WebDriverCommand::GetTitle, Route::GetPageSource => WebDriverCommand::GetPageSource, Route::GetWindowHandle => WebDriverCommand::GetWindowHandle, Route::GetWindowHandles => WebDriverCommand::GetWindowHandles, Route::NewWindow => WebDriverCommand::NewWindow(serde_json::from_str(raw_body)?), Route::CloseWindow => WebDriverCommand::CloseWindow, Route::GetTimeouts => WebDriverCommand::GetTimeouts, Route::SetTimeouts => WebDriverCommand::SetTimeouts(serde_json::from_str(raw_body)?), Route::GetWindowRect | Route::GetWindowPosition | Route::GetWindowSize => { WebDriverCommand::GetWindowRect } Route::SetWindowRect | Route::SetWindowPosition | Route::SetWindowSize => { WebDriverCommand::SetWindowRect(serde_json::from_str(raw_body)?) } Route::MinimizeWindow => WebDriverCommand::MinimizeWindow, Route::MaximizeWindow => WebDriverCommand::MaximizeWindow, Route::FullscreenWindow => WebDriverCommand::FullscreenWindow, Route::SwitchToWindow => { WebDriverCommand::SwitchToWindow(serde_json::from_str(raw_body)?) } Route::SwitchToFrame => { WebDriverCommand::SwitchToFrame(serde_json::from_str(raw_body)?) } Route::SwitchToParentFrame => WebDriverCommand::SwitchToParentFrame, Route::FindElement => WebDriverCommand::FindElement(serde_json::from_str(raw_body)?), Route::FindElements => WebDriverCommand::FindElements(serde_json::from_str(raw_body)?), Route::FindElementElement => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::FindElementElement(element, serde_json::from_str(raw_body)?) } Route::FindElementElements => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::FindElementElements(element, serde_json::from_str(raw_body)?) } Route::FindShadowRootElement => { let shadow_id = try_opt!( params.get("shadowId"), ErrorStatus::InvalidArgument, "Missing shadowId parameter" ); let shadow_root = ShadowRoot(shadow_id.as_str().into()); WebDriverCommand::FindShadowRootElement( shadow_root, serde_json::from_str(raw_body)?, ) } Route::FindShadowRootElements => { let shadow_id = try_opt!( params.get("shadowId"), ErrorStatus::InvalidArgument, "Missing shadowId parameter" ); let shadow_root = ShadowRoot(shadow_id.as_str().into()); WebDriverCommand::FindShadowRootElements( shadow_root, serde_json::from_str(raw_body)?, ) } Route::GetActiveElement => WebDriverCommand::GetActiveElement, Route::GetShadowRoot => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetShadowRoot(element) } Route::GetComputedLabel => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetComputedLabel(element) } Route::GetComputedRole => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetComputedRole(element) } Route::IsDisplayed => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::IsDisplayed(element) } Route::IsSelected => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::IsSelected(element) } Route::GetElementAttribute => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); let attr = try_opt!( params.get("name"), ErrorStatus::InvalidArgument, "Missing name parameter" ) .as_str(); WebDriverCommand::GetElementAttribute(element, attr.into()) } Route::GetElementProperty => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); let property = try_opt!( params.get("name"), ErrorStatus::InvalidArgument, "Missing name parameter" ) .as_str(); WebDriverCommand::GetElementProperty(element, property.into()) } Route::GetCSSValue => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); let property = try_opt!( params.get("propertyName"), ErrorStatus::InvalidArgument, "Missing propertyName parameter" ) .as_str(); WebDriverCommand::GetCSSValue(element, property.into()) } Route::GetElementText => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetElementText(element) } Route::GetElementTagName => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetElementTagName(element) } Route::GetElementRect => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::GetElementRect(element) } Route::IsEnabled => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::IsEnabled(element) } Route::ElementClick => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::ElementClick(element) } Route::ElementClear => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::ElementClear(element) } Route::ElementSendKeys => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::ElementSendKeys(element, serde_json::from_str(raw_body)?) } Route::ExecuteScript => { WebDriverCommand::ExecuteScript(serde_json::from_str(raw_body)?) } Route::ExecuteAsyncScript => { WebDriverCommand::ExecuteAsyncScript(serde_json::from_str(raw_body)?) } Route::GetCookies => WebDriverCommand::GetCookies, Route::GetNamedCookie => { let name = try_opt!( params.get("name"), ErrorStatus::InvalidArgument, "Missing 'name' parameter" ) .as_str() .into(); WebDriverCommand::GetNamedCookie(name) } Route::AddCookie => WebDriverCommand::AddCookie(serde_json::from_str(raw_body)?), Route::DeleteCookies => WebDriverCommand::DeleteCookies, Route::DeleteCookie => { let name = try_opt!( params.get("name"), ErrorStatus::InvalidArgument, "Missing name parameter" ) .as_str() .into(); WebDriverCommand::DeleteCookie(name) } Route::PerformActions => { WebDriverCommand::PerformActions(serde_json::from_str(raw_body)?) } Route::ReleaseActions => WebDriverCommand::ReleaseActions, Route::DismissAlert => WebDriverCommand::DismissAlert, Route::AcceptAlert => WebDriverCommand::AcceptAlert, Route::GetAlertText => WebDriverCommand::GetAlertText, Route::SendAlertText => { WebDriverCommand::SendAlertText(serde_json::from_str(raw_body)?) } Route::TakeScreenshot => WebDriverCommand::TakeScreenshot, Route::TakeElementScreenshot => { let element_id = try_opt!( params.get("elementId"), ErrorStatus::InvalidArgument, "Missing elementId parameter" ); let element = WebElement(element_id.as_str().into()); WebDriverCommand::TakeElementScreenshot(element) } Route::Print => WebDriverCommand::Print(serde_json::from_str(raw_body)?), Route::Status => WebDriverCommand::Status, Route::Extension(ref extension) => extension.command(params, &body_data)?, Route::WebAuthnAddVirtualAuthenticator => { WebDriverCommand::WebAuthnAddVirtualAuthenticator(serde_json::from_str(raw_body)?) } Route::WebAuthnRemoveVirtualAuthenticator => { WebDriverCommand::WebAuthnRemoveVirtualAuthenticator } Route::WebAuthnAddCredential => { WebDriverCommand::WebAuthnAddCredential(serde_json::from_str(raw_body)?) } Route::WebAuthnGetCredentials => WebDriverCommand::WebAuthnGetCredentials, Route::WebAuthnRemoveCredential => WebDriverCommand::WebAuthnRemoveCredential, Route::WebAuthnRemoveAllCredentials => WebDriverCommand::WebAuthnRemoveAllCredentials, Route::WebAuthnSetUserVerified => { WebDriverCommand::WebAuthnSetUserVerified(serde_json::from_str(raw_body)?) } }; Ok(WebDriverMessage::new(session_id, command)) } fn get_session_id(params: &Parameters) -> Option { params.get("sessionId").cloned() } fn decode_body(body: &str, requires_body: bool) -> WebDriverResult { if requires_body { match serde_json::from_str(body) { Ok(x @ Value::Object(_)) => Ok(x), Ok(_) => Err(WebDriverError::new( ErrorStatus::InvalidArgument, "Body was not a JSON Object", )), Err(e) => { if e.is_io() { Err(WebDriverError::new( ErrorStatus::InvalidArgument, format!("I/O error whilst decoding body: {}", e), )) } else { let msg = format!("Failed to decode request as JSON: {}", body); let stack = format!("Syntax error at :{}:{}", e.line(), e.column()); Err(WebDriverError::new_with_stack( ErrorStatus::InvalidArgument, msg, stack, )) } } } } else { Ok(Value::Null) } } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct ActionsParameters { pub actions: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(remote = "Self")] pub struct AddCookieParameters { pub name: String, pub value: String, pub path: Option, pub domain: Option, #[serde(default)] pub secure: bool, #[serde(default)] pub httpOnly: bool, #[serde(skip_serializing_if = "Option::is_none")] pub expiry: Option, pub sameSite: Option, } impl<'de> Deserialize<'de> for AddCookieParameters { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct Wrapper { #[serde(with = "AddCookieParameters")] cookie: AddCookieParameters, } Wrapper::deserialize(deserializer).map(|wrapper| wrapper.cookie) } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct GetParameters { pub url: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct GetNamedCookieParameters { pub name: Option, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct JavascriptCommandParameters { pub script: String, pub args: Option>, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct LocatorParameters { pub using: LocatorStrategy, pub value: String, } /// Wrapper around the two supported variants of new session paramters. /// /// The Spec variant is used for storing spec-compliant parameters whereas /// the legacy variant is used to store `desiredCapabilities`/`requiredCapabilities` /// parameters, and is intended to minimise breakage as we transition users to /// the spec design. #[derive(Debug, PartialEq)] pub enum NewSessionParameters { Spec(SpecNewSessionParameters), Legacy(LegacyNewSessionParameters), } impl<'de> Deserialize<'de> for NewSessionParameters { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let value = serde_json::Value::deserialize(deserializer)?; if let Some(caps) = value.get("capabilities") { if !caps.is_object() { return Err(de::Error::custom("capabilities must be objects")); } let caps = SpecNewSessionParameters::deserialize(caps).map_err(de::Error::custom)?; return Ok(NewSessionParameters::Spec(caps)); } warn!("You are using deprecated legacy session negotiation patterns (desiredCapabilities/requiredCapabilities), see https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities#Legacy"); let legacy = LegacyNewSessionParameters::deserialize(value).map_err(de::Error::custom)?; Ok(NewSessionParameters::Legacy(legacy)) } } impl CapabilitiesMatching for NewSessionParameters { fn match_browser( &self, browser_capabilities: &mut T, ) -> WebDriverResult> { match self { NewSessionParameters::Spec(x) => x.match_browser(browser_capabilities), NewSessionParameters::Legacy(x) => x.match_browser(browser_capabilities), } } } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct NewWindowParameters { #[serde(rename = "type")] pub type_hint: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum PrintPageRange { Integer(u64), Range(String), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct PrintParameters { pub orientation: PrintOrientation, #[serde(deserialize_with = "deserialize_to_print_scale_f64")] pub scale: f64, pub background: bool, pub page: PrintPage, pub margin: PrintMargins, pub page_ranges: Vec, 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, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum PrintOrientation { Landscape, #[default] Portrait, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct PrintPage { #[serde(deserialize_with = "deserialize_to_positive_f64")] pub width: f64, #[serde(deserialize_with = "deserialize_to_positive_f64")] pub height: f64, } impl Default for PrintPage { fn default() -> Self { PrintPage { width: 21.59, height: 27.94, } } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct PrintMargins { #[serde(deserialize_with = "deserialize_to_positive_f64")] pub top: f64, #[serde(deserialize_with = "deserialize_to_positive_f64")] pub bottom: f64, #[serde(deserialize_with = "deserialize_to_positive_f64")] pub left: f64, #[serde(deserialize_with = "deserialize_to_positive_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, Serialize, Deserialize, PartialEq)] pub enum WebAuthnProtocol { #[serde(rename = "ctap1/u2f")] Ctap1U2f, #[serde(rename = "ctap2")] Ctap2, #[serde(rename = "ctap2_1")] Ctap2_1, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum AuthenticatorTransport { Usb, Nfc, Ble, SmartCard, Hybrid, Internal, } fn default_as_true() -> bool { true } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AuthenticatorParameters { pub protocol: WebAuthnProtocol, pub transport: AuthenticatorTransport, #[serde(default)] pub has_resident_key: bool, #[serde(default)] pub has_user_verification: bool, #[serde(default = "default_as_true")] pub is_user_consenting: bool, #[serde(default)] pub is_user_verified: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct UserVerificationParameters { #[serde(rename = "isUserVerified")] pub is_user_verified: bool, } fn deserialize_to_positive_f64<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let val = f64::deserialize(deserializer)?; if val < 0.0 { return Err(de::Error::custom(format!("{} is negative", val))); }; Ok(val) } fn deserialize_to_print_scale_f64<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let val = f64::deserialize(deserializer)?; if !(0.1..=2.0).contains(&val) { return Err(de::Error::custom(format!("{} is outside range 0.1-2", val))); }; Ok(val) } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct SendKeysParameters { pub text: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct SwitchToFrameParameters { pub id: FrameId, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct SwitchToWindowParameters { pub handle: String, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct TakeScreenshotParameters { pub element: Option, } #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct TimeoutsParameters { #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_u64" )] pub implicit: Option, #[serde( default, rename = "pageLoad", skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_u64" )] pub page_load: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_nullable_u64" )] #[allow(clippy::option_option)] pub script: Option>, } #[allow(clippy::option_option)] fn deserialize_to_nullable_u64<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { let opt: Option = Option::deserialize(deserializer)?; let value = match opt { Some(n) => { if n < 0.0 || n.fract() != 0.0 { return Err(de::Error::custom(format!( "{} is not a positive Integer", n ))); } if (n as u64) > MAX_SAFE_INTEGER { return Err(de::Error::custom(format!( "{} is greater than maximum safe integer", n ))); } Some(Some(n as u64)) } None => Some(None), }; Ok(value) } fn deserialize_to_u64<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let opt: Option = Option::deserialize(deserializer)?; let value = match opt { Some(n) => { if n < 0.0 || n.fract() != 0.0 { return Err(de::Error::custom(format!( "{} is not a positive Integer", n ))); } if (n as u64) > MAX_SAFE_INTEGER { return Err(de::Error::custom(format!( "{} is greater than maximum safe integer", n ))); } Some(n as u64) } None => return Err(de::Error::custom("null is not a positive integer")), }; Ok(value) } /// A top-level browsing context’s window rect is a dictionary of the /// [`screenX`], [`screenY`], `width`, and `height` attributes of the /// `WindowProxy`. /// /// In some user agents the operating system’s window dimensions, including /// decorations, are provided by the proprietary `window.outerWidth` and /// `window.outerHeight` DOM properties. /// /// [`screenX`]: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-screenx /// [`screenY`]: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-screeny #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct WindowRectParameters { #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_i32" )] pub x: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_i32" )] pub y: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_positive_i32" )] pub width: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_to_positive_i32" )] pub height: Option, } fn deserialize_to_i32<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let opt = Option::deserialize(deserializer)?.map(|value: f64| value as i64); let value = match opt { Some(n) => { if n < i64::from(i32::min_value()) || n > i64::from(i32::max_value()) { return Err(de::Error::custom(format!("'{}' is larger than i32", n))); } Some(n as i32) } None => None, }; Ok(value) } fn deserialize_to_positive_i32<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let opt = Option::deserialize(deserializer)?.map(|value: f64| value as i64); let value = match opt { Some(n) => { if n < 0 || n > i64::from(i32::max_value()) { return Err(de::Error::custom(format!("'{}' is outside of i32", n))); } Some(n as i32) } None => None, }; Ok(value) } #[cfg(test)] mod tests { use super::*; use crate::capabilities::SpecNewSessionParameters; use crate::common::ELEMENT_KEY; use crate::test::assert_de; use serde_json::{self, json}; #[test] fn test_json_actions_parameters_missing_actions_field() { assert!(serde_json::from_value::(json!({})).is_err()); } #[test] fn test_json_actions_parameters_invalid() { assert!(serde_json::from_value::(json!({ "actions": null })).is_err()); } #[test] fn test_json_action_parameters_empty_list() { assert_de( &ActionsParameters { actions: vec![] }, json!({"actions": []}), ); } #[test] fn test_json_action_parameters_with_unknown_field() { assert_de( &ActionsParameters { actions: vec![] }, json!({"actions": [], "foo": "bar"}), ); } #[test] fn test_json_add_cookie_parameters_with_values() { let json = json!({"cookie": { "name": "foo", "value": "bar", "path": "/", "domain": "foo.bar", "expiry": 123, "secure": true, "httpOnly": false, "sameSite": "Lax", }}); let cookie = AddCookieParameters { name: "foo".into(), value: "bar".into(), path: Some("/".into()), domain: Some("foo.bar".into()), expiry: Some(Date(123)), secure: true, httpOnly: false, sameSite: Some("Lax".into()), }; assert_de(&cookie, json); } #[test] fn test_json_add_cookie_parameters_with_optional_null_fields() { let json = json!({"cookie": { "name": "foo", "value": "bar", "path": null, "domain": null, "expiry": null, "secure": true, "httpOnly": false, "sameSite": null, }}); let cookie = AddCookieParameters { name: "foo".into(), value: "bar".into(), path: None, domain: None, expiry: None, secure: true, httpOnly: false, sameSite: None, }; assert_de(&cookie, json); } #[test] fn test_json_add_cookie_parameters_without_optional_fields() { let json = json!({"cookie": { "name": "foo", "value": "bar", "secure": true, "httpOnly": false, }}); let cookie = AddCookieParameters { name: "foo".into(), value: "bar".into(), path: None, domain: None, expiry: None, secure: true, httpOnly: false, sameSite: None, }; assert_de(&cookie, json); } #[test] fn test_json_add_cookie_parameters_with_invalid_cookie_field() { assert!(serde_json::from_value::(json!({"name": "foo"})).is_err()); } #[test] fn test_json_add_cookie_parameters_with_unknown_field() { let json = json!({"cookie": { "name": "foo", "value": "bar", "secure": true, "httpOnly": false, "foo": "bar", }, "baz": "bah"}); let cookie = AddCookieParameters { name: "foo".into(), value: "bar".into(), path: None, domain: None, expiry: None, secure: true, httpOnly: false, sameSite: None, }; assert_de(&cookie, json); } #[test] fn test_json_get_parameters_with_url() { assert_de( &GetParameters { url: "foo.bar".into(), }, json!({"url": "foo.bar"}), ); } #[test] fn test_json_get_parameters_with_invalid_url_value() { assert!(serde_json::from_value::(json!({"url": 3})).is_err()); } #[test] fn test_json_get_parameters_with_invalid_url_field() { assert!(serde_json::from_value::(json!({"foo": "bar"})).is_err()); } #[test] fn test_json_get_parameters_with_unknown_field() { assert_de( &GetParameters { url: "foo.bar".into(), }, json!({"url": "foo.bar", "foo": "bar"}), ); } #[test] fn test_json_get_named_cookie_parameters_with_value() { assert_de( &GetNamedCookieParameters { name: Some("foo".into()), }, json!({"name": "foo"}), ); } #[test] fn test_json_get_named_cookie_parameters_with_optional_null_field() { assert_de( &GetNamedCookieParameters { name: None }, json!({ "name": null }), ); } #[test] fn test_json_get_named_cookie_parameters_without_optional_null_field() { assert_de(&GetNamedCookieParameters { name: None }, json!({})); } #[test] fn test_json_get_named_cookie_parameters_with_invalid_name_field() { assert!(serde_json::from_value::(json!({"name": 3})).is_err()); } #[test] fn test_json_get_named_cookie_parameters_with_unknown_field() { assert_de( &GetNamedCookieParameters { name: Some("foo".into()), }, json!({"name": "foo", "foo": "bar"}), ); } #[test] fn test_json_javascript_command_parameters_with_values() { let json = json!({ "script": "foo", "args": ["1", 2], }); let execute_script = JavascriptCommandParameters { script: "foo".into(), args: Some(vec!["1".into(), 2.into()]), }; assert_de(&execute_script, json); } #[test] fn test_json_javascript_command_parameters_with_optional_null_field() { let json = json!({ "script": "foo", "args": null, }); let execute_script = JavascriptCommandParameters { script: "foo".into(), args: None, }; assert_de(&execute_script, json); } #[test] fn test_json_javascript_command_parameters_without_optional_null_field() { let execute_script = JavascriptCommandParameters { script: "foo".into(), args: None, }; assert_de(&execute_script, json!({"script": "foo"})); } #[test] fn test_json_javascript_command_parameters_invalid_script_field() { let json = json!({ "script": null }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_javascript_command_parameters_invalid_args_field() { let json = json!({ "script": null, "args": "1", }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_javascript_command_parameters_missing_script_field() { let json = json!({ "args": null }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_javascript_command_parameters_with_unknown_field() { let json = json!({ "script": "foo", "foo": "bar", }); let execute_script = JavascriptCommandParameters { script: "foo".into(), args: None, }; assert_de(&execute_script, json); } #[test] fn test_json_locator_parameters_with_values() { let json = json!({ "using": "xpath", "value": "bar", }); let locator = LocatorParameters { using: LocatorStrategy::XPath, value: "bar".into(), }; assert_de(&locator, json); } #[test] fn test_json_locator_parameters_invalid_using_field() { let json = json!({ "using": "foo", "value": "bar", }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_locator_parameters_invalid_value_field() { let json = json!({ "using": "xpath", "value": 3, }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_locator_parameters_missing_using_field() { assert!(serde_json::from_value::(json!({"value": "bar"})).is_err()); } #[test] fn test_json_locator_parameters_missing_value_field() { assert!(serde_json::from_value::(json!({"using": "xpath"})).is_err()); } #[test] fn test_json_locator_parameters_with_unknown_field() { let json = json!({ "using": "xpath", "value": "bar", "foo": "bar", }); let locator = LocatorParameters { using: LocatorStrategy::XPath, value: "bar".into(), }; assert_de(&locator, json); } #[test] fn test_json_new_session_parameters_spec() { let json = json!({"capabilities": { "alwaysMatch": {}, "firstMatch": [{}], }}); let caps = NewSessionParameters::Spec(SpecNewSessionParameters { alwaysMatch: Capabilities::new(), firstMatch: vec![Capabilities::new()], }); assert_de(&caps, json); } #[test] fn test_json_new_session_parameters_capabilities_null() { let json = json!({ "capabilities": null }); assert!(serde_json::from_value::(json).is_err()); } #[test] fn test_json_new_session_parameters_legacy() { let json = json!({ "desiredCapabilities": {}, "requiredCapabilities": {}, }); let caps = NewSessionParameters::Legacy(LegacyNewSessionParameters { desired: Capabilities::new(), required: Capabilities::new(), }); assert_de(&caps, json); } #[test] fn test_json_new_session_parameters_spec_and_legacy() { let json = json!({ "capabilities": { "alwaysMatch": {}, "firstMatch": [{}], }, "desiredCapabilities": {}, "requiredCapabilities": {}, }); let caps = NewSessionParameters::Spec(SpecNewSessionParameters { alwaysMatch: Capabilities::new(), firstMatch: vec![Capabilities::new()], }); assert_de(&caps, json); } #[test] fn test_json_new_session_parameters_with_unknown_field() { let json = json!({ "capabilities": { "alwaysMatch": {}, "firstMatch": [{}] }, "foo": "bar", }); let caps = NewSessionParameters::Spec(SpecNewSessionParameters { alwaysMatch: Capabilities::new(), firstMatch: vec![Capabilities::new()], }); assert_de(&caps, json); } #[test] fn test_json_new_window_parameters_without_type() { assert_de(&NewWindowParameters { type_hint: None }, json!({})); } #[test] fn test_json_new_window_parameters_with_optional_null_type() { assert_de( &NewWindowParameters { type_hint: None }, json!({ "type": null }), ); } #[test] fn test_json_new_window_parameters_with_supported_type() { assert_de( &NewWindowParameters { type_hint: Some("tab".into()), }, json!({"type": "tab"}), ); } #[test] fn test_json_new_window_parameters_with_unknown_type() { assert_de( &NewWindowParameters { type_hint: Some("foo".into()), }, json!({"type": "foo"}), ); } #[test] fn test_json_new_window_parameters_with_invalid_type() { assert!(serde_json::from_value::(json!({"type": 3})).is_err()); } #[test] fn test_json_new_window_parameters_with_unknown_field() { let json = json!({ "type": "tab", "foo": "bar", }); let new_window = NewWindowParameters { type_hint: Some("tab".into()), }; assert_de(&new_window, json); } #[test] fn test_json_print_defaults() { let params = PrintParameters::default(); assert_de(¶ms, json!({})); } #[test] fn test_json_print() { let params = PrintParameters { orientation: PrintOrientation::Landscape, page: PrintPage { width: 10.0, ..Default::default() }, margin: PrintMargins { top: 10.0, ..Default::default() }, scale: 1.5, ..Default::default() }; assert_de( ¶ms, json!({"orientation": "landscape", "page": {"width": 10}, "margin": {"top": 10}, "scale": 1.5}), ); } #[test] fn test_json_scale_invalid() { assert!(serde_json::from_value::(json!({"scale": 3})).is_err()); } #[test] fn test_json_authenticator() { let params = AuthenticatorParameters { protocol: WebAuthnProtocol::Ctap1U2f, transport: AuthenticatorTransport::Usb, has_resident_key: false, has_user_verification: false, is_user_consenting: false, is_user_verified: false, }; assert_de( ¶ms, json!({"protocol": "ctap1/u2f", "transport": "usb", "hasResidentKey": false, "hasUserVerification": false, "isUserConsenting": false, "isUserVerified": false}), ); } #[test] fn test_json_credential() { use base64::{engine::general_purpose::URL_SAFE, Engine}; let encoded_string = URL_SAFE.encode(b"hello internet~"); let params = CredentialParameters { credential_id: r"c3VwZXIgcmVhZGVy".to_string(), is_resident_credential: true, rp_id: "valid.rpid".to_string(), private_key: encoded_string.clone(), user_handle: encoded_string.clone(), sign_count: 0, }; assert_de( ¶ms, json!({"credentialId": r"c3VwZXIgcmVhZGVy", "isResidentCredential": true, "rpId": "valid.rpid", "privateKey": encoded_string, "userHandle": encoded_string, "signCount": 0}), ); } #[test] fn test_json_user_verification() { let params = UserVerificationParameters { is_user_verified: false, }; assert_de(¶ms, json!({"isUserVerified": false})); } #[test] fn test_json_send_keys_parameters_with_value() { assert_de( &SendKeysParameters { text: "foo".into() }, json!({"text": "foo"}), ); } #[test] fn test_json_send_keys_parameters_invalid_text_field() { assert!(serde_json::from_value::(json!({"text": 3})).is_err()); } #[test] fn test_json_send_keys_parameters_missing_text_field() { assert!(serde_json::from_value::(json!({})).is_err()); } #[test] fn test_json_send_keys_parameters_with_unknown_field() { let json = json!({ "text": "foo", "foo": "bar", }); let send_keys = SendKeysParameters { text: "foo".into() }; assert_de(&send_keys, json); } #[test] fn test_json_switch_to_frame_parameters_with_number() { assert_de( &SwitchToFrameParameters { id: FrameId::Short(3), }, json!({"id": 3}), ); } #[test] fn test_json_switch_to_frame_parameters_with_null() { assert_de( &SwitchToFrameParameters { id: FrameId::Top }, json!({"id": null}), ); } #[test] fn test_json_switch_to_frame_parameters_with_web_element() { assert_de( &SwitchToFrameParameters { id: FrameId::Element(WebElement("foo".to_string())), }, json!({"id": {"element-6066-11e4-a52e-4f735466cecf": "foo"}}), ); } #[test] fn test_json_switch_to_frame_parameters_with_missing_id() { assert!(serde_json::from_value::(json!({})).is_err()) } #[test] fn test_json_switch_to_frame_parameters_with_invalid_id_field() { assert!(serde_json::from_value::(json!({"id": "3"})).is_err()); } #[test] fn test_json_switch_to_frame_parameters_with_unknown_field() { let json = json!({ "id":3, "foo": "bar", }); let switch_to_frame = SwitchToFrameParameters { id: FrameId::Short(3), }; assert_de(&switch_to_frame, json); } #[test] fn test_json_switch_to_window_parameters_with_value() { assert_de( &SwitchToWindowParameters { handle: "foo".into(), }, json!({"handle": "foo"}), ); } #[test] fn test_json_switch_to_window_parameters_invalid_handle_field() { assert!(serde_json::from_value::(json!({"handle": 3})).is_err()); } #[test] fn test_json_switch_to_window_parameters_missing_handle_field() { assert!(serde_json::from_value::(json!({})).is_err()); } #[test] fn test_json_switch_to_window_parameters_with_unknown_field() { let json = json!({ "handle": "foo", "foo": "bar", }); let switch_to_window = SwitchToWindowParameters { handle: "foo".into(), }; assert_de(&switch_to_window, json); } #[test] fn test_json_take_screenshot_parameters_with_element() { assert_de( &TakeScreenshotParameters { element: Some(WebElement("elem".into())), }, json!({"element": {ELEMENT_KEY: "elem"}}), ); } #[test] fn test_json_take_screenshot_parameters_with_optional_null_field() { assert_de( &TakeScreenshotParameters { element: None }, json!({ "element": null }), ); } #[test] fn test_json_take_screenshot_parameters_without_optional_null_field() { assert_de(&TakeScreenshotParameters { element: None }, json!({})); } #[test] fn test_json_take_screenshot_parameters_with_invalid_element_field() { assert!( serde_json::from_value::(json!({"element": "foo"})).is_err() ); } #[test] fn test_json_take_screenshot_parameters_with_unknown_field() { let json = json!({ "element": {ELEMENT_KEY: "elem"}, "foo": "bar", }); let take_screenshot = TakeScreenshotParameters { element: Some(WebElement("elem".into())), }; assert_de(&take_screenshot, json); } #[test] fn test_json_timeout_parameters_with_only_null_script_timeout() { let timeouts = TimeoutsParameters { implicit: None, page_load: None, script: Some(None), }; assert_de(&timeouts, json!({ "script": null })); } #[test] fn test_json_timeout_parameters_with_only_null_implicit_timeout() { assert!(serde_json::from_value::(json!({ "implicit": null })).is_err()); } #[test] fn test_json_timeout_parameters_with_only_null_pageload_timeout() { assert!(serde_json::from_value::(json!({ "pageLoad": null })).is_err()); } #[test] fn test_json_timeout_parameters_without_optional_null_field() { let timeouts = TimeoutsParameters { implicit: None, page_load: None, script: None, }; assert_de(&timeouts, json!({})); } #[test] fn test_json_timeout_parameters_with_unknown_field() { let json = json!({ "script": 60000, "foo": "bar", }); let timeouts = TimeoutsParameters { implicit: None, page_load: None, script: Some(Some(60000)), }; assert_de(&timeouts, json); } #[test] fn test_json_window_rect_parameters_with_values() { let json = json!({ "x": 0, "y": 1, "width": 2, "height": 3, }); let rect = WindowRectParameters { x: Some(0i32), y: Some(1i32), width: Some(2i32), height: Some(3i32), }; assert_de(&rect, json); } #[test] fn test_json_window_rect_parameters_with_optional_null_fields() { let json = json!({ "x": null, "y": null, "width": null, "height": null, }); let rect = WindowRectParameters { x: None, y: None, width: None, height: None, }; assert_de(&rect, json); } #[test] fn test_json_window_rect_parameters_without_optional_fields() { let rect = WindowRectParameters { x: None, y: None, width: None, height: None, }; assert_de(&rect, json!({})); } #[test] fn test_json_window_rect_parameters_invalid_values_float() { let json = json!({ "x": 1.1, "y": 2.2, "width": 3.3, "height": 4.4, }); let rect = WindowRectParameters { x: Some(1), y: Some(2), width: Some(3), height: Some(4), }; assert_de(&rect, json); } #[test] fn test_json_window_rect_parameters_with_unknown_field() { let json = json!({ "x": 1.1, "y": 2.2, "foo": "bar", }); let rect = WindowRectParameters { x: Some(1), y: Some(2), width: None, height: None, }; assert_de(&rect, json); } }