//! Types used to represent particular elements on a page. use crate::wd::Locator; use crate::{error, Client}; use base64::Engine; use serde::Serialize; use serde_json::Value as Json; use std::fmt::{Display, Formatter}; use std::ops::Deref; use webdriver::command::WebDriverCommand; use webdriver::common::FrameId; /// Web element reference. /// /// > Each element has an associated web element reference that uniquely identifies the element /// > across all browsing contexts. The web element reference for every element representing the /// > same element must be the same. It must be a string, and should be the result of generating /// > a UUID. /// /// See [11. Elements](https://www.w3.org/TR/webdriver1/#elements) of the WebDriver standard. #[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct ElementRef(String); impl Display for ElementRef { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl AsRef for ElementRef { fn as_ref(&self) -> &str { &self.0 } } impl Deref for ElementRef { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl From for String { fn from(id: ElementRef) -> Self { id.0 } } impl From for ElementRef { fn from(s: String) -> Self { ElementRef(s) } } /// A single DOM element on the current page. /// /// Note that there is a lot of subtlety in how you can interact with an element through WebDriver, /// which [the WebDriver standard goes into detail on](https://www.w3.org/TR/webdriver1/#elements). /// The same goes for inspecting [element state](https://www.w3.org/TR/webdriver1/#element-state). #[derive(Clone, Debug, Serialize)] pub struct Element { /// The high-level WebDriver client, for sending commands. #[serde(skip_serializing)] pub(crate) client: Client, /// The encapsulated WebElement struct. #[serde(flatten)] pub(crate) element: webdriver::common::WebElement, } impl Element { /// Construct an `Element` with the specified element id. /// The element id is the id given by the webdriver. pub fn from_element_id(client: Client, element_id: ElementRef) -> Self { Self { client, element: webdriver::common::WebElement(element_id.0), } } /// Get back the [`Client`] hosting this `Element`. pub fn client(self) -> Client { self.client } /// Get the element id as given by the webdriver. pub fn element_id(&self) -> ElementRef { ElementRef(self.element.0.clone()) } } /// An HTML form on the current page. #[derive(Clone, Debug)] pub struct Form { pub(crate) client: Client, pub(crate) form: webdriver::common::WebElement, } /// [Command Contexts](https://www.w3.org/TR/webdriver1/#command-contexts) impl Element { /// Switches to the frame contained within the element. /// /// See [10.5 Switch To Frame](https://www.w3.org/TR/webdriver1/#switch-to-frame) of the /// WebDriver standard. #[cfg_attr(docsrs, doc(alias = "Switch To Frame"))] pub async fn enter_frame(&self) -> Result<(), error::CmdError> { let params = webdriver::command::SwitchToFrameParameters { id: Some(FrameId::Element(self.element.clone())), }; self.client .issue(WebDriverCommand::SwitchToFrame(params)) .await?; Ok(()) } } /// [Element Retrieval](https://www.w3.org/TR/webdriver1/#element-retrieval) impl Element { /// Find the first descendant element that matches the given [`Locator`]. /// /// See [12.4 Find Element From /// Element](https://www.w3.org/TR/webdriver1/#find-element-from-element) of the WebDriver /// standard. #[cfg_attr(docsrs, doc(alias = "Find Element From Element"))] pub async fn find(&self, search: Locator<'_>) -> Result { let res = self .client .issue(WebDriverCommand::FindElementElement( self.element.clone(), search.into_parameters(), )) .await?; let e = self.client.parse_lookup(res)?; Ok(Element { client: self.client.clone(), element: e, }) } /// Find all descendant elements that match the given [`Locator`]. /// /// See [12.5 Find Elements From /// Element](https://www.w3.org/TR/webdriver1/#find-elements-from-element) of the WebDriver /// standard. #[cfg_attr(docsrs, doc(alias = "Find Elements From Element"))] pub async fn find_all(&self, search: Locator<'_>) -> Result, error::CmdError> { let res = self .client .issue(WebDriverCommand::FindElementElements( self.element.clone(), search.into_parameters(), )) .await?; let array = self.client.parse_lookup_all(res)?; Ok(array .into_iter() .map(move |e| Element { client: self.client.clone(), element: e, }) .collect()) } } /// [Element State](https://www.w3.org/TR/webdriver1/#element-state) impl Element { /// Return true if the element is currently selected. /// /// See [13.1 Is Element Selected](https://www.w3.org/TR/webdriver1/#is-element-selected) /// of the WebDriver standard. pub async fn is_selected(&self) -> Result { let cmd = WebDriverCommand::IsSelected(self.element.clone()); match self.client.issue(cmd).await? { Json::Bool(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Return true if the element is currently enabled. /// /// See [13.8 Is Element Enabled](https://www.w3.org/TR/webdriver1/#is-element-enabled) /// of the WebDriver standard. pub async fn is_enabled(&self) -> Result { let cmd = WebDriverCommand::IsEnabled(self.element.clone()); match self.client.issue(cmd).await? { Json::Bool(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Return true if the element is currently displayed. /// /// See [Element Displayedness](https://www.w3.org/TR/webdriver1/#element-displayedness) /// of the WebDriver standard. pub async fn is_displayed(&self) -> Result { let cmd = WebDriverCommand::IsDisplayed(self.element.clone()); match self.client.issue(cmd).await? { Json::Bool(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Look up an [attribute] value for this element by name. /// /// `Ok(None)` is returned if the element does not have the given attribute. /// /// See [13.2 Get Element Attribute](https://www.w3.org/TR/webdriver1/#get-element-attribute) /// of the WebDriver standard. /// /// [attribute]: https://dom.spec.whatwg.org/#concept-attribute #[cfg_attr(docsrs, doc(alias = "Get Element Attribute"))] pub async fn attr(&self, attribute: &str) -> Result, error::CmdError> { let cmd = WebDriverCommand::GetElementAttribute(self.element.clone(), attribute.to_string()); match self.client.issue(cmd).await? { Json::String(v) => Ok(Some(v)), Json::Null => Ok(None), v => Err(error::CmdError::NotW3C(v)), } } /// Look up a DOM [property] for this element by name. /// /// `Ok(None)` is returned if the element does not have the given property. /// /// Boolean properties such as "checked" will be returned as the String "true" or "false". /// /// See [13.3 Get Element Property](https://www.w3.org/TR/webdriver1/#get-element-property) /// of the WebDriver standard. /// /// [property]: https://www.ecma-international.org/ecma-262/5.1/#sec-8.12.1 #[cfg_attr(docsrs, doc(alias = "Get Element Property"))] pub async fn prop(&self, prop: &str) -> Result, error::CmdError> { let cmd = WebDriverCommand::GetElementProperty(self.element.clone(), prop.to_string()); match self.client.issue(cmd).await? { Json::String(v) => Ok(Some(v)), Json::Bool(b) => Ok(Some(b.to_string())), Json::Null => Ok(None), v => Err(error::CmdError::NotW3C(v)), } } /// Look up the [computed value] of a CSS property for this element by name. /// /// `Ok(String::new())` is returned if the the given CSS property is not found. /// /// See [13.4 Get Element CSS Value](https://www.w3.org/TR/webdriver1/#get-element-css-value) /// of the WebDriver standard. /// /// [computed value]: https://drafts.csswg.org/css-cascade-4/#computed-value #[cfg_attr(docsrs, doc(alias = "Get Element CSS Value"))] pub async fn css_value(&self, prop: &str) -> Result { let cmd = WebDriverCommand::GetCSSValue(self.element.clone(), prop.to_string()); match self.client.issue(cmd).await? { Json::String(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Retrieve the text contents of this element. /// /// See [13.5 Get Element Text](https://www.w3.org/TR/webdriver1/#get-element-text) /// of the WebDriver standard. #[cfg_attr(docsrs, doc(alias = "Get Element Text"))] pub async fn text(&self) -> Result { let cmd = WebDriverCommand::GetElementText(self.element.clone()); match self.client.issue(cmd).await? { Json::String(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Retrieve the tag name of this element. /// /// See [13.6 Get Element Tag Name](https://www.w3.org/TR/webdriver1/#get-element-tag-name) /// of the WebDriver standard. #[cfg_attr(docsrs, doc(alias = "Get Element Tag Name"))] pub async fn tag_name(&self) -> Result { let cmd = WebDriverCommand::GetElementTagName(self.element.clone()); match self.client.issue(cmd).await? { Json::String(v) => Ok(v), v => Err(error::CmdError::NotW3C(v)), } } /// Gets the x, y, width, and height properties of the current element. /// /// See [13.7 Get Element Rect](https://www.w3.org/TR/webdriver1/#dfn-get-element-rect) of the /// WebDriver standard. #[cfg_attr(docsrs, doc(alias = "Get Element Rect"))] pub async fn rectangle(&self) -> Result<(f64, f64, f64, f64), error::CmdError> { match self .client .issue(WebDriverCommand::GetElementRect(self.element.clone())) .await? { Json::Object(mut obj) => { let x = match obj.remove("x").and_then(|x| x.as_f64()) { Some(x) => x, None => return Err(error::CmdError::NotW3C(Json::Object(obj))), }; let y = match obj.remove("y").and_then(|y| y.as_f64()) { Some(y) => y, None => return Err(error::CmdError::NotW3C(Json::Object(obj))), }; let width = match obj.remove("width").and_then(|width| width.as_f64()) { Some(width) => width, None => return Err(error::CmdError::NotW3C(Json::Object(obj))), }; let height = match obj.remove("height").and_then(|height| height.as_f64()) { Some(height) => height, None => return Err(error::CmdError::NotW3C(Json::Object(obj))), }; Ok((x, y, width, height)) } v => Err(error::CmdError::NotW3C(v)), } } /// Retrieve the HTML contents of this element. /// /// `inner` dictates whether the wrapping node's HTML is excluded or not. For example, take the /// HTML: /// /// ```html ///

