[WebDriver] Properly report error for find_element & find_elements; Get correct visible text when matching links (#37452)

1. Properly report new types of errors for `find_element` and
`find_elements`. Previously never reported.
1.1. `InvalidSelector`
1.2. `NoSuchElement`
1.3. `InvalidArgument`

2. Get the visible text for `<a>` correctly in
`script::webdriver_handler` so that matching would work.

Testing: `./mach test-wpt -r --log-raw "D:\servo test log\all.txt"
webdriver/tests/classic/find_element/find.py
webdriver/tests/classic/find_elements/find.py --product servodriver`

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
This commit is contained in:
Euclid Ye 2025-06-16 13:23:26 +08:00 committed by GitHub
parent 62b078dd62
commit f8f7c6ebd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 138 additions and 122 deletions

View file

@ -2278,6 +2278,7 @@ impl ScriptThread {
selector, selector,
partial, partial,
reply, reply,
can_gc,
) )
}, },
WebDriverScriptCommand::FindElementTagName(selector, reply) => { WebDriverScriptCommand::FindElementTagName(selector, reply) => {
@ -2304,6 +2305,7 @@ impl ScriptThread {
selector, selector,
partial, partial,
reply, reply,
can_gc,
) )
}, },
WebDriverScriptCommand::FindElementsTagName(selector, reply) => { WebDriverScriptCommand::FindElementsTagName(selector, reply) => {
@ -2336,6 +2338,7 @@ impl ScriptThread {
selector, selector,
partial, partial,
reply, reply,
can_gc,
), ),
WebDriverScriptCommand::FindElementElementTagName(selector, element_id, reply) => { WebDriverScriptCommand::FindElementElementTagName(selector, element_id, reply) => {
webdriver_handlers::handle_find_element_element_tag_name( webdriver_handlers::handle_find_element_element_tag_name(
@ -2368,6 +2371,7 @@ impl ScriptThread {
selector, selector,
partial, partial,
reply, reply,
can_gc,
), ),
WebDriverScriptCommand::FindElementElementsTagName(selector, element_id, reply) => { WebDriverScriptCommand::FindElementElementsTagName(selector, element_id, reply) => {
webdriver_handlers::handle_find_element_elements_tag_name( webdriver_handlers::handle_find_element_elements_tag_name(

View file

@ -104,16 +104,19 @@ pub(crate) fn find_node_by_unique_id_in_document(
} }
} }
/// <https://w3c.github.io/webdriver/#dfn-link-text-selector>
fn matching_links( fn matching_links(
links: &NodeList, links: &NodeList,
link_text: String, link_text: String,
partial: bool, partial: bool,
can_gc: CanGc,
) -> impl Iterator<Item = String> + '_ { ) -> impl Iterator<Item = String> + '_ {
links links
.iter() .iter()
.filter(move |node| { .filter(move |node| {
let content = node let content = node
.GetTextContent() .downcast::<HTMLElement>()
.map(|element| element.InnerText(can_gc))
.map_or("".to_owned(), String::from) .map_or("".to_owned(), String::from)
.trim() .trim()
.to_owned(); .to_owned();
@ -123,32 +126,41 @@ fn matching_links(
content == link_text content == link_text
} }
}) })
.map(|node| { .map(|node| node.unique_id(node.owner_doc().window().pipeline_id()))
node.upcast::<Node>()
.unique_id(node.owner_doc().window().pipeline_id())
})
} }
fn all_matching_links( fn all_matching_links(
root_node: &Node, root_node: &Node,
link_text: String, link_text: String,
partial: bool, partial: bool,
can_gc: CanGc,
) -> Result<Vec<String>, ErrorStatus> { ) -> Result<Vec<String>, ErrorStatus> {
// <https://w3c.github.io/webdriver/#dfn-find>
// Step 7.2. If a DOMException, SyntaxError, XPathException, or other error occurs
// during the execution of the element location strategy, return error invalid selector.
root_node root_node
.query_selector_all(DOMString::from("a")) .query_selector_all(DOMString::from("a"))
.map_err(|_| ErrorStatus::UnknownError) .map_err(|_| ErrorStatus::InvalidSelector)
.map(|nodes| matching_links(&nodes, link_text, partial).collect()) .map(|nodes| matching_links(&nodes, link_text, partial, can_gc).collect())
} }
fn first_matching_link( fn first_matching_link(
root_node: &Node, root_node: &Node,
link_text: String, link_text: String,
partial: bool, partial: bool,
can_gc: CanGc,
) -> Result<Option<String>, ErrorStatus> { ) -> Result<Option<String>, ErrorStatus> {
// <https://w3c.github.io/webdriver/#dfn-find>
// Step 7.2. If a DOMException, SyntaxError, XPathException, or other error occurs
// during the execution of the element location strategy, return error invalid selector.
root_node root_node
.query_selector_all(DOMString::from("a")) .query_selector_all(DOMString::from("a"))
.map_err(|_| ErrorStatus::UnknownError) .map_err(|_| ErrorStatus::InvalidSelector)
.map(|nodes| matching_links(&nodes, link_text, partial).take(1).next()) .map(|nodes| {
matching_links(&nodes, link_text, partial, can_gc)
.take(1)
.next()
})
} }
#[allow(unsafe_code)] #[allow(unsafe_code)]
@ -584,25 +596,42 @@ pub(crate) fn handle_get_element_in_view_center_point(
.unwrap(); .unwrap();
} }
fn retrieve_document_and_check_root_existence(
documents: &DocumentCollection,
pipeline: PipelineId,
) -> Result<DomRoot<Document>, ErrorStatus> {
let document = documents
.find_document(pipeline)
.ok_or(ErrorStatus::NoSuchWindow)?;
// <https://w3c.github.io/webdriver/#find-element>
// <https://w3c.github.io/webdriver/#find-elements>
// Step 7 - 8. If current browsing context's document element is null,
// return error with error code no such element.
if document.GetDocumentElement().is_none() {
Err(ErrorStatus::NoSuchElement)
} else {
Ok(document)
}
}
pub(crate) fn handle_find_element_css( pub(crate) fn handle_find_element_css(
documents: &DocumentCollection, documents: &DocumentCollection,
pipeline: PipelineId, pipeline: PipelineId,
selector: String, selector: String,
reply: IpcSender<Result<Option<String>, ErrorStatus>>, reply: IpcSender<Result<Option<String>, ErrorStatus>>,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send( .send(
documents
.find_document(pipeline)
.ok_or(ErrorStatus::UnknownError)
.and_then(|document| {
document document
.QuerySelector(DOMString::from(selector)) .QuerySelector(DOMString::from(selector))
.map_err(|_| ErrorStatus::InvalidSelector) .map_err(|_| ErrorStatus::InvalidSelector)
})
.map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))), .map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))),
) )
.unwrap(); .unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
} }
pub(crate) fn handle_find_element_link_text( pub(crate) fn handle_find_element_link_text(
@ -611,17 +640,19 @@ pub(crate) fn handle_find_element_link_text(
selector: String, selector: String,
partial: bool, partial: bool,
reply: IpcSender<Result<Option<String>, ErrorStatus>>, reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
.send( Ok(document) => reply
documents .send(first_matching_link(
.find_document(pipeline) document.upcast::<Node>(),
.ok_or(ErrorStatus::UnknownError) selector.clone(),
.and_then(|document| { partial,
first_matching_link(document.upcast::<Node>(), selector.clone(), partial) can_gc,
}), ))
) .unwrap(),
.unwrap(); Err(error) => reply.send(Err(error)).unwrap(),
}
} }
pub(crate) fn handle_find_element_tag_name( pub(crate) fn handle_find_element_tag_name(
@ -631,20 +662,16 @@ pub(crate) fn handle_find_element_tag_name(
reply: IpcSender<Result<Option<String>, ErrorStatus>>, reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc, can_gc: CanGc,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
.send( Ok(document) => reply
documents .send(Ok(document
.find_document(pipeline)
.ok_or(ErrorStatus::UnknownError)
.map(|document| {
document
.GetElementsByTagName(DOMString::from(selector), can_gc) .GetElementsByTagName(DOMString::from(selector), can_gc)
.elements_iter() .elements_iter()
.next() .next()
}) .map(|node| node.upcast::<Node>().unique_id(pipeline))))
.map(|node| node.map(|x| x.upcast::<Node>().unique_id(pipeline))), .unwrap(),
) Err(error) => reply.send(Err(error)).unwrap(),
.unwrap(); }
} }
pub(crate) fn handle_find_elements_css( pub(crate) fn handle_find_elements_css(
@ -653,16 +680,12 @@ pub(crate) fn handle_find_elements_css(
selector: String, selector: String,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>, reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send( .send(
documents
.find_document(pipeline)
.ok_or(ErrorStatus::UnknownError)
.and_then(|document| {
document document
.QuerySelectorAll(DOMString::from(selector)) .QuerySelectorAll(DOMString::from(selector))
.map_err(|_| ErrorStatus::InvalidSelector) .map_err(|_| ErrorStatus::InvalidSelector)
})
.map(|nodes| { .map(|nodes| {
nodes nodes
.iter() .iter()
@ -670,7 +693,9 @@ pub(crate) fn handle_find_elements_css(
.collect() .collect()
}), }),
) )
.unwrap(); .unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
} }
pub(crate) fn handle_find_elements_link_text( pub(crate) fn handle_find_elements_link_text(
@ -679,17 +704,19 @@ pub(crate) fn handle_find_elements_link_text(
selector: String, selector: String,
partial: bool, partial: bool,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>, reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
.send( Ok(document) => reply
documents .send(all_matching_links(
.find_document(pipeline) document.upcast::<Node>(),
.ok_or(ErrorStatus::UnknownError) selector.clone(),
.and_then(|document| { partial,
all_matching_links(document.upcast::<Node>(), selector.clone(), partial) can_gc,
}), ))
) .unwrap(),
.unwrap(); Err(error) => reply.send(Err(error)).unwrap(),
}
} }
pub(crate) fn handle_find_elements_tag_name( pub(crate) fn handle_find_elements_tag_name(
@ -699,20 +726,16 @@ pub(crate) fn handle_find_elements_tag_name(
reply: IpcSender<Result<Vec<String>, ErrorStatus>>, reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc, can_gc: CanGc,
) { ) {
reply match retrieve_document_and_check_root_existence(documents, pipeline) {
.send( Ok(document) => reply
documents .send(Ok(document
.find_document(pipeline) .GetElementsByTagName(DOMString::from(selector), can_gc)
.ok_or(ErrorStatus::UnknownError)
.map(|document| document.GetElementsByTagName(DOMString::from(selector), can_gc))
.map(|nodes| {
nodes
.elements_iter() .elements_iter()
.map(|x| x.upcast::<Node>().unique_id(pipeline)) .map(|x| x.upcast::<Node>().unique_id(pipeline))
.collect::<Vec<String>>() .collect::<Vec<String>>()))
}), .unwrap(),
) Err(error) => reply.send(Err(error)).unwrap(),
.unwrap(); }
} }
pub(crate) fn handle_find_element_element_css( pub(crate) fn handle_find_element_element_css(
@ -740,11 +763,12 @@ pub(crate) fn handle_find_element_element_link_text(
selector: String, selector: String,
partial: bool, partial: bool,
reply: IpcSender<Result<Option<String>, ErrorStatus>>, reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) { ) {
reply reply
.send( .send(
find_node_by_unique_id(documents, pipeline, element_id) find_node_by_unique_id(documents, pipeline, element_id)
.and_then(|node| first_matching_link(&node, selector.clone(), partial)), .and_then(|node| first_matching_link(&node, selector.clone(), partial, can_gc)),
) )
.unwrap(); .unwrap();
} }
@ -803,11 +827,12 @@ pub(crate) fn handle_find_element_elements_link_text(
selector: String, selector: String,
partial: bool, partial: bool,
reply: IpcSender<Result<Vec<String>, ErrorStatus>>, reply: IpcSender<Result<Vec<String>, ErrorStatus>>,
can_gc: CanGc,
) { ) {
reply reply
.send( .send(
find_node_by_unique_id(documents, pipeline, element_id) find_node_by_unique_id(documents, pipeline, element_id)
.and_then(|node| all_matching_links(&node, selector.clone(), partial)), .and_then(|node| all_matching_links(&node, selector.clone(), partial, can_gc)),
) )
.unwrap(); .unwrap();
} }

View file

@ -929,10 +929,15 @@ impl Handler {
))) )))
} }
/// <https://w3c.github.io/webdriver/#find-element>
fn handle_find_element( fn handle_find_element(
&self, &self,
parameters: &LocatorParameters, parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> { ) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument.
if parameters.value.is_empty() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();
match parameters.using { match parameters.using {
@ -961,13 +966,16 @@ impl Handler {
}, },
} }
// Step 10. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result.
match wait_for_script_response(receiver)? { match wait_for_script_response(receiver)? {
Ok(value) => { Ok(value) => match value {
let value_resp = serde_json::to_value( Some(value) => {
value.map(|x| serde_json::to_value(WebElement(x)).unwrap()), let value_resp = serde_json::to_value(WebElement(value)).unwrap();
)?;
Ok(WebDriverResponse::Generic(ValueResponse(value_resp))) Ok(WebDriverResponse::Generic(ValueResponse(value_resp)))
}, },
None => Err(WebDriverError::new(ErrorStatus::NoSuchElement, "")),
},
Err(error) => Err(WebDriverError::new(error, "")), Err(error) => Err(WebDriverError::new(error, "")),
} }
} }
@ -1121,13 +1129,16 @@ impl Handler {
} }
} }
// https://w3c.github.io/webdriver/#find-elements /// <https://w3c.github.io/webdriver/#find-elements>
fn handle_find_elements( fn handle_find_elements(
&self, &self,
parameters: &LocatorParameters, parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> { ) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument.
if parameters.value.is_empty() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
let (sender, receiver) = ipc::channel().unwrap(); let (sender, receiver) = ipc::channel().unwrap();
match parameters.using { match parameters.using {
LocatorStrategy::CSSSelector => { LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindElementsCSS(parameters.value.clone(), sender); let cmd = WebDriverScriptCommand::FindElementsCSS(parameters.value.clone(), sender);
@ -1156,10 +1167,7 @@ impl Handler {
match wait_for_script_response(receiver)? { match wait_for_script_response(receiver)? {
Ok(value) => { Ok(value) => {
let resp_value: Vec<Value> = value let resp_value: Vec<WebElement> = value.into_iter().map(WebElement).collect();
.into_iter()
.map(|x| serde_json::to_value(WebElement(x)).unwrap())
.collect();
Ok(WebDriverResponse::Generic(ValueResponse( Ok(WebDriverResponse::Generic(ValueResponse(
serde_json::to_value(resp_value)?, serde_json::to_value(resp_value)?,
))) )))

View file

@ -2,15 +2,6 @@
[test_no_browsing_context] [test_no_browsing_context]
expected: FAIL expected: FAIL
[test_no_such_element_with_unknown_selector[not-existent\]]
expected: FAIL
[test_no_such_element_with_unknown_selector[existent-other-frame\]]
expected: FAIL
[test_no_such_element_with_unknown_selector[existent-inside-shadow-root\]]
expected: FAIL
[test_find_element[xpath-//a\]] [test_find_element[xpath-//a\]]
expected: FAIL expected: FAIL

View file

@ -5,18 +5,6 @@
[test_find_elements[xpath-//a\]] [test_find_elements[xpath-//a\]]
expected: FAIL expected: FAIL
[test_find_elements_link_text[<a href=#>link<br>text</a>-link\\ntext\]]
expected: FAIL
[test_find_elements_link_text[<a href=# style='text-transform: uppercase'>link text</a>-LINK TEXT\]]
expected: FAIL
[test_find_elements_partial_link_text[<a href=#>partial link<br>text</a>-k\\nt\]]
expected: FAIL
[test_find_elements_partial_link_text[<a href=# style='text-transform: uppercase'>partial link text</a>-LINK\]]
expected: FAIL
[test_xhtml_namespace[xpath-//*[name()='a'\]\]] [test_xhtml_namespace[xpath-//*[name()='a'\]\]]
expected: FAIL expected: FAIL