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, SHADOW_KEY};
#[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<str> 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<ElementRef> for String {
fn from(id: ElementRef) -> Self {
id.0
}
}
impl From<String> for ElementRef {
fn from(s: String) -> Self {
ElementRef(s)
}
}
#[derive(Clone, Debug, Serialize)]
pub struct Element {
#[serde(skip_serializing)]
pub(crate) client: Client,
#[serde(flatten)]
pub(crate) element: webdriver::common::WebElement,
}
impl Element {
pub fn from_element_id(client: Client, element_id: ElementRef) -> Self {
Self {
client,
element: webdriver::common::WebElement(element_id.0),
}
}
pub fn client(self) -> Client {
self.client
}
pub fn element_id(&self) -> ElementRef {
ElementRef(self.element.0.clone())
}
#[cfg_attr(docsrs, doc(alias = "Get Element Shadow Root"))]
pub async fn shadow_root(&self) -> Result<ShadowRoot, error::CmdError> {
let res = self
.client
.issue(WebDriverCommand::GetShadowRoot(self.element.clone()))
.await?;
let shadow = parse_shadow_root(res)?;
Ok(ShadowRoot {
client: self.client.clone(),
shadow_root: shadow,
})
}
}
#[derive(Clone, Debug)]
pub struct ShadowRoot {
pub(crate) client: Client,
pub(crate) shadow_root: webdriver::common::ShadowRoot,
}
fn parse_shadow_root(res: Json) -> Result<webdriver::common::ShadowRoot, error::CmdError> {
let mut res = match res {
Json::Object(o) => o,
res => return Err(error::CmdError::NotW3C(res)),
};
if !res.contains_key(SHADOW_KEY) {
return Err(error::CmdError::NotW3C(Json::Object(res)));
}
match res.remove(SHADOW_KEY) {
Some(Json::String(shadow_id)) => Ok(webdriver::common::ShadowRoot(shadow_id)),
Some(v) => {
res.insert(SHADOW_KEY.to_string(), v);
Err(error::CmdError::NotW3C(Json::Object(res)))
}
None => Err(error::CmdError::NotW3C(Json::Object(res))),
}
}
impl ShadowRoot {
pub fn client(self) -> Client {
self.client
}
}
impl ShadowRoot {
#[cfg_attr(docsrs, doc(alias = "Find Element From Shadow Root"))]
pub async fn find(&self, search: Locator<'_>) -> Result<Element, error::CmdError> {
let res = self
.client
.issue(WebDriverCommand::FindShadowRootElement(
self.shadow_root.clone(),
search.into_parameters(),
))
.await?;
let e = self.client.parse_lookup(res)?;
Ok(Element {
client: self.client.clone(),
element: e,
})
}
#[cfg_attr(docsrs, doc(alias = "Find Elements From Shadow Root"))]
pub async fn find_all(&self, search: Locator<'_>) -> Result<Vec<Element>, error::CmdError> {
let res = self
.client
.issue(WebDriverCommand::FindShadowRootElements(
self.shadow_root.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())
}
}
#[derive(Clone, Debug)]
pub struct Form {
pub(crate) client: Client,
pub(crate) form: webdriver::common::WebElement,
}
impl Element {
#[cfg_attr(docsrs, doc(alias = "Switch To Frame"))]
pub async fn enter_frame(&self) -> Result<(), error::CmdError> {
let params = webdriver::command::SwitchToFrameParameters {
id: FrameId::Element(self.element.clone()),
};
self.client
.issue(WebDriverCommand::SwitchToFrame(params))
.await?;
Ok(())
}
}
impl Element {
#[cfg_attr(docsrs, doc(alias = "Find Element From Element"))]
pub async fn find(&self, search: Locator<'_>) -> Result<Element, error::CmdError> {
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,
})
}
#[cfg_attr(docsrs, doc(alias = "Find Elements From Element"))]
pub async fn find_all(&self, search: Locator<'_>) -> Result<Vec<Element>, 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())
}
}
impl Element {
pub async fn is_selected(&self) -> Result<bool, error::CmdError> {
let cmd = WebDriverCommand::IsSelected(self.element.clone());
match self.client.issue(cmd).await? {
Json::Bool(v) => Ok(v),
v => Err(error::CmdError::NotW3C(v)),
}
}
pub async fn is_enabled(&self) -> Result<bool, error::CmdError> {
let cmd = WebDriverCommand::IsEnabled(self.element.clone());
match self.client.issue(cmd).await? {
Json::Bool(v) => Ok(v),
v => Err(error::CmdError::NotW3C(v)),
}
}
pub async fn is_displayed(&self) -> Result<bool, error::CmdError> {
let cmd = WebDriverCommand::IsDisplayed(self.element.clone());
match self.client.issue(cmd).await? {
Json::Bool(v) => Ok(v),
v => Err(error::CmdError::NotW3C(v)),
}
}
#[cfg_attr(docsrs, doc(alias = "Get Element Attribute"))]
pub async fn attr(&self, attribute: &str) -> Result<Option<String>, 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)),
}
}
#[cfg_attr(docsrs, doc(alias = "Get Element Property"))]
pub async fn prop(&self, prop: &str) -> Result<Option<String>, 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)),
}
}
#[cfg_attr(docsrs, doc(alias = "Get Element CSS Value"))]
pub async fn css_value(&self, prop: &str) -> Result<String, error::CmdError> {
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)),
}
}
#[cfg_attr(docsrs, doc(alias = "Get Element Text"))]
pub async fn text(&self) -> Result<String, error::CmdError> {
let cmd = WebDriverCommand::GetElementText(self.element.clone());
match self.client.issue(cmd).await? {
Json::String(v) => Ok(v),
v => Err(error::CmdError::NotW3C(v)),
}
}
#[cfg_attr(docsrs, doc(alias = "Get Element Tag Name"))]
pub async fn tag_name(&self) -> Result<String, error::CmdError> {
let cmd = WebDriverCommand::GetElementTagName(self.element.clone());
match self.client.issue(cmd).await? {
Json::String(v) => Ok(v),
v => Err(error::CmdError::NotW3C(v)),
}
}
#[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)),
}
}
#[cfg_attr(docsrs, doc(alias = "innerHTML"))]
#[cfg_attr(docsrs, doc(alias = "outerHTML"))]
pub async fn html(&self, inner: bool) -> Result<String, error::CmdError> {
let prop = if inner { "innerHTML" } else { "outerHTML" };
Ok(self.prop(prop).await?.unwrap())
}
}
impl Element {
#[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) {
Ok(())
} else {
Err(error::CmdError::NotW3C(r))
}
}
#[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))
}
}
#[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))
}
}
}
impl Element {
#[cfg_attr(docsrs, doc(alias = "Take Element Screenshot"))]
pub async fn screenshot(&self) -> Result<Vec<u8>, 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))
}
}
}
impl Element {
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(())
}
pub async fn select_by(&self, locator: Locator<'_>) -> Result<(), error::CmdError> {
self.find(locator).await?.click().await
}
pub async fn select_by_value(&self, value: &str) -> Result<(), error::CmdError> {
self.select_by(Locator::Css(&format!("option[value='{}']", value)))
.await
}
pub async fn select_by_index(&self, index: usize) -> Result<(), error::CmdError> {
self.select_by(Locator::Css(&format!("option:nth-of-type({})", index + 1)))
.await
}
pub async fn select_by_label(&self, label: &str) -> Result<(), error::CmdError> {
self.select_by(Locator::XPath(&format!(r".//option[.='{}']", label)))
.await
}
}
impl Form {
pub fn client(self) -> Client {
self.client
}
}
impl Form {
pub async fn set(&self, locator: Locator<'_>, value: &str) -> Result<Self, error::CmdError> {
let locator =
WebDriverCommand::FindElementElement(self.form.clone(), locator.into_parameters());
let value = Json::from(value);
let res = self.client.issue(locator).await?;
let field = self.client.parse_lookup(res)?;
let args = vec![via_json!(&field), value];
let cmd = webdriver::command::JavascriptCommandParameters {
script: "arguments[0].value = arguments[1]".to_string(),
args: Some(args),
};
let res = self
.client
.issue(WebDriverCommand::ExecuteScript(cmd))
.await?;
if res.is_null() {
Ok(Form {
client: self.client.clone(),
form: self.form.clone(),
})
} else {
Err(error::CmdError::NotW3C(res))
}
}
pub async fn set_by_name(&self, field: &str, value: &str) -> Result<Self, error::CmdError> {
let locator = format!("[name='{}']", field);
let locator = Locator::Css(&locator);
self.set(locator, value).await
}
}
impl Form {
pub async fn submit(&self) -> Result<(), error::CmdError> {
self.submit_with(Locator::Css("input[type=submit],button[type=submit]"))
.await
}
pub async fn submit_with(&self, button: Locator<'_>) -> Result<(), error::CmdError> {
let locator =
WebDriverCommand::FindElementElement(self.form.clone(), button.into_parameters());
let res = self.client.issue(locator).await?;
let submit = self.client.parse_lookup(res)?;
let res = self
.client
.issue(WebDriverCommand::ElementClick(submit))
.await?;
if res.is_null() || res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
Ok(())
} else {
Err(error::CmdError::NotW3C(res))
}
}
pub async fn submit_using(&self, button_label: &str) -> Result<(), error::CmdError> {
let escaped = button_label.replace('\\', "\\\\").replace('"', "\\\"");
let btn = format!(
"input[type=submit][value=\"{}\" i],\
button[type=submit][value=\"{}\" i]",
escaped, escaped
);
self.submit_with(Locator::Css(&btn)).await
}
pub async fn submit_direct(&self) -> Result<(), error::CmdError> {
let args = vec![via_json!(&self.form)];
let cmd = webdriver::command::JavascriptCommandParameters {
script: "document.createElement('form').submit.call(arguments[0])".to_string(),
args: Some(args),
};
let res = self
.client
.issue(WebDriverCommand::ExecuteScript(cmd))
.await?;
if res.is_null() || res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
Ok(())
} else {
Err(error::CmdError::NotW3C(res))
}
}
pub async fn submit_sneaky(&self, field: &str, value: &str) -> Result<(), error::CmdError> {
let args = vec![via_json!(&self.form), Json::from(field), Json::from(value)];
let cmd = webdriver::command::JavascriptCommandParameters {
script: "\
var h = document.createElement('input');\
h.setAttribute('type', 'hidden');\
h.setAttribute('name', arguments[1]);\
h.value = arguments[2];\
arguments[0].appendChild(h)"
.to_string(),
args: Some(args),
};
let res = self
.client
.issue(WebDriverCommand::ExecuteScript(cmd))
.await?;
if res.is_null() | res.as_object().map(|o| o.is_empty()).unwrap_or(false) {
self.submit_direct().await
} else {
Err(error::CmdError::NotW3C(res))
}
}
}