/// ``` /// /// With `inner = true`, `
` would be returned. With `inner = false`, /// `

` would be returned instead. #[cfg_attr(docsrs, doc(alias = "innerHTML"))] #[cfg_attr(docsrs, doc(alias = "outerHTML"))] pub async fn html(&self, inner: bool) -> Result { let prop = if inner { "innerHTML" } else { "outerHTML" }; Ok(self.prop(prop).await?.unwrap()) } } /// [Element Interaction](https://www.w3.org/TR/webdriver1/#element-interaction) impl Element { /// Simulate the user clicking on this element. /// /// See [14.1 Element Click](https://www.w3.org/TR/webdriver1/#element-click) of the WebDriver /// standard. #[cfg_attr(docsrs, doc(alias = "Element Click"))] pub async fn click(&self) -> Result<(), error::CmdError> { let cmd = WebDriverCommand::ElementClick(self.element.clone()); let r = self.client.issue(cmd).await?; if r.is_null() || r.as_object().map(|o| o.is_empty()).unwrap_or(false) { // geckodriver returns {} :( Ok(()) } else { Err(error::CmdError::NotW3C(r)) } } /// Clear this element. /// /// See [14.2 Element Clear](https://www.w3.org/TR/webdriver1/#element-clear) of the WebDriver /// standard. #[cfg_attr(docsrs, doc(alias = "Element Clear"))] pub async fn clear(&self) -> Result<(), error::CmdError> { let cmd = WebDriverCommand::ElementClear(self.element.clone()); let r = self.client.issue(cmd).await?; if r.is_null() { Ok(()) } else { Err(error::CmdError::NotW3C(r)) } } /// Simulate the user sending keys to this element. /// /// This operation scrolls into view the form control element and then sends the provided keys /// to the element. In case the element is not keyboard-interactable, an element not /// interactable error is returned. /// /// See [14.3 Element Send Keys](https://www.w3.org/TR/webdriver1/#element-send-keys) of the /// WebDriver standard. #[cfg_attr(docsrs, doc(alias = "Element Send Keys"))] pub async fn send_keys(&self, text: &str) -> Result<(), error::CmdError> { let cmd = WebDriverCommand::ElementSendKeys( self.element.clone(), webdriver::command::SendKeysParameters { text: text.to_owned(), }, ); let r = self.client.issue(cmd).await?; if r.is_null() { Ok(()) } else { Err(error::CmdError::NotW3C(r)) } } } /// [Screen Capture](https://www.w3.org/TR/webdriver1/#screen-capture) impl Element { /// Get a PNG-encoded screenshot of this element. /// /// See [19.2 Take Element Screenshot](https://www.w3.org/TR/webdriver1/#dfn-take-element-screenshot) of the WebDriver /// standard. #[cfg_attr(docsrs, doc(alias = "Take Element Screenshot"))] pub async fn screenshot(&self) -> Result, error::CmdError> { let src = self .client .issue(WebDriverCommand::TakeElementScreenshot( self.element.clone(), )) .await?; if let Some(src) = src.as_str() { base64::engine::general_purpose::STANDARD .decode(src) .map_err(error::CmdError::ImageDecodeError) } else { Err(error::CmdError::NotW3C(src)) } } } /// Higher-level operations. impl Element { /// Follow the `href` target of the element matching the given CSS selector *without* causing a /// click interaction. pub async fn follow(&self) -> Result<(), error::CmdError> { let cmd = WebDriverCommand::GetElementAttribute(self.element.clone(), "href".to_string()); let href = self.client.issue(cmd).await?; let href = match href { Json::String(v) => v, Json::Null => { let e = error::WebDriver::new( error::ErrorStatus::InvalidArgument, "cannot follow element without href attribute", ); return Err(error::CmdError::Standard(e)); } v => return Err(error::CmdError::NotW3C(v)), }; let url = self.client.current_url_().await?; let href = url.join(&href)?; self.client.goto(href.as_str()).await?; Ok(()) } /// Find and click an `