[wdspec] Add WebDriver classic tests for actions on elements in ShadowRoot

Differential Revision: https://phabricator.services.mozilla.com/D182474

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1806897
gecko-commit: fc23046c2b2028181a35cf83a5e8e1c75a2ceaca
gecko-reviewers: webdriver-reviewers, whimboo
diff --git a/webdriver/tests/classic/perform_actions/__init__.py b/webdriver/tests/classic/perform_actions/__init__.py
index 6fec8a8..af87e19 100644
--- a/webdriver/tests/classic/perform_actions/__init__.py
+++ b/webdriver/tests/classic/perform_actions/__init__.py
@@ -1,3 +1,31 @@
+def assert_pointer_events(session, expected_events, target, pointer_type):
+    events = session.execute_script("return window.recordedEvents;")
+    assert len(events) == len(expected_events)
+    event_types = [e["type"] for e in events]
+    assert expected_events == event_types
+
+    for e in events:
+        assert e["target"] == target
+        assert e["pointerType"] == pointer_type
+
+
+def record_pointer_events(session, element):
+    # Record basic mouse / pointer events on a given element.
+    session.execute_script(
+        """
+        window.recordedEvents = [];
+        function onPointerEvent(event) {
+            window.recordedEvents.push({
+                "pointerType": event.pointerType,
+                "target": event.target.id,
+                "type": event.type,
+            });
+        }
+        arguments[0].addEventListener("pointerdown", onPointerEvent);
+        arguments[0].addEventListener("pointerup", onPointerEvent);
+    """,
+        args=(element,),
+    )
 def perform_actions(session, actions):
     return session.transport.send(
         "POST",
diff --git a/webdriver/tests/classic/perform_actions/key.py b/webdriver/tests/classic/perform_actions/key.py
index 6c34452..7809fcd 100644
--- a/webdriver/tests/classic/perform_actions/key.py
+++ b/webdriver/tests/classic/perform_actions/key.py
@@ -36,3 +36,27 @@
         .perform()
 
     assert get_keys(key_reporter) == "ef"
+
+
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_element_in_shadow_tree(session, get_test_page, key_chain, mode, nested):
+    session.url = get_test_page(
+        shadow_doc="<div><input type=text></div>",
+        shadow_root_mode=mode,
+        nested_shadow_dom=nested,
+    )
+
+    shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+    if nested:
+        shadow_root = shadow_root.find_element(
+            "css selector", "inner-custom-element"
+        ).shadow_root
+
+    input_el = shadow_root.find_element("css selector", "input")
+    input_el.click()
+
+    key_chain.key_down("a").key_up("a").perform()
+
+    assert input_el.property("value") == "a"
diff --git a/webdriver/tests/classic/perform_actions/pointer_mouse.py b/webdriver/tests/classic/perform_actions/pointer_mouse.py
index 31d0849..152447a 100644
--- a/webdriver/tests/classic/perform_actions/pointer_mouse.py
+++ b/webdriver/tests/classic/perform_actions/pointer_mouse.py
@@ -13,6 +13,8 @@
 from tests.support.helpers import filter_dict
 from tests.support.sync import Poll
 
+from . import assert_pointer_events, record_pointer_events
+
 
 def test_null_response_value(session, mouse_chain):
     value = mouse_chain.click().perform()
@@ -101,6 +103,36 @@
             assert e["target"] == "outer"
 
 
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_click_element_in_shadow_tree(
+    session, get_test_page, mouse_chain, mode, nested
+):
+    session.url = get_test_page(
+        shadow_doc="""
+        <div id="pointer-target"
+             style="width: 10px; height: 10px; background-color:blue;">
+        </div>""",
+        shadow_root_mode=mode,
+        nested_shadow_dom=nested,
+    )
+
+    shadow_root = session.find.css("custom-element", all=False).shadow_root
+    if nested:
+        shadow_root = shadow_root.find_element("css selector", "inner-custom-element").shadow_root
+
+    target = shadow_root.find_element("css selector", "#pointer-target")
+    record_pointer_events(session, target)
+
+    mouse_chain.click(element=target).perform()
+    assert_pointer_events(
+        session,
+        expected_events=["pointerdown", "pointerup"],
+        target="pointer-target",
+        pointer_type="mouse",
+    )
+
+
 def test_click_navigation(session, url, inline):
     destination = url("/webdriver/tests/support/html/test_actions.html")
     start = inline("<a href=\"{}\" id=\"link\">destination</a>".format(destination))
diff --git a/webdriver/tests/classic/perform_actions/pointer_pen.py b/webdriver/tests/classic/perform_actions/pointer_pen.py
index e9cd103..fcd1aba 100644
--- a/webdriver/tests/classic/perform_actions/pointer_pen.py
+++ b/webdriver/tests/classic/perform_actions/pointer_pen.py
@@ -10,6 +10,8 @@
 )
 from tests.classic.perform_actions.support.refine import get_events
 
+from . import assert_pointer_events, record_pointer_events
+
 
 def test_null_response_value(session, pen_chain):
     value = pen_chain.click().perform()
