From d781d1b1cbb167d6481cad7abcecd2dd3b730a4b Mon Sep 17 00:00:00 2001 From: Euclid Ye Date: Tue, 1 Jul 2025 01:20:52 +0800 Subject: [PATCH] [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 --- components/script/script_thread.rs | 33 +++++ components/script/webdriver_handlers.rs | 134 ++++++++++++++++++ components/shared/embedder/webdriver.rs | 3 + components/webdriver_server/lib.rs | 35 +++-- .../tests/classic/find_element/find.py.ini | 9 -- .../find_element_from_element/find.py.ini | 6 - .../find_element_from_shadow_root/find.py.ini | 3 - .../tests/classic/find_elements/find.py.ini | 9 -- .../find_elements_from_element/find.py.ini | 6 - .../find.py.ini | 3 - 10 files changed, 190 insertions(+), 51 deletions(-) diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 2abe3d98f86..3ea10ca9bdc 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -2270,6 +2270,15 @@ impl ScriptThread { 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) => { webdriver_handlers::handle_find_element_elements_css_selector( &documents, @@ -2303,6 +2312,18 @@ impl ScriptThread { 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( selector, shadow_root_id, @@ -2337,6 +2358,18 @@ impl ScriptThread { 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) => { webdriver_handlers::handle_get_element_shadow_root( &documents, diff --git a/components/script/webdriver_handlers.rs b/components/script/webdriver_handlers.rs index b219da74b77..e72d0d88cad 100644 --- a/components/script/webdriver_handlers.rs +++ b/components/script/webdriver_handlers.rs @@ -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::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializerMethods; +use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{ + XPathResultConstants, XPathResultMethods, +}; use crate::dom::bindings::conversions::{ ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior, 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( } } +/// +fn find_elements_xpath_strategy( + document: &Document, + start_node: &Node, + selector: String, + pipeline: PipelineId, + can_gc: CanGc, +) -> Result, 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::() { + 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, ErrorStatus>>, + can_gc: CanGc, +) { + match retrieve_document_and_check_root_existence(documents, pipeline) { + Ok(document) => reply + .send(find_elements_xpath_strategy( + &document, + document.upcast::(), + selector, + pipeline, + can_gc, + )) + .unwrap(), + Err(error) => reply.send(Err(error)).unwrap(), + } +} + pub(crate) fn handle_find_element_elements_css_selector( documents: &DocumentCollection, pipeline: PipelineId, @@ -823,6 +907,31 @@ pub(crate) fn handle_find_element_elements_tag_name( .unwrap(); } +pub(crate) fn handle_find_element_elements_xpath_selector( + documents: &DocumentCollection, + pipeline: PipelineId, + element_id: String, + selector: String, + reply: IpcSender, 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::(), + selector, + pipeline, + can_gc, + ) + }), + ) + .unwrap(); +} + /// pub(crate) fn handle_find_shadow_elements_css_selector( documents: &DocumentCollection, @@ -902,6 +1011,31 @@ pub(crate) fn handle_find_shadow_elements_tag_name( .unwrap(); } +pub(crate) fn handle_find_shadow_elements_xpath_selector( + documents: &DocumentCollection, + pipeline: PipelineId, + shadow_root_id: String, + selector: String, + reply: IpcSender, 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::(), + selector, + pipeline, + can_gc, + ) + }), + ) + .unwrap(); +} + /// pub(crate) fn handle_get_element_shadow_root( documents: &DocumentCollection, diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index ea4b9eebd55..58fdcdcb4c9 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -134,6 +134,7 @@ pub enum WebDriverScriptCommand { FindElementsCSSSelector(String, IpcSender, ErrorStatus>>), FindElementsLinkText(String, bool, IpcSender, ErrorStatus>>), FindElementsTagName(String, IpcSender, ErrorStatus>>), + FindElementsXpathSelector(String, IpcSender, ErrorStatus>>), FindElementElementsCSSSelector(String, String, IpcSender, ErrorStatus>>), FindElementElementsLinkText( String, @@ -142,6 +143,7 @@ pub enum WebDriverScriptCommand { IpcSender, ErrorStatus>>, ), FindElementElementsTagName(String, String, IpcSender, ErrorStatus>>), + FindElementElementsXPathSelector(String, String, IpcSender, ErrorStatus>>), FindShadowElementsCSSSelector(String, String, IpcSender, ErrorStatus>>), FindShadowElementsLinkText( String, @@ -150,6 +152,7 @@ pub enum WebDriverScriptCommand { IpcSender, ErrorStatus>>, ), FindShadowElementsTagName(String, String, IpcSender, ErrorStatus>>), + FindShadowElementsXPathSelector(String, String, IpcSender, ErrorStatus>>), GetElementShadowRoot(String, IpcSender, ErrorStatus>>), ElementClick(String, IpcSender, ErrorStatus>>), GetActiveElement(IpcSender>), diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index 003964ea5c2..0f011270efe 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -1172,11 +1172,12 @@ impl Handler { WebDriverScriptCommand::FindElementsTagName(parameters.value.clone(), sender); self.browsing_context_script_command::(cmd)?; }, - _ => { - return Err(WebDriverError::new( - ErrorStatus::UnsupportedOperation, - "Unsupported locator strategy", - )); + LocatorStrategy::XPath => { + let cmd = WebDriverScriptCommand::FindElementsXpathSelector( + parameters.value.clone(), + sender, + ); + self.browsing_context_script_command::(cmd)?; }, } @@ -1242,11 +1243,13 @@ impl Handler { ); self.browsing_context_script_command::(cmd)?; }, - _ => { - return Err(WebDriverError::new( - ErrorStatus::UnsupportedOperation, - "Unsupported locator strategy", - )); + LocatorStrategy::XPath => { + let cmd = WebDriverScriptCommand::FindElementElementsXPathSelector( + parameters.value.clone(), + element.to_string(), + sender, + ); + self.browsing_context_script_command::(cmd)?; }, } @@ -1302,11 +1305,13 @@ impl Handler { ); self.browsing_context_script_command::(cmd)?; }, - _ => { - return Err(WebDriverError::new( - ErrorStatus::UnsupportedOperation, - "Unsupported locator strategy", - )); + LocatorStrategy::XPath => { + let cmd = WebDriverScriptCommand::FindShadowElementsXPathSelector( + parameters.value.clone(), + shadow_root.to_string(), + sender, + ); + self.browsing_context_script_command::(cmd)?; }, } diff --git a/tests/wpt/meta/webdriver/tests/classic/find_element/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_element/find.py.ini index 0fd6fbb2bf1..5330ee265a2 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_element/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_element/find.py.ini @@ -1,12 +1,3 @@ [find.py] [test_no_browsing_context] expected: FAIL - - [test_find_element[xpath-//a\]] - expected: FAIL - - [test_xhtml_namespace[xpath-//*[name()='a'\]\]] - expected: FAIL - - [test_htmldocument[xpath-/html\]] - expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/find_element_from_element/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_element_from_element/find.py.ini index d3786ff5851..c8d1c11c7f3 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_element_from_element/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_element_from_element/find.py.ini @@ -2,12 +2,6 @@ [test_no_browsing_context] expected: FAIL - [test_find_element[xpath-//a\]] - expected: FAIL - - [test_xhtml_namespace[xpath-//*[name()='a'\]\]] - expected: FAIL - [test_parent_htmldocument] expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/find_element_from_shadow_root/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_element_from_shadow_root/find.py.ini index 95572375a3c..ecaaaaf9ca6 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_element_from_shadow_root/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_element_from_shadow_root/find.py.ini @@ -2,9 +2,6 @@ [test_no_browsing_context] expected: FAIL - [test_find_element[open-xpath-//a\]] - expected: FAIL - [test_find_element[closed-css selector-#linkText\]] expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/find_elements/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_elements/find.py.ini index 072f59fd4f7..5330ee265a2 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_elements/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_elements/find.py.ini @@ -1,12 +1,3 @@ [find.py] [test_no_browsing_context] expected: FAIL - - [test_find_elements[xpath-//a\]] - expected: FAIL - - [test_xhtml_namespace[xpath-//*[name()='a'\]\]] - expected: FAIL - - [test_htmldocument[xpath-/html\]] - expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/find_elements_from_element/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_elements_from_element/find.py.ini index 1ddbffa1dee..c8d1c11c7f3 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_elements_from_element/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_elements_from_element/find.py.ini @@ -2,12 +2,6 @@ [test_no_browsing_context] expected: FAIL - [test_find_elements[xpath-//a\]] - expected: FAIL - - [test_xhtml_namespace[xpath-//*[name()='a'\]\]] - expected: FAIL - [test_parent_htmldocument] expected: FAIL diff --git a/tests/wpt/meta/webdriver/tests/classic/find_elements_from_shadow_root/find.py.ini b/tests/wpt/meta/webdriver/tests/classic/find_elements_from_shadow_root/find.py.ini index 10233bfa9c3..aa385206d20 100644 --- a/tests/wpt/meta/webdriver/tests/classic/find_elements_from_shadow_root/find.py.ini +++ b/tests/wpt/meta/webdriver/tests/classic/find_elements_from_shadow_root/find.py.ini @@ -2,9 +2,6 @@ [test_no_browsing_context] expected: FAIL - [test_find_elements[open-xpath-//a\]] - expected: FAIL - [test_find_elements[closed-css selector-#linkText\]] expected: FAIL