webdriver: Improve "element click" by using container + Upgrade outdated test (#38436)

- According to
[spec](https://w3c.github.io/webdriver/#ref-for-dfn-in-view-3), we
should use container instead of element itself to determine "in-view".
- Updated `test_element_intercepted_no_pointer_events` in
`element_click/interactability.py` to expect "element not interactable".
This was outdated with spec as original test was written 7 years ago
https://github.com/web-platform-tests/wpt/pull/11453.


Testing: new passing cases for `<option>`, `<select>`.

---------

Signed-off-by: Euclid Ye <euclid.ye@huawei.com>
This commit is contained in:
Euclid Ye 2025-08-04 16:12:50 +08:00 committed by GitHub
parent 79a45c7da3
commit c59ee57b5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 32 additions and 46 deletions

View file

@ -1786,7 +1786,7 @@ pub(crate) fn handle_element_clear(
.unwrap(); .unwrap();
} }
fn get_option_parent(node: &Node) -> Option<DomRoot<Node>> { fn get_option_parent(node: &Node) -> Option<DomRoot<Element>> {
// Get parent for `<option>` or `<optiongrp>` based on container spec: // Get parent for `<option>` or `<optiongrp>` based on container spec:
// > 1. Let datalist parent be the first datalist element reached by traversing the tree // > 1. Let datalist parent be the first datalist element reached by traversing the tree
// > in reverse order from element, or undefined if the root of the tree is reached. // > in reverse order from element, or undefined if the root of the tree is reached.
@ -1801,18 +1801,19 @@ fn get_option_parent(node: &Node) -> Option<DomRoot<Node>> {
node.preceding_nodes(&root_node) node.preceding_nodes(&root_node)
.find(|preceding| preceding.is::<HTMLSelectElement>()) .find(|preceding| preceding.is::<HTMLSelectElement>())
}) })
.map(|result_node| DomRoot::downcast::<Element>(result_node).unwrap())
} }
// https://w3c.github.io/webdriver/#dfn-container /// <https://w3c.github.io/webdriver/#dfn-container>
fn get_container(node: &Node) -> Option<DomRoot<Node>> { fn get_container(element: &Element) -> Option<DomRoot<Element>> {
if node.is::<HTMLOptionElement>() { if element.is::<HTMLOptionElement>() {
return get_option_parent(node); return get_option_parent(element.upcast::<Node>());
} }
if node.is::<HTMLOptGroupElement>() { if element.is::<HTMLOptGroupElement>() {
let option_parent = get_option_parent(node); return get_option_parent(element.upcast::<Node>())
return option_parent.or_else(|| Some(DomRoot::from_ref(node))); .or_else(|| Some(DomRoot::from_ref(element)));
} }
Some(DomRoot::from_ref(node)) Some(DomRoot::from_ref(element))
} }
// https://w3c.github.io/webdriver/#element-click // https://w3c.github.io/webdriver/#element-click
@ -1834,7 +1835,7 @@ pub(crate) fn handle_element_click(
} }
} }
let Some(container) = get_container(element.upcast::<Node>()) else { let Some(container) = get_container(&element) else {
return Err(ErrorStatus::UnknownError); return Err(ErrorStatus::UnknownError);
}; };
@ -1843,17 +1844,22 @@ pub(crate) fn handle_element_click(
// Step 6. If element's container is still not in view // Step 6. If element's container is still not in view
// return error with error code element not interactable. // return error with error code element not interactable.
let document = documents let paint_tree = get_element_pointer_interactable_paint_tree(
.find_document(pipeline) &container,
.expect("Document existence guaranteed by `get_known_element`"); &documents
if !is_element_in_view(&element, &document, can_gc) { .find_document(pipeline)
.expect("Document existence guaranteed by `get_known_element`"),
can_gc,
);
if !is_element_in_view(&container, &paint_tree, can_gc) {
return Err(ErrorStatus::ElementNotInteractable); return Err(ErrorStatus::ElementNotInteractable);
} }
// Step 7 // Step 7
// TODO: return error if obscured // TODO: return error if obscured
// Step 8 // Step 8 for <option> element.
match element.downcast::<HTMLOptionElement>() { match element.downcast::<HTMLOptionElement>() {
Some(option_element) => { Some(option_element) => {
// Steps 8.2 - 8.4 // Steps 8.2 - 8.4
@ -1906,20 +1912,21 @@ pub(crate) fn handle_element_click(
} }
/// <https://w3c.github.io/webdriver/#dfn-in-view> /// <https://w3c.github.io/webdriver/#dfn-in-view>
fn is_element_in_view(element: &Element, document: &Document, can_gc: CanGc) -> bool { fn is_element_in_view(element: &Element, paint_tree: &[DomRoot<Element>], can_gc: CanGc) -> bool {
// An element is in view if it is a member of its own pointer-interactable paint tree,
// given the pretense that its pointer events are not disabled.
if !paint_tree.contains(&DomRoot::from_ref(element)) {
return false;
}
use style::computed_values::pointer_events::T as PointerEvents; use style::computed_values::pointer_events::T as PointerEvents;
// https://w3c.github.io/webdriver/#dfn-pointer-events-are-not-disabled // https://w3c.github.io/webdriver/#dfn-pointer-events-are-not-disabled
// An element is said to have pointer events disabled // An element is said to have pointer events disabled
// if the resolved value of its "pointer-events" style property is "none". // if the resolved value of its "pointer-events" style property is "none".
let pointer_events_enabled = element let pointer_events_not_disabled = element
.style(can_gc) .style(can_gc)
.is_none_or(|style| style.get_inherited_ui().pointer_events != PointerEvents::None); .is_none_or(|style| style.get_inherited_ui().pointer_events != PointerEvents::None);
// An element is in view if it is a member of its own pointer-interactable paint tree, pointer_events_not_disabled
// given the pretense that its pointer events are not disabled.
pointer_events_enabled &&
get_element_pointer_interactable_paint_tree(element, document, can_gc)
.contains(&DomRoot::from_ref(element))
} }
/// <https://w3c.github.io/webdriver/#dfn-pointer-interactable-paint-tree> /// <https://w3c.github.io/webdriver/#dfn-pointer-interactable-paint-tree>

View file

@ -943609,7 +943609,7 @@
] ]
], ],
"interactability.py": [ "interactability.py": [
"65f8a9015eeaa6a450e603d1ca6e1914516eb506", "aefaa1fd8e8aaddc6eaf336b5f1d8d820665b907",
[ [
null, null,
{} {}

View file

@ -1,6 +1,3 @@
[interactability.py] [interactability.py]
[test_element_intercepted] [test_element_intercepted]
expected: FAIL expected: FAIL
[test_element_intercepted_no_pointer_events]
expected: FAIL

View file

@ -13,21 +13,3 @@
[test_out_of_view_dropdown] [test_out_of_view_dropdown]
expected: FAIL expected: FAIL
[test_click_multiple_option]
expected: FAIL
[test_click_preselected_multiple_option]
expected: FAIL
[test_click_multiple_does_not_deselect_others]
expected: FAIL
[test_click_selected_multiple_option]
expected: FAIL
[test_out_of_view_multiple]
expected: FAIL
[test_option_disabled]
expected: FAIL

View file

@ -115,11 +115,11 @@ def test_element_intercepted(session, inline):
assert_error(response, "element click intercepted") assert_error(response, "element click intercepted")
def test_element_intercepted_no_pointer_events(session, inline): def test_element_not_interactable_pointer_events_none(session, inline):
session.url = inline("""<input type=button value=Roger style="pointer-events: none">""") session.url = inline("""<input type=button value=Roger style="pointer-events: none">""")
element = session.find.css("input", all=False) element = session.find.css("input", all=False)
response = element_click(session, element) response = element_click(session, element)
assert_error(response, "element click intercepted") assert_error(response, "element not interactable")
def test_element_not_visible_overflow_hidden(session, inline): def test_element_not_visible_overflow_hidden(session, inline):