@@ -34,6 +36,44 @@
         pen_chain.click(element=element).perform()
 
 
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_pen_pointer_in_shadow_tree(
+    session, get_test_page, pen_chain, mode, nested
+):
+    session.url = get_test_page(
+        shadow_doc="""
+        <div id="pointer-target"
+             style="width: 10px; height: 10px; background-color:blue;">
+        </div>""",
+        shadow_root_mode=mode,
+        nested_shadow_dom=nested,
+    )
+
+    shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+    if nested:
+        shadow_root = shadow_root.find_element(
+            "css selector", "inner-custom-element"
+        ).shadow_root
+
+    target = shadow_root.find_element("css selector", "#pointer-target")
+
+    record_pointer_events(session, target)
+
+    pen_chain.pointer_move(0, 0, origin=target) \
+        .pointer_down() \
+        .pointer_up() \
+        .perform()
+
+    assert_pointer_events(
+        session,
+        expected_events=["pointerdown", "pointerup"],
+        target="pointer-target",
+        pointer_type="pen",
+    )
+
+
 def test_pen_pointer_properties(session, test_actions_pointer_page, pen_chain):
     pointerArea = session.find.css("#pointerArea", all=False)
     center = get_inview_center(pointerArea.rect, get_viewport_rect(session))
diff --git a/webdriver/tests/classic/perform_actions/pointer_touch.py b/webdriver/tests/classic/perform_actions/pointer_touch.py
index 7f940f6..6c8e2f3 100644
--- a/webdriver/tests/classic/perform_actions/pointer_touch.py
+++ b/webdriver/tests/classic/perform_actions/pointer_touch.py
@@ -9,6 +9,7 @@
 )
 from tests.classic.perform_actions.support.refine import get_events
 
+from . import assert_pointer_events, record_pointer_events
 
 def test_null_response_value(session, touch_chain):
     value = touch_chain.click().perform()
@@ -33,6 +34,41 @@
         touch_chain.click(element=element).perform()
 
 
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_touch_pointer_in_shadow_tree(
+    session, get_test_page, touch_chain, mode, nested
+):
+    session.url = get_test_page(
+        shadow_doc="""
+        <div id="pointer-target"
+             style="width: 10px; height: 10px; background-color:blue;">
+        </div>""",
+        shadow_root_mode=mode,
+        nested_shadow_dom=nested,
+    )
+
+    shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+    if nested:
+        shadow_root = shadow_root.find_element(
+            "css selector", "inner-custom-element"
+        ).shadow_root
+
+    target = shadow_root.find_element("css selector", "#pointer-target")
+
+    record_pointer_events(session, target)
+
+    touch_chain.pointer_move(0, 0, origin=target).pointer_down().pointer_up().perform()
+
+    assert_pointer_events(
+        session,
+        expected_events=["pointerdown", "pointerup"],
+        target="pointer-target",
+        pointer_type="touch",
+    )
+
+
 def test_touch_pointer_properties(session, test_actions_pointer_page, touch_chain):
     pointerArea = session.find.css("#pointerArea", all=False)
     center = get_inview_center(pointerArea.rect, get_viewport_rect(session))
diff --git a/webdriver/tests/classic/perform_actions/wheel.py b/webdriver/tests/classic/perform_actions/wheel.py
index 7ee0ae7..c83c90d 100644
--- a/webdriver/tests/classic/perform_actions/wheel.py
+++ b/webdriver/tests/classic/perform_actions/wheel.py
@@ -64,6 +64,56 @@
     assert events[0]["target"] == "iframeContent"
 
 
+@pytest.mark.parametrize("mode", ["open", "closed"])
+@pytest.mark.parametrize("nested", [False, True], ids=["outer", "inner"])
+def test_wheel_scroll_shadow_tree(session, get_test_page, wheel_chain, mode, nested):
+    session.url = get_test_page(
+        shadow_doc="""
+        <div id="scrollableShadowTree"
+             style="width: 100px; height: 100px; overflow: auto;">
+            <div
+                id="scrollableShadowTreeContent"
+                style="width: 600px; height: 1000px; background-color:blue"></div>
+        </div>""",
+        shadow_root_mode=mode,
+        nested_shadow_dom=nested,
+    )
+
+    shadow_root = session.find.css("custom-element", all=False).shadow_root
+
+    if nested:
+        shadow_root = shadow_root.find_element(
+            "css selector", "inner-custom-element"
+        ).shadow_root
+
+    scrollable = shadow_root.find_element("css selector", "#scrollableShadowTree")
+
+    # Add a simplified event recorder to track events in the test ShadowRoot.
+    session.execute_script(
+        """
+        window.wheelEvents = [];
+        arguments[0].addEventListener("wheel",
+            function(event) {
+                window.wheelEvents.push({
+                    "deltaX": event.deltaX,
+                    "deltaY": event.deltaY,
+                    "target": event.target.id
+                });
+            }
+        );
+    """,
+        args=(scrollable,),
+    )
+
+    wheel_chain.scroll(0, 0, 5, 10, origin=scrollable).perform()
+
+    events = session.execute_script("return window.wheelEvents;") or []
+    assert len(events) == 1
+    assert events[0]["deltaX"] >= 5
+    assert events[0]["deltaY"] >= 10
+    assert events[0]["target"] == "scrollableShadowTreeContent"
+
+
 @pytest.mark.parametrize("missing", ["x", "y", "deltaX", "deltaY"])
 def test_wheel_missing_prop(session, test_actions_scroll_page, wheel_chain, missing):
     session.execute_script("document.scrollingElement.scrollTop = 0")