From 27e31bd7a05e829cc3432e751226a5818a94275e Mon Sep 17 00:00:00 2001 From: forks Date: Sat, 13 Dec 2025 20:56:04 +0800 Subject: [PATCH 1/2] Add ShadowRoot support (GetShadowRoot and find from shadow) --- src/elements.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++++- src/session.rs | 15 ++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/elements.rs b/src/elements.rs index a2672b0..b1c3fec 100644 --- a/src/elements.rs +++ b/src/elements.rs @@ -8,7 +8,7 @@ use serde_json::Value as Json; use std::fmt::{Display, Formatter}; use std::ops::Deref; use webdriver::command::WebDriverCommand; -use webdriver::common::FrameId; +use webdriver::common::{FrameId, SHADOW_KEY}; /// Web element reference. /// @@ -87,6 +87,114 @@ impl Element { pub fn element_id(&self) -> ElementRef { ElementRef(self.element.0.clone()) } + + /// Get the shadow root of the element. + /// + /// Returns a [`ShadowRoot`] that can be used to find elements within the shadow DOM. + /// + /// See [12.6 Get Element Shadow Root](https://www.w3.org/TR/webdriver2/#get-element-shadow-root) + /// of the WebDriver standard. + #[cfg_attr(docsrs, doc(alias = "Get Element Shadow Root"))] + pub async fn shadow_root(&self) -> Result { + 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, + }) + } +} + +/// A shadow root of an element on the current page. +/// +/// A shadow root encapsulates a shadow DOM tree, which is a separate DOM subtree +/// attached to an element. This struct provides methods to find elements within +/// the shadow DOM. +/// +/// See [Shadow Root](https://www.w3.org/TR/webdriver2/#dfn-shadow-roots) of the WebDriver standard. +#[derive(Clone, Debug)] +pub struct ShadowRoot { + /// The high-level WebDriver client, for sending commands. + pub(crate) client: Client, + /// The encapsulated ShadowRoot struct. + pub(crate) shadow_root: webdriver::common::ShadowRoot, +} + +/// Extract the `ShadowRoot` from a `GetShadowRoot` command response. +fn parse_shadow_root(res: Json) -> Result { + 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 { + /// Get back the [`Client`] hosting this `ShadowRoot`. + pub fn client(self) -> Client { + self.client + } +} + +/// [Element Retrieval](https://www.w3.org/TR/webdriver2/#element-retrieval) for Shadow Root +impl ShadowRoot { + /// Find the first element within the shadow root that matches the given [`Locator`]. + /// + /// See [12.4.1 Find Element From Shadow Root](https://www.w3.org/TR/webdriver2/#find-element-from-shadow-root) + /// of the WebDriver standard. + #[cfg_attr(docsrs, doc(alias = "Find Element From Shadow Root"))] + pub async fn find(&self, search: Locator<'_>) -> Result { + 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, + }) + } + + /// Find all elements within the shadow root that match the given [`Locator`]. + /// + /// See [12.4.2 Find Elements From Shadow Root](https://www.w3.org/TR/webdriver2/#find-elements-from-shadow-root) + /// of the WebDriver standard. + #[cfg_attr(docsrs, doc(alias = "Find Elements From Shadow Root"))] + pub async fn find_all(&self, search: Locator<'_>) -> Result, 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()) + } } /// An HTML form on the current page. diff --git a/src/session.rs b/src/session.rs index ae0ca75..258a6c6 100644 --- a/src/session.rs +++ b/src/session.rs @@ -89,6 +89,12 @@ impl WebDriverCompatibleCommand for Wcmd { WebDriverCommand::FindElementElements(ref p, _) => { base.join(&format!("element/{}/elements", p.0)) } + WebDriverCommand::FindShadowRootElement(ref shadow, _) => { + base.join(&format!("shadow/{}/element", shadow.0)) + } + WebDriverCommand::FindShadowRootElements(ref shadow, _) => { + base.join(&format!("shadow/{}/elements", shadow.0)) + } WebDriverCommand::GetActiveElement => base.join("element/active"), WebDriverCommand::IsDisplayed(ref we) => { base.join(&format!("element/{}/displayed", we.0)) @@ -142,9 +148,12 @@ impl WebDriverCompatibleCommand for Wcmd { WebDriverCommand::TakeElementScreenshot(ref we) => { base.join(&format!("element/{}/screenshot", we.0)) } + WebDriverCommand::GetShadowRoot(ref we) => { + base.join(&format!("element/{}/shadow", we.0)) + } WebDriverCommand::Print(..) => base.join("print"), WebDriverCommand::Status => unreachable!(), - _ => unimplemented!(), + _ => unimplemented!("{:?}", self), } } @@ -194,7 +203,9 @@ impl WebDriverCompatibleCommand for Wcmd { WebDriverCommand::FindElement(ref loc) | WebDriverCommand::FindElements(ref loc) | WebDriverCommand::FindElementElement(_, ref loc) - | WebDriverCommand::FindElementElements(_, ref loc) => { + | WebDriverCommand::FindElementElements(_, ref loc) + | WebDriverCommand::FindShadowRootElement(_, ref loc) + | WebDriverCommand::FindShadowRootElements(_, ref loc) => { body = Some(serde_json::to_string(loc).unwrap()); method = Method::POST; } From 1c728a7847bc78bd6cbbd4a78c4340eca1270fab Mon Sep 17 00:00:00 2001 From: Jon Gjengset Date: Sat, 28 Feb 2026 16:18:56 +0100 Subject: [PATCH 2/2] bump ci