use beet_core::prelude::*;
use serde_json::Value;
use serde_json::json;
use super::Session;
#[derive(Debug, Clone)]
pub struct Element {
session: Session,
context_id: String,
handle: String,
}
impl Element {
pub(crate) fn new(
session: &Session,
context_id: &str,
handle: &str,
) -> Self {
Self {
session: session.clone(),
context_id: context_id.to_string(),
handle: handle.to_string(),
}
}
pub(crate) fn from_bidi_response(
session: &Session,
context_id: &str,
resp: &Value,
) -> Option<Self> {
let handle = resp
.pointer("/result/result/handle")
.and_then(|v| v.as_str())
.or_else(|| {
resp.pointer("/result/result/sharedId")
.and_then(|v| v.as_str())
})?;
Some(Self::new(session, context_id, handle))
}
pub fn handle(&self) -> &str { &self.handle }
async fn call_function(
&self,
function_declaration: &str,
arguments: &[Value],
result_ownership_root: bool,
) -> Result<Value> {
let ownership = if result_ownership_root {
"root"
} else {
"none"
};
self.session
.command(
"script.callFunction",
json!({
"functionDeclaration": function_declaration,
"this": { "handle": self.handle },
"arguments": arguments,
"target": { "context": self.context_id },
"awaitPromise": true,
"resultOwnership": ownership
}),
)
.await
}
pub async fn click(&self) -> Result<()> {
self.call_function(
"function(){ this.click(); return true; }",
&[],
false,
)
.await?;
Ok(())
}
pub async fn get_attribute(&self, name: &str) -> Result<Option<String>> {
let resp = self
.call_function(
"function(n){ return this.getAttribute(n); }",
&[json!({ "type": "string", "value": name })],
true,
)
.await?;
let val = resp
.pointer("/result/result/value")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Ok(val)
}
pub async fn set_attribute(&self, name: &str, value: &str) -> Result<()> {
self.call_function(
"function(n,v){ this.setAttribute(n,v); return true; }",
&[
json!({ "type": "string", "value": name }),
json!({ "type": "string", "value": value }),
],
false,
)
.await?;
Ok(())
}
pub async fn remove_attribute(&self, name: &str) -> Result<()> {
self.call_function(
"function(n){ this.removeAttribute(n); return true; }",
&[json!({ "type": "string", "value": name })],
false,
)
.await?;
Ok(())
}
pub async fn get_property(&self, name: &str) -> Result<Value> {
self.call_function(
"function(n){ return this[n]; }",
&[json!({ "type": "string", "value": name })],
true,
)
.await
}
pub async fn set_property(
&self,
name: &str,
value: Value,
) -> Result<Value> {
let bidi_arg = if value.is_string() {
json!({ "type": "string", "value": value.as_str().unwrap() })
} else if value.is_number() {
if let Some(n) = value.as_i64() {
json!({ "type": "number", "value": n })
} else if let Some(n) = value.as_u64() {
json!({ "type": "number", "value": n })
} else if let Some(f) = value.as_f64() {
json!({ "type": "number", "value": f })
} else {
json!({ "type": "undefined" })
}
} else if value.is_boolean() {
json!({ "type": "boolean", "value": value.as_bool().unwrap() })
} else if value.is_null() {
json!({ "type": "null" })
} else {
json!({ "type": "string", "value": value.to_string() })
};
self.call_function(
"function(n,v){ this[n] = v; return this[n]; }",
&[json!({ "type": "string", "value": name }), bidi_arg],
true,
)
.await
}
pub async fn inner_html(&self) -> Result<String> {
let resp = self
.call_function("function(){ return this.innerHTML; }", &[], true)
.await?;
resp.pointer("/result/result/value")
.and_then(|v| v.as_str())
.ok_or_else(|| bevyhow!("innerHTML missing in response"))?
.to_string()
.xok()
}
pub async fn set_inner_html(&self, html: &str) -> Result<()> {
self.call_function(
"function(h){ this.innerHTML = h; return true; }",
&[json!({ "type": "string", "value": html })],
false,
)
.await?;
Ok(())
}
pub async fn inner_text(&self) -> Result<String> {
let resp = self
.call_function("function(){ return this.innerText; }", &[], true)
.await?;
resp.pointer("/result/result/value")
.and_then(|v| v.as_str())
.ok_or_else(|| bevyhow!("innerText missing in response"))?
.to_string()
.xok()
}
pub async fn set_inner_text(&self, html: &str) -> Result<()> {
self.call_function(
"function(h){ this.innerText = h; return true; }",
&[json!({ "type": "string", "value": html })],
false,
)
.await?;
Ok(())
}
pub async fn text_content(&self) -> Result<Option<String>> {
let resp = self
.call_function("function(){ return this.textContent; }", &[], true)
.await?;
let val = resp
.pointer("/result/result/value")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Ok(val)
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
use beet_core::prelude::*;
#[beet_core::test]
async fn visit_and_read_title() {
App::default()
.run_io_task_local(async move {
let (proc, page) =
Page::visit("https://example.com").await.unwrap();
page.current_url()
.await
.unwrap()
.xpect_eq("https://example.com/");
page.query_selector("h1")
.await
.unwrap()
.unwrap()
.inner_text()
.await
.unwrap()
.xpect_eq("Example Domain");
let anchor = page.query_selector("a").await.unwrap().unwrap();
anchor
.inner_text()
.await
.unwrap()
.xpect_eq("More information...");
anchor.click().await.unwrap();
Backoff::default()
.with_max_attempts(10)
.retry_async(|_| async {
match page.current_url().await.unwrap().as_str() {
"https://www.iana.org/help/example-domains" => {
Ok(())
}
_ => Err(()),
}
})
.await
.unwrap();
page.kill().await.unwrap();
proc.kill().unwrap();
})
.await;
}
}