[WebDriver] Implement XPath Locator Strategy (#37783)

1. Implement XPath Locator Strategy
2. Use it for "Find Element(s)", "Find Element(s) from Element", "Find
Element(s) from Shadow Root"

Testing: `tests\wpt\tests\webdriver\tests\classic\find_element*\find.py`

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
This commit is contained in:
Euclid Ye 2025-07-01 01:20:52 +08:00 committed by GitHub
parent f682f9d6f5
commit d781d1b1cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 190 additions and 51 deletions

View file

@ -2270,6 +2270,15 @@ impl ScriptThread {
can_gc, can_gc,
) )
}, },
WebDriverScriptCommand::FindElementsXpathSelector(selector, reply) => {
webdriver_handlers::handle_find_elements_xpath_selector(
&documents,
pipeline_id,
selector,
reply,
can_gc,
)
},
WebDriverScriptCommand::FindElementElementsCSSSelector(selector, element_id, reply) => { WebDriverScriptCommand::FindElementElementsCSSSelector(selector, element_id, reply) => {
webdriver_handlers::handle_find_element_elements_css_selector( webdriver_handlers::handle_find_element_elements_css_selector(
&documents, &documents,
@ -2303,6 +2312,18 @@ impl ScriptThread {
can_gc, can_gc,
) )
}, },
WebDriverScriptCommand::FindElementElementsXPathSelector(
selector,
element_id,
reply,
) => webdriver_handlers::handle_find_element_elements_xpath_selector(
&documents,
pipeline_id,
element_id,
selector,
reply,
can_gc,
),
WebDriverScriptCommand::FindShadowElementsCSSSelector( WebDriverScriptCommand::FindShadowElementsCSSSelector(
selector, selector,
shadow_root_id, shadow_root_id,
@ -2337,6 +2358,18 @@ impl ScriptThread {
reply, reply,
) )
}, },
WebDriverScriptCommand::FindShadowElementsXPathSelector(
selector,
shadow_root_id,
reply,
) => webdriver_handlers::handle_find_shadow_elements_xpath_selector(
&documents,
pipeline_id,
shadow_root_id,
selector,
reply,
can_gc,
),
WebDriverScriptCommand::GetElementShadowRoot(element_id, reply) => { WebDriverScriptCommand::GetElementShadowRoot(element_id, reply) => {
webdriver_handlers::handle_get_element_shadow_root( webdriver_handlers::handle_get_element_shadow_root(
&documents, &documents,

View file

@ -43,6 +43,9 @@ use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelec
use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods}; use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, NodeMethods};
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods; use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods;
use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{
XPathResultConstants, XPathResultMethods,
};
use crate::dom::bindings::conversions::{ use crate::dom::bindings::conversions::{
ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior, ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior,
get_property, get_property_jsval, jsid_to_string, jsstring_to_str, root_from_object, get_property, get_property_jsval, jsid_to_string, jsstring_to_str, root_from_object,
@ -759,6 +762,87 @@ pub(crate) fn handle_find_elements_tag_name(
} }
} }
/// <https://w3c.github.io/webdriver/#xpath>
fn find_elements_xpath_strategy(
document: &Document,
start_node: &Node,
selector: String,
pipeline: PipelineId,
can_gc: CanGc,
) -> Result<Vec<String>, ErrorStatus> {
// Step 1. Let evaluateResult be the result of calling evaluate,
// with arguments selector, start node, null, ORDERED_NODE_SNAPSHOT_TYPE, and null.
// A snapshot is used to promote operation atomicity.
let evaluate_result = match document.Evaluate(
DOMString::from(selector),
start_node,
None,
XPathResultConstants::ORDERED_NODE_SNAPSHOT_TYPE,
None,
can_gc,
) {
Ok(res) => res,
Err(_) => return Err(ErrorStatus::InvalidSelector),
};
// Step 2. Let index be 0. (Handled altogether in Step 5.)
// Step 3: Let length be the result of getting the property "snapshotLength"
// from evaluateResult.
let length = match evaluate_result.GetSnapshotLength() {
Ok(len) => len,
Err(_) => return Err(ErrorStatus::InvalidSelector),
};
// Step 4: Prepare result vector
let mut result = Vec::new();
// Step 5: Repeat, while index is less than length:
for index in 0..length {
// Step 5.1. Let node be the result of calling snapshotItem with
// evaluateResult as this and index as the argument.
let node = match evaluate_result.SnapshotItem(index) {
Ok(node) => node.expect(
"Node should always exist as ORDERED_NODE_SNAPSHOT_TYPE \
gives static result and we verified the length!",
),
Err(_) => return Err(ErrorStatus::InvalidSelector),
};
// Step 5.2. If node is not an element return an error with error code invalid selector.
if !node.is::<Element>() {
return Err(ErrorStatus::InvalidSelector);
}
// Step 5.3. Append node to result.
result.push(node.unique_id(pipeline));
}
// Step 6. Return success with data result.
Ok(result)
}
pub(crate) fn handle_find_elements_xpath_selector(
documents: &DocumentCollection,
pipeline: PipelineId,
selector: String,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc,
) {
match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send(find_elements_xpath_strategy(
&document,
document.upcast::<Node>(),
selector,
pipeline,
can_gc,
))
.unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
}
pub(crate) fn handle_find_element_elements_css_selector( pub(crate) fn handle_find_element_elements_css_selector(
documents: &DocumentCollection, documents: &DocumentCollection,
pipeline: PipelineId, pipeline: PipelineId,
@ -823,6 +907,31 @@ pub(crate) fn handle_find_element_elements_tag_name(
.unwrap(); .unwrap();
} }
pub(crate) fn handle_find_element_elements_xpath_selector(
documents: &DocumentCollection,
pipeline: PipelineId,
element_id: String,
selector: String,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc,
) {
reply
.send(
get_known_element(documents, pipeline, element_id).and_then(|element| {
find_elements_xpath_strategy(
&documents
.find_document(pipeline)
.expect("Document existence guaranteed by `get_known_element`"),
element.upcast::<Node>(),
selector,
pipeline,
can_gc,
)
}),
)
.unwrap();
}
/// <https://w3c.github.io/webdriver/#find-elements-from-shadow-root> /// <https://w3c.github.io/webdriver/#find-elements-from-shadow-root>
pub(crate) fn handle_find_shadow_elements_css_selector( pub(crate) fn handle_find_shadow_elements_css_selector(
documents: &DocumentCollection, documents: &DocumentCollection,
@ -902,6 +1011,31 @@ pub(crate) fn handle_find_shadow_elements_tag_name(
.unwrap(); .unwrap();
} }
pub(crate) fn handle_find_shadow_elements_xpath_selector(
documents: &DocumentCollection,
pipeline: PipelineId,
shadow_root_id: String,
selector: String,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc,
) {
reply
.send(
get_known_shadow_root(documents, pipeline, shadow_root_id).and_then(|shadow_root| {
find_elements_xpath_strategy(
&documents
.find_document(pipeline)
.expect("Document existence guaranteed by `get_known_shadow_root`"),
shadow_root.upcast::<Node>(),
selector,
pipeline,
can_gc,
)
}),
)
.unwrap();
}
/// <https://www.w3.org/TR/webdriver2/#dfn-get-element-shadow-root> /// <https://www.w3.org/TR/webdriver2/#dfn-get-element-shadow-root>
pub(crate) fn handle_get_element_shadow_root( pub(crate) fn handle_get_element_shadow_root(
documents: &DocumentCollection, documents: &DocumentCollection,

View file

@ -134,6 +134,7 @@ pub enum WebDriverScriptCommand {
FindElementsCSSSelector(String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementsCSSSelector(String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementsLinkText(String, bool, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementsLinkText(String, bool, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementsTagName(String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementsTagName(String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementsXpathSelector(String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementElementsLinkText( FindElementElementsLinkText(
String, String,
@ -142,6 +143,7 @@ pub enum WebDriverScriptCommand {
IpcSender<Result<Vec<String>, ErrorStatus>>, IpcSender<Result<Vec<String>, ErrorStatus>>,
), ),
FindElementElementsTagName(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementElementsTagName(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementElementsXPathSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindShadowElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindShadowElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindShadowElementsLinkText( FindShadowElementsLinkText(
String, String,
@ -150,6 +152,7 @@ pub enum WebDriverScriptCommand {
IpcSender<Result<Vec<String>, ErrorStatus>>, IpcSender<Result<Vec<String>, ErrorStatus>>,
), ),
FindShadowElementsTagName(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindShadowElementsTagName(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindShadowElementsXPathSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
GetElementShadowRoot(String, IpcSender<Result<Option<String>, ErrorStatus>>), GetElementShadowRoot(String, IpcSender<Result<Option<String>, ErrorStatus>>),
ElementClick(String, IpcSender<Result<Option<String>, ErrorStatus>>), ElementClick(String, IpcSender<Result<Option<String>, ErrorStatus>>),
GetActiveElement(IpcSender<Option<String>>), GetActiveElement(IpcSender<Option<String>>),

View file

@ -1172,11 +1172,12 @@ impl Handler {
WebDriverScriptCommand::FindElementsTagName(parameters.value.clone(), sender); WebDriverScriptCommand::FindElementsTagName(parameters.value.clone(), sender);
self.browsing_context_script_command::<true>(cmd)?; self.browsing_context_script_command::<true>(cmd)?;
}, },
_ => { LocatorStrategy::XPath => {
return Err(WebDriverError::new( let cmd = WebDriverScriptCommand::FindElementsXpathSelector(
ErrorStatus::UnsupportedOperation, parameters.value.clone(),
"Unsupported locator strategy", sender,
)); );
self.browsing_context_script_command::<true>(cmd)?;
}, },
} }
@ -1242,11 +1243,13 @@ impl Handler {
); );
self.browsing_context_script_command::<true>(cmd)?; self.browsing_context_script_command::<true>(cmd)?;
}, },
_ => { LocatorStrategy::XPath => {
return Err(WebDriverError::new( let cmd = WebDriverScriptCommand::FindElementElementsXPathSelector(
ErrorStatus::UnsupportedOperation, parameters.value.clone(),
"Unsupported locator strategy", element.to_string(),
)); sender,
);
self.browsing_context_script_command::<true>(cmd)?;
}, },
} }
@ -1302,11 +1305,13 @@ impl Handler {
); );
self.browsing_context_script_command::<true>(cmd)?; self.browsing_context_script_command::<true>(cmd)?;
}, },
_ => { LocatorStrategy::XPath => {
return Err(WebDriverError::new( let cmd = WebDriverScriptCommand::FindShadowElementsXPathSelector(
ErrorStatus::UnsupportedOperation, parameters.value.clone(),
"Unsupported locator strategy", shadow_root.to_string(),
)); sender,
);
self.browsing_context_script_command::<true>(cmd)?;
}, },
} }

View file

@ -1,12 +1,3 @@
[find.py] [find.py]
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_element[xpath-//a\]]
expected: FAIL
[test_xhtml_namespace[xpath-//*[name()='a'\]\]]
expected: FAIL
[test_htmldocument[xpath-/html\]]
expected: FAIL

View file

@ -2,12 +2,6 @@
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_element[xpath-//a\]]
expected: FAIL
[test_xhtml_namespace[xpath-//*[name()='a'\]\]]
expected: FAIL
[test_parent_htmldocument] [test_parent_htmldocument]
expected: FAIL expected: FAIL

View file

@ -2,9 +2,6 @@
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_element[open-xpath-//a\]]
expected: FAIL
[test_find_element[closed-css selector-#linkText\]] [test_find_element[closed-css selector-#linkText\]]
expected: FAIL expected: FAIL

View file

@ -1,12 +1,3 @@
[find.py] [find.py]
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_elements[xpath-//a\]]
expected: FAIL
[test_xhtml_namespace[xpath-//*[name()='a'\]\]]
expected: FAIL
[test_htmldocument[xpath-/html\]]
expected: FAIL

View file

@ -2,12 +2,6 @@
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_elements[xpath-//a\]]
expected: FAIL
[test_xhtml_namespace[xpath-//*[name()='a'\]\]]
expected: FAIL
[test_parent_htmldocument] [test_parent_htmldocument]
expected: FAIL expected: FAIL

View file

@ -2,9 +2,6 @@
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_find_elements[open-xpath-//a\]]
expected: FAIL
[test_find_elements[closed-css selector-#linkText\]] [test_find_elements[closed-css selector-#linkText\]]
expected: FAIL expected: FAIL