webdriver: Support "scroll into view" for commands (#38508)

Implement scroll into view steps for all WebDriver command that requires
it (element click, element send keys, element clear, and take element
screenshot).

Testing: `element_send_keys/scroll_into_view.py`,
`element_click/scroll_into_view.py`, `element_clear/clear.py`

---------

Signed-off-by: PotatoCP <Kenzie.Raditya.Tirtarahardja@huawei.com>
This commit is contained in:
Kenzie Raditya Tirtarahardja 2025-09-12 14:07:58 +08:00 committed by GitHub
parent 9e9bd80bba
commit 097a69169a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 73 additions and 25 deletions

View file

@ -2413,8 +2413,8 @@ impl ScriptThread {
WebDriverScriptCommand::GetElementRect(node_id, reply) => {
webdriver_handlers::handle_get_rect(&documents, pipeline_id, node_id, reply, can_gc)
},
WebDriverScriptCommand::GetBoundingClientRect(node_id, reply) => {
webdriver_handlers::handle_get_bounding_client_rect(
WebDriverScriptCommand::ScrollAndGetBoundingClientRect(node_id, reply) => {
webdriver_handlers::handle_scroll_and_get_bounding_client_rect(
&documents,
pipeline_id,
node_id,

View file

@ -39,7 +39,9 @@ use crate::dom::attr::is_boolean_attribute;
use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
use crate::dom::bindings::codegen::Bindings::DOMRectBinding::DOMRectMethods;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods;
use crate::dom::bindings::codegen::Bindings::ElementBinding::{
ElementMethods, ScrollIntoViewOptions, ScrollLogicalPosition,
};
use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods;
@ -52,6 +54,7 @@ use crate::dom::bindings::codegen::Bindings::XMLSerializerBinding::XMLSerializer
use crate::dom::bindings::codegen::Bindings::XPathResultBinding::{
XPathResultConstants, XPathResultMethods,
};
use crate::dom::bindings::codegen::UnionTypes::BooleanOrScrollIntoViewOptions;
use crate::dom::bindings::conversions::{
ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior,
get_property, get_property_jsval, jsid_to_string, root_from_object,
@ -1264,7 +1267,9 @@ pub(crate) fn handle_will_send_keys(
// Step 7. If file is false or the session's strict file interactability
if !is_file_input || strict_file_interactability {
// TODO(24059): Step 7.1. Scroll Into View
// Step 7.1. Scroll into view the element
scroll_into_view(&element, documents, &pipeline, can_gc);
// TODO: Step 7.2 - 7.5
// Wait until element become keyboard-interactable
@ -1280,7 +1285,12 @@ pub(crate) fn handle_will_send_keys(
if !element.is_active_element() {
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
html_element.Focus(
&FocusOptions {
preventScroll: true,
},
can_gc,
);
} else {
element_has_focus = element.focus_state();
}
@ -1599,7 +1609,7 @@ pub(crate) fn handle_get_rect(
.unwrap();
}
pub(crate) fn handle_get_bounding_client_rect(
pub(crate) fn handle_scroll_and_get_bounding_client_rect(
documents: &DocumentCollection,
pipeline: PipelineId,
element_id: String,
@ -1609,6 +1619,8 @@ pub(crate) fn handle_get_bounding_client_rect(
reply
.send(
get_known_element(documents, pipeline, element_id).map(|element| {
scroll_into_view(&element, documents, &pipeline, can_gc);
let rect = element.GetBoundingClientRect(can_gc);
Rect::new(
Point2D::new(rect.X() as f32, rect.Y() as f32),
@ -1818,7 +1830,12 @@ fn clear_a_resettable_element(element: &Element, can_gc: CanGc) -> Result<(), Er
// Step 3. Invoke the focusing steps for the element.
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
html_element.Focus(
&FocusOptions {
preventScroll: true,
},
can_gc,
);
// Step 4. Run clear algorithm for element.
if let Some(input_element) = element.downcast::<HTMLInputElement>() {
@ -1857,7 +1874,9 @@ pub(crate) fn handle_element_clear(
return Err(ErrorStatus::InvalidElementState);
}
// TODO: Step 5. Scroll Into View
// Step 5. Scroll Into View
scroll_into_view(&element, documents, &pipeline, can_gc);
// TODO: Step 6 - 10
// Wait until element become interactable and check.
@ -1926,7 +1945,7 @@ pub(crate) fn handle_element_click(
};
// Step 5
// TODO: scroll into view is not implemented in Servo
scroll_into_view(&container, documents, &pipeline, can_gc);
// Step 6. If element's container is still not in view
// return error with error code element not interactable.
@ -1969,7 +1988,12 @@ pub(crate) fn handle_element_click(
Some(html_element) => {
// TODO: "Focusing steps" has a different meaning from the focus() method.
// The actual focusing steps should be implemented
html_element.Focus(&FocusOptions::default(), can_gc);
html_element.Focus(
&FocusOptions {
preventScroll: true,
},
can_gc,
);
},
None => return Err(ErrorStatus::UnknownError),
}
@ -2124,3 +2148,35 @@ pub(crate) fn handle_remove_load_status_sender(
window.set_webdriver_load_status_sender(None);
}
}
/// <https://w3c.github.io/webdriver/#dfn-scrolls-into-view>
fn scroll_into_view(
element: &Element,
documents: &DocumentCollection,
pipeline: &PipelineId,
can_gc: CanGc,
) {
// Check if element is already in view
let paint_tree = get_element_pointer_interactable_paint_tree(
element,
&documents
.find_document(*pipeline)
.expect("Document existence guaranteed by `get_known_element`"),
can_gc,
);
if is_element_in_view(element, &paint_tree) {
return;
}
// Step 1. Let options be the following ScrollIntoViewOptions:
// - Logical scroll position "block": end
// - Logical scroll position "inline": nearest
let options = BooleanOrScrollIntoViewOptions::ScrollIntoViewOptions(ScrollIntoViewOptions {
parent: Default::default(),
block: ScrollLogicalPosition::End,
inline: ScrollLogicalPosition::Nearest,
container: Default::default(),
});
// Step 2. Run scrollIntoView
element.ScrollIntoView(options);
}

View file

@ -235,7 +235,7 @@ pub enum WebDriverScriptCommand {
GetElementTagName(String, IpcSender<Result<String, ErrorStatus>>),
GetElementText(String, IpcSender<Result<String, ErrorStatus>>),
GetElementInViewCenterPoint(String, IpcSender<Result<Option<(i64, i64)>, ErrorStatus>>),
GetBoundingClientRect(String, IpcSender<Result<UntypedRect<f32>, ErrorStatus>>),
ScrollAndGetBoundingClientRect(String, IpcSender<Result<UntypedRect<f32>, ErrorStatus>>),
GetBrowsingContextId(
WebDriverFrameId,
IpcSender<Result<BrowsingContextId, ErrorStatus>>,

View file

@ -2400,8 +2400,9 @@ impl Handler {
self.handle_any_user_prompts(webview_id)?;
// Step 3 - 4
let cmd = WebDriverScriptCommand::GetBoundingClientRect(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::No)?;
let cmd =
WebDriverScriptCommand::ScrollAndGetBoundingClientRect(element.to_string(), sender);
self.browsing_context_script_command(cmd, VerifyBrowsingContextIsOpen::Yes)?;
match wait_for_ipc_response(receiver)? {
Ok(rect) => {

View file

@ -0,0 +1,3 @@
[interactability.py]
[test_element_not_visible_overflow_hidden]
expected: FAIL

View file

@ -1,3 +0,0 @@
[scroll_into_view.py]
[test_scroll_into_view]
expected: FAIL

View file

@ -1,12 +1,3 @@
[scroll_into_view.py]
[test_contenteditable_element_outside_of_scrollable_viewport]
expected: FAIL
[test_element_already_in_viewport[{block: 'start'}\]]
expected: FAIL
[test_element_already_in_viewport[{block: 'end'}\]]
expected: FAIL
[test_element_already_in_viewport[{block: 'nearest'}\]]
expected: FAIL