use crate::prelude::*;
use beet_core::prelude::*;
use bevy::prelude::Result;
use serde_json::Value;
use serde_json::json;
#[derive(Debug, Clone)]
pub struct Page {
pub(super) session: Session,
pub(super) context_id: String,
}
impl Page {
pub async fn from_session(session: Session) -> Result<Self> {
let tree = session
.command("browsingContext.getTree", json!({"maxDepth": 0}))
.await?;
let contexts = tree["result"]["contexts"]
.as_array()
.ok_or_else(|| bevyhow!("contexts array missing"))?;
let first = contexts
.get(0)
.and_then(|c| c.get("context"))
.and_then(|c| c.as_str())
.ok_or_else(|| bevyhow!("no top-level context discovered"))?;
Ok(Self {
session,
context_id: first.to_string(),
})
}
pub async fn visit(url: &str) -> Result<(ClientProcess, Self)> {
let process = ClientProcess::new()?;
let session = process.new_session().await?;
let mut page = Self::from_session(session).await?;
page.navigate(url).await?;
Ok((process, page))
}
pub async fn visit_with_client(client: &Client, url: &str) -> Result<Self> {
let session = client.new_session().await?;
let mut page = Self::from_session(session).await?;
page.navigate(url).await?;
Ok(page)
}
pub fn session(&self) -> &Session { &self.session }
pub async fn navigate(&mut self, url: &str) -> Result<()> {
self.session
.command(
"browsingContext.navigate",
json!({
"context": self.context_id,
"url": url,
"wait": "complete"
}),
)
.await
.map_err(|err| {
bevyhow!("navigate to {url} failed, is it a valid url?\n {err}")
})?;
Ok(())
}
pub async fn evaluate(&self, expression: &str) -> Result<Value> {
self.session
.command(
"script.evaluate",
json!({
"expression": expression,
"target": { "context": self.context_id },
"awaitPromise": true,
"resultOwnership": "root"
}),
)
.await
}
pub async fn current_url(&self) -> Result<String> {
self.evaluate("location.href")
.await?
.pointer("/result/result/value")
.and_then(|v| v.as_str())
.ok_or_else(|| bevyhow!("missing location.href value"))?
.to_string()
.xok()
}
pub async fn query_selector(
&self,
selector: &str,
) -> Result<Option<Element>> {
let expr = format!("document.querySelector({selector:?})");
let resp = self.evaluate(&expr).await?;
let ty = resp
.pointer("/result/result/type")
.and_then(|v| v.as_str())
.unwrap_or("undefined");
if ty == "null" || ty == "undefined" {
return Ok(None);
}
if let Some(el) =
Element::from_bidi_response(&self.session, &self.context_id, &resp)
{
Ok(Some(el))
} else {
Err(bevyhow!(
"query_selector: element present but missing BiDi handle/sharedId"
))
}
}
pub async fn kill(self) -> Result<()> { self.session.kill().await }
}
#[cfg(test)]
mod tests {
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.evaluate("document.querySelector('h1')?.textContent")
.await
.unwrap()
.pointer("/result/result/value")
.and_then(|v| v.as_str())
.unwrap()
.to_string()
.xpect_eq("Example Domain");
page.kill().await.unwrap();
proc.kill().unwrap();
})
.await;
}
}