use std::sync::Arc;
use serde_json::{Value, json};
use crate::browser::element::Element;
use crate::browser::static_element::StaticElement;
use crate::browser::tab::TabCore;
use crate::locator::{self, Query};
use crate::{Error, Result};
#[derive(Clone)]
pub struct ShadowRoot {
core: Arc<TabCore>,
object_id: String,
frame_id: Option<String>,
}
impl ShadowRoot {
pub(crate) fn new(core: Arc<TabCore>, object_id: String, frame_id: Option<String>) -> Self {
Self {
core,
object_id,
frame_id,
}
}
pub fn object_id(&self) -> &str {
&self.object_id
}
fn frame_id_ref(&self) -> &str {
self.frame_id.as_deref().unwrap_or(&self.core.main_frame_id)
}
async fn call(&self, declaration: &str, extra: Vec<Value>, by_value: bool) -> Result<Value> {
let mut args = vec![json!({ "objectId": self.object_id })];
args.extend(extra);
match &self.frame_id {
Some(fid) => {
self.core
.call_function_in(fid, declaration, args, by_value)
.await
}
None => self.core.call_function(declaration, args, by_value).await,
}
}
fn css_of(selector: &str) -> Result<String> {
match locator::parse(selector) {
Query::Css(sel) => Ok(sel),
Query::Xpath(_) => Err(Error::Other(
"shadow root 内不支持 xpath 定位(document.evaluate 不进 shadow);请用 CSS / tag: / @attr".into(),
)),
}
}
fn make_element(&self, object_id: String) -> Element {
match &self.frame_id {
Some(fid) => Element::new_in_frame(self.core.clone(), object_id, fid.clone()),
None => Element::new(self.core.clone(), object_id),
}
}
pub async fn ele(&self, selector: &str) -> Result<Element> {
let css = Self::css_of(selector)?;
let result = self
.call(
"(root, sel) => root.querySelector(sel)",
vec![json!({ "value": css })],
false,
)
.await?;
match result.get("objectId").and_then(|v| v.as_str()) {
Some(oid) => Ok(self.make_element(oid.to_string())),
None => Err(Error::ElementNotFound(selector.to_string())),
}
}
pub async fn eles(&self, selector: &str) -> Result<Vec<Element>> {
let css = Self::css_of(selector)?;
let result = self
.call(
"(root, sel) => Array.from(root.querySelectorAll(sel))",
vec![json!({ "value": css })],
false,
)
.await?;
let Some(array_object_id) = result.get("objectId").and_then(|v| v.as_str()) else {
return Ok(Vec::new());
};
let oids = self
.core
.node_array_object_ids(self.frame_id_ref(), array_object_id)
.await?;
Ok(oids.into_iter().map(|oid| self.make_element(oid)).collect())
}
pub async fn html(&self) -> Result<String> {
let v = self
.call("root => root.innerHTML ?? ''", vec![], true)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn run_js(&self, body: &str) -> Result<Value> {
let decl = format!("(root) => {{ {body} }}");
self.call(&decl, vec![], true).await
}
pub async fn s_ele(&self, selector: &str) -> Result<StaticElement> {
StaticElement::parse(&self.html().await?)?.ele(selector)
}
pub async fn s_eles(&self, selector: &str) -> Result<Vec<StaticElement>> {
StaticElement::parse(&self.html().await?)?.eles(selector)
}
}
#[cfg(test)]
mod tests {
use super::ShadowRoot;
#[test]
fn css_of_accepts_css_rejects_xpath() {
assert_eq!(ShadowRoot::css_of("#x").unwrap(), "#x");
assert_eq!(ShadowRoot::css_of(".cls").unwrap(), ".cls");
assert_eq!(ShadowRoot::css_of("tag:button").unwrap(), "button");
assert_eq!(ShadowRoot::css_of("css:div.box").unwrap(), "div.box");
assert!(ShadowRoot::css_of("xpath://button").is_err());
assert!(ShadowRoot::css_of("@id:kw").is_err());
assert!(ShadowRoot::css_of("text:提交").is_err());
}
}