[WebDriver] Reuse "Find Elements" handler in "Find element" (#37737)

All "Find Element ..." in
[spec](https://w3c.github.io/webdriver/#find-element-from-element) has
exactly same step has "Find Elements ...", except they extract the first
element if there is any. We now reuse the handler instead of running
numerous repetitive steps, same as what we did for ["Find Element from
Shadow
Root"](https://w3c.github.io/webdriver/#find-element-from-shadow-root)
in #37578.

This reduces binary size by 98KB for Release profile in Windows and
improves maintainability.

Testing: `./mach test-wpt -r
.\tests\wpt\tests\webdriver\tests\classic\find_element* --product
servodriver`

---------

Signed-off-by: Euclid Ye <yezhizhenjiakang@gmail.com>
This commit is contained in:
Euclid Ye 2025-06-27 16:45:58 +08:00 committed by GitHub
parent 8e2ef5c248
commit 0329f33520
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 23 additions and 325 deletions

View file

@ -2247,33 +2247,6 @@ impl ScriptThread {
WebDriverScriptCommand::DeleteCookie(name, reply) => { WebDriverScriptCommand::DeleteCookie(name, reply) => {
webdriver_handlers::handle_delete_cookie(&documents, pipeline_id, name, reply) webdriver_handlers::handle_delete_cookie(&documents, pipeline_id, name, reply)
}, },
WebDriverScriptCommand::FindElementCSSSelector(selector, reply) => {
webdriver_handlers::handle_find_element_css_selector(
&documents,
pipeline_id,
selector,
reply,
)
},
WebDriverScriptCommand::FindElementLinkText(selector, partial, reply) => {
webdriver_handlers::handle_find_element_link_text(
&documents,
pipeline_id,
selector,
partial,
reply,
can_gc,
)
},
WebDriverScriptCommand::FindElementTagName(selector, reply) => {
webdriver_handlers::handle_find_element_tag_name(
&documents,
pipeline_id,
selector,
reply,
can_gc,
)
},
WebDriverScriptCommand::FindElementsCSSSelector(selector, reply) => { WebDriverScriptCommand::FindElementsCSSSelector(selector, reply) => {
webdriver_handlers::handle_find_elements_css_selector( webdriver_handlers::handle_find_elements_css_selector(
&documents, &documents,
@ -2301,39 +2274,6 @@ impl ScriptThread {
can_gc, can_gc,
) )
}, },
WebDriverScriptCommand::FindElementElementCSSSelector(selector, element_id, reply) => {
webdriver_handlers::handle_find_element_element_css_selector(
&documents,
pipeline_id,
element_id,
selector,
reply,
)
},
WebDriverScriptCommand::FindElementElementLinkText(
selector,
element_id,
partial,
reply,
) => webdriver_handlers::handle_find_element_element_link_text(
&documents,
pipeline_id,
element_id,
selector,
partial,
reply,
can_gc,
),
WebDriverScriptCommand::FindElementElementTagName(selector, element_id, reply) => {
webdriver_handlers::handle_find_element_element_tag_name(
&documents,
pipeline_id,
element_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,

View file

@ -217,25 +217,6 @@ fn all_matching_links(
.map(|nodes| matching_links(&nodes, link_text, partial, can_gc).collect()) .map(|nodes| matching_links(&nodes, link_text, partial, can_gc).collect())
} }
fn first_matching_link(
root_node: &Node,
link_text: String,
partial: bool,
can_gc: CanGc,
) -> 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
.query_selector_all(DOMString::from("a"))
.map_err(|_| ErrorStatus::InvalidSelector)
.map(|nodes| {
matching_links(&nodes, link_text, partial, can_gc)
.take(1)
.next()
})
}
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn object_has_to_json_property( unsafe fn object_has_to_json_property(
cx: *mut JSContext, cx: *mut JSContext,
@ -714,65 +695,6 @@ fn retrieve_document_and_check_root_existence(
} }
} }
pub(crate) fn handle_find_element_css_selector(
documents: &DocumentCollection,
pipeline: PipelineId,
selector: String,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
) {
match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send(
document
.QuerySelector(DOMString::from(selector))
.map_err(|_| ErrorStatus::InvalidSelector)
.map(|element| element.map(|x| x.upcast::<Node>().unique_id(pipeline))),
)
.unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
}
pub(crate) fn handle_find_element_link_text(
documents: &DocumentCollection,
pipeline: PipelineId,
selector: String,
partial: bool,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) {
match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send(first_matching_link(
document.upcast::<Node>(),
selector.clone(),
partial,
can_gc,
))
.unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
}
pub(crate) fn handle_find_element_tag_name(
documents: &DocumentCollection,
pipeline: PipelineId,
selector: String,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) {
match retrieve_document_and_check_root_existence(documents, pipeline) {
Ok(document) => reply
.send(Ok(document
.GetElementsByTagName(DOMString::from(selector), can_gc)
.elements_iter()
.next()
.map(|element| element.upcast::<Node>().unique_id(pipeline))))
.unwrap(),
Err(error) => reply.send(Err(error)).unwrap(),
}
}
pub(crate) fn handle_find_elements_css_selector( pub(crate) fn handle_find_elements_css_selector(
documents: &DocumentCollection, documents: &DocumentCollection,
pipeline: PipelineId, pipeline: PipelineId,
@ -837,65 +759,6 @@ pub(crate) fn handle_find_elements_tag_name(
} }
} }
pub(crate) fn handle_find_element_element_css_selector(
documents: &DocumentCollection,
pipeline: PipelineId,
element_id: String,
selector: String,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
) {
reply
.send(
get_known_element(documents, pipeline, element_id).and_then(|element| {
element
.upcast::<Node>()
.query_selector(DOMString::from(selector))
.map_err(|_| ErrorStatus::InvalidSelector)
.map(|element| element.map(|x| x.upcast::<Node>().unique_id(pipeline)))
}),
)
.unwrap();
}
pub(crate) fn handle_find_element_element_link_text(
documents: &DocumentCollection,
pipeline: PipelineId,
element_id: String,
selector: String,
partial: bool,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) {
reply
.send(
get_known_element(documents, pipeline, element_id).and_then(|element| {
first_matching_link(element.upcast::<Node>(), selector.clone(), partial, can_gc)
}),
)
.unwrap();
}
pub(crate) fn handle_find_element_element_tag_name(
documents: &DocumentCollection,
pipeline: PipelineId,
element_id: String,
selector: String,
reply: IpcSender<Result<Option<String>, ErrorStatus>>,
can_gc: CanGc,
) {
reply
.send(
get_known_element(documents, pipeline, element_id).map(|element| {
element
.GetElementsByTagName(DOMString::from(selector), can_gc)
.elements_iter()
.next()
.map(|x| x.upcast::<Node>().unique_id(pipeline))
}),
)
.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,

View file

@ -131,28 +131,9 @@ pub enum WebDriverScriptCommand {
DeleteCookie(String, IpcSender<Result<(), ErrorStatus>>), DeleteCookie(String, IpcSender<Result<(), ErrorStatus>>),
ExecuteScript(String, IpcSender<WebDriverJSResult>), ExecuteScript(String, IpcSender<WebDriverJSResult>),
ExecuteAsyncScript(String, IpcSender<WebDriverJSResult>), ExecuteAsyncScript(String, IpcSender<WebDriverJSResult>),
FindElementCSSSelector(String, IpcSender<Result<Option<String>, ErrorStatus>>),
FindElementLinkText(String, bool, IpcSender<Result<Option<String>, ErrorStatus>>),
FindElementTagName(String, IpcSender<Result<Option<String>, ErrorStatus>>),
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>>),
FindElementElementCSSSelector(
String,
String,
IpcSender<Result<Option<String>, ErrorStatus>>,
),
FindElementElementLinkText(
String,
String,
bool,
IpcSender<Result<Option<String>, ErrorStatus>>,
),
FindElementElementTagName(
String,
String,
IpcSender<Result<Option<String>, ErrorStatus>>,
),
FindElementElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>), FindElementElementsCSSSelector(String, String, IpcSender<Result<Vec<String>, ErrorStatus>>),
FindElementElementsLinkText( FindElementElementsLinkText(
String, String,

View file

@ -970,53 +970,11 @@ impl Handler {
&self, &self,
parameters: &LocatorParameters, parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> { ) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument. // Step 1 - 9.
if parameters.value.is_empty() { let res = self.handle_find_elements(parameters)?;
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
let (sender, receiver) = ipc::channel().unwrap();
match parameters.using {
LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindElementCSSSelector(
parameters.value.clone(),
sender,
);
self.browsing_context_script_command::<true>(cmd)?;
},
LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => {
let cmd = WebDriverScriptCommand::FindElementLinkText(
parameters.value.clone(),
parameters.using == LocatorStrategy::PartialLinkText,
sender,
);
self.browsing_context_script_command::<true>(cmd)?;
},
LocatorStrategy::TagName => {
let cmd =
WebDriverScriptCommand::FindElementTagName(parameters.value.clone(), sender);
self.browsing_context_script_command::<true>(cmd)?;
},
_ => {
return Err(WebDriverError::new(
ErrorStatus::UnsupportedOperation,
"Unsupported locator strategy",
));
},
}
// Step 10. If result is empty, return error with error code no such element. // Step 10. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result. // Otherwise, return the first element of result.
match wait_for_script_response(receiver)? { unwrap_first_element_response(res)
Ok(value) => match value {
Some(value) => {
let value_resp = serde_json::to_value(WebElement(value)).unwrap();
Ok(WebDriverResponse::Generic(ValueResponse(value_resp)))
},
None => Err(WebDriverError::new(ErrorStatus::NoSuchElement, "")),
},
Err(error) => Err(WebDriverError::new(error, "")),
}
} }
/// <https://w3c.github.io/webdriver/#close-window> /// <https://w3c.github.io/webdriver/#close-window>
@ -1234,62 +1192,16 @@ impl Handler {
} }
/// <https://w3c.github.io/webdriver/#find-element-from-element> /// <https://w3c.github.io/webdriver/#find-element-from-element>
fn handle_find_element_element( fn handle_find_element_from_element(
&self, &self,
element: &WebElement, element: &WebElement,
parameters: &LocatorParameters, parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> { ) -> WebDriverResult<WebDriverResponse> {
// Step 4. If selector is undefined, return error with error code invalid argument. // Step 1 - 8.
if parameters.value.is_empty() { let res = self.handle_find_elements_from_element(element, parameters)?;
return Err(WebDriverError::new(ErrorStatus::InvalidArgument, ""));
}
let (sender, receiver) = ipc::channel().unwrap();
match parameters.using {
LocatorStrategy::CSSSelector => {
let cmd = WebDriverScriptCommand::FindElementElementCSSSelector(
parameters.value.clone(),
element.to_string(),
sender,
);
self.browsing_context_script_command::<true>(cmd)?;
},
LocatorStrategy::LinkText | LocatorStrategy::PartialLinkText => {
let cmd = WebDriverScriptCommand::FindElementElementLinkText(
parameters.value.clone(),
element.to_string(),
parameters.using == LocatorStrategy::PartialLinkText,
sender,
);
self.browsing_context_script_command::<true>(cmd)?;
},
LocatorStrategy::TagName => {
let cmd = WebDriverScriptCommand::FindElementElementTagName(
parameters.value.clone(),
element.to_string(),
sender,
);
self.browsing_context_script_command::<true>(cmd)?;
},
_ => {
return Err(WebDriverError::new(
ErrorStatus::UnsupportedOperation,
"Unsupported locator strategy",
));
},
}
// Step 9. If result is empty, return error with error code no such element. // Step 9. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result. // Otherwise, return the first element of result.
match wait_for_script_response(receiver)? { unwrap_first_element_response(res)
Ok(value) => match value {
Some(value) => {
let value_resp = serde_json::to_value(WebElement(value))?;
Ok(WebDriverResponse::Generic(ValueResponse(value_resp)))
},
None => Err(WebDriverError::new(ErrorStatus::NoSuchElement, "")),
},
Err(error) => Err(WebDriverError::new(error, "")),
}
} }
/// <https://w3c.github.io/webdriver/#find-elements-from-element> /// <https://w3c.github.io/webdriver/#find-elements-from-element>
@ -1418,22 +1330,11 @@ impl Handler {
shadow_root: &ShadowRoot, shadow_root: &ShadowRoot,
parameters: &LocatorParameters, parameters: &LocatorParameters,
) -> WebDriverResult<WebDriverResponse> { ) -> WebDriverResult<WebDriverResponse> {
// Step 1 - 8.
let res = self.handle_find_elements_from_shadow_root(shadow_root, parameters)?; let res = self.handle_find_elements_from_shadow_root(shadow_root, parameters)?;
// Step 9. If result is empty, return error with error code no such element. // Step 9. If result is empty, return error with error code no such element.
// Otherwise, return the first element of result. // Otherwise, return the first element of result.
if let WebDriverResponse::Generic(ValueResponse(values)) = res { unwrap_first_element_response(res)
let arr = values.as_array().unwrap();
if let Some(first) = arr.first() {
Ok(WebDriverResponse::Generic(ValueResponse(first.clone())))
} else {
Err(WebDriverError::new(ErrorStatus::NoSuchElement, ""))
}
} else {
Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Unexpected response",
))
}
} }
fn handle_get_shadow_root(&self, element: WebElement) -> WebDriverResult<WebDriverResponse> { fn handle_get_shadow_root(&self, element: WebElement) -> WebDriverResult<WebDriverResponse> {
@ -2255,7 +2156,7 @@ impl WebDriverHandler<ServoExtensionRoute> for Handler {
WebDriverCommand::FindElement(ref parameters) => self.handle_find_element(parameters), WebDriverCommand::FindElement(ref parameters) => self.handle_find_element(parameters),
WebDriverCommand::FindElements(ref parameters) => self.handle_find_elements(parameters), WebDriverCommand::FindElements(ref parameters) => self.handle_find_elements(parameters),
WebDriverCommand::FindElementElement(ref element, ref parameters) => { WebDriverCommand::FindElementElement(ref element, ref parameters) => {
self.handle_find_element_element(element, parameters) self.handle_find_element_from_element(element, parameters)
}, },
WebDriverCommand::FindElementElements(ref element, ref parameters) => { WebDriverCommand::FindElementElements(ref element, ref parameters) => {
self.handle_find_elements_from_element(element, parameters) self.handle_find_elements_from_element(element, parameters)
@ -2380,3 +2281,16 @@ where
.recv() .recv()
.map_err(|_| WebDriverError::new(ErrorStatus::NoSuchWindow, "")) .map_err(|_| WebDriverError::new(ErrorStatus::NoSuchWindow, ""))
} }
fn unwrap_first_element_response(res: WebDriverResponse) -> WebDriverResult<WebDriverResponse> {
if let WebDriverResponse::Generic(ValueResponse(values)) = res {
let arr = values.as_array().unwrap();
if let Some(first) = arr.first() {
Ok(WebDriverResponse::Generic(ValueResponse(first.clone())))
} else {
Err(WebDriverError::new(ErrorStatus::NoSuchElement, ""))
}
} else {
unreachable!()
}
}