[wdspec] Add and refactor tests for parsing of action messages for WebDriver BiDi.

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

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1824227
gecko-commit: 84311eec21cca361b9e93d6aa8719f0d9a74c311
gecko-reviewers: webdriver-reviewers, jdescottes
diff --git a/webdriver/tests/bidi/input/perform_actions/invalid.py b/webdriver/tests/bidi/input/perform_actions/invalid.py
index 19b7a57..7513ab9 100644
--- a/webdriver/tests/bidi/input/perform_actions/invalid.py
+++ b/webdriver/tests/bidi/input/perform_actions/invalid.py
@@ -1,117 +1,797 @@
 import pytest
+import pytest_asyncio
 
-from webdriver.bidi.modules.input import Actions, get_element_origin
-from webdriver.bidi.error import (
-    InvalidArgumentException,
-    MoveTargetOutOfBoundsException,
-    NoSuchElementException,
-    NoSuchFrameException,
-    NoSuchNodeException,
-)
-from webdriver.bidi.modules.script import ContextTarget
+from webdriver.bidi.modules.input import Actions
+from webdriver.bidi.error import InvalidArgumentException
 
 
 pytestmark = pytest.mark.asyncio
 
 
+MAX_INT = 9007199254740991
+MIN_INT = -MAX_INT
+
+
+@pytest_asyncio.fixture
+async def perform_actions(bidi_session, top_context):
+    async def perform_actions(actions, context=top_context["context"]):
+        return await bidi_session.input.perform_actions(
+            actions=actions, context=context
+        )
+
+    yield perform_actions
+
+
+def create_key_action(key_action, overrides=None, removals=None):
+    action = {
+        "type": key_action,
+        "value": "",
+    }
+
+    if overrides is not None:
+        action.update(overrides)
+
+    if removals is not None:
+        for removal in removals:
+            del action[removal]
+
+    return action
+
+
+def create_pointer_action(pointer_action, overrides=None, removals=None):
+    action = {
+        "type": pointer_action,
+        "width": 0,
+        "height": 0,
+        "pressure": 0.0,
+        "tangentialPressure": 0.0,
+        "twist": 0,
+        "tiltX": 0,
+        "tiltY": 0,
+    }
+
+    if pointer_action == "pointerMove":
+        action.update({"x": 0, "y": 0})
+    elif pointer_action in ["pointerDown", "pointerUp"]:
+        action.update({"button": 0})
+
+    if overrides is not None:
+        action.update(overrides)
+
+    if removals is not None:
+        for removal in removals:
+            del action[removal]
+
+    return action
+
+
+def create_wheel_action(wheel_action, overrides=None, removals=None):
+    action = {
+        "type": wheel_action,
+        "x": 0,
+        "y": 0,
+        "deltaX": 0,
+        "deltaY": 0,
+        "deltaZ": 0,
+        "deltaMode": 0,
+        "origin": "viewport",
+    }
+
+    if overrides is not None:
+        action.update(overrides)
+
+    if removals is not None:
+        for removal in removals:
+            del action[removal]
+
+    return action
+
+
 @pytest.mark.parametrize("value", [None, True, 42, {}, []])
-async def test_params_context_invalid_type(bidi_session, value):
+async def test_params_context_invalid_type(perform_actions, value):
     actions = Actions()
     actions.add_key()
+
     with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(actions=actions, context=value)
+        await perform_actions(actions, context=value)
 
 
-async def test_params_contexts_value_invalid_value(bidi_session):
-    actions = Actions()
-    actions.add_key()
-    with pytest.raises(NoSuchFrameException):
-        await bidi_session.input.perform_actions(actions=actions, context="foo")
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_actions_invalid_type(perform_actions, value):
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(value)
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_invalid_type(perform_actions, value):
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([value])
+
+
+async def test_params_input_source_action_sequence_type_missing(perform_actions):
+    actions = [
+        {
+            "id": "foo",
+            "actions": [],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+async def test_params_input_source_action_sequence_id_missing(
+    perform_actions, action_type
+):
+    actions = [
+        {
+            "type": action_type,
+            "actions": [],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+async def test_params_input_source_action_sequence_actions_missing(
+    perform_actions, action_type
+):
+    actions = [
+        {
+            "type": action_type,
+            "id": "foo",
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_type_invalid_type(
+    perform_actions, value
+):
+    actions = [
+        {
+            "type": value,
+            "id": "foo",
+            "actions": [],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["", "nones", "keys", "pointers", "wheels"])
+async def test_params_input_source_action_sequence_type_invalid_value(
+    perform_actions, action_type
+):
+    actions = [
+        {
+            "type": action_type,
+            "id": "foo",
+            "actions": [],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_id_invalid_type(
+    perform_actions, action_type, value
+):
+    actions = [
+        {
+            "type": action_type,
+            "id": value,
+            "actions": [],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_actions_invalid_type(
+    perform_actions, action_type, value
+):
+    actions = [
+        {
+            "type": action_type,
+            "id": "foo",
+            "actions": value,
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 42, {}])
+async def test_params_input_source_action_sequence_actions_actions_invalid_type(
+    perform_actions, action_type, value
+):
+    actions = [
+        {
+            "type": action_type,
+            "id": "foo",
+            "actions": [value],
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, "foo", True, 42, []])
+async def test_params_input_source_action_sequence_pointer_parameters_invalid_type(
+    perform_actions, value
+):
+    actions = [{"type": "pointer", "id": "foo", "actions": [], "parameters": value}]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_pointer_parameters_pointer_type_invalid_type(
+    perform_actions, value
+):
+    actions = [
+        {
+            "type": "pointer",
+            "id": "foo",
+            "actions": [],
+            "parameters": {
+                "pointerType": value,
+            },
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("value", ["", "mouses", "pens", "touchs"])
+async def test_params_input_source_action_sequence_pointer_parameters_pointer_type_invalid_value(
+    perform_actions, value
+):
+    actions = [
+        {
+            "type": "pointer",
+            "id": "foo",
+            "actions": [],
+            "parameters": {
+                "pointerType": value,
+            },
+        }
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions(actions)
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_input_source_action_sequence_actions_type_invalid_type(
+    perform_actions, action_type, value
+):
+    action = {"type": value, "duration": 0}
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": action_type, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", ["", "pauses"])
+async def test_params_input_source_action_sequence_actions_subtype_invalid_value(
+    perform_actions, action_type, value
+):
+    action = {"type": value, "duration": 0}
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": action_type, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_input_source_action_sequence_actions_pause_duration_invalid_type(
+    perform_actions, action_type, value
+):
+    action = {"type": "pause", "duration": value}
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": action_type, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("action_type", ["none", "key", "pointer", "wheel"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_input_source_action_sequence_actions_pause_duration_invalid_value(
+    perform_actions, action_type, value
+):
+    action = {"type": "pause", "duration": value}
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": action_type, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", ["", "pauses"])
+async def test_params_null_action_type_invalid_value(perform_actions, value):
+    action = {"type": value, "duration": 0}
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "none", "id": "foo", "actions": [action]}])
+
+
+async def test_params_key_action_subtype_missing(perform_actions):
+    action = create_key_action("keyDown", {"value": "f"}, removals=["type"])
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "key", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", ["", "keyDowns", "keyUps"])
+async def test_params_key_action_subtype_invalid_value(perform_actions, value):
+    action = create_key_action(value, {"value": "f"})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "key", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("key_action", ["keyDown", "keyUp"])
+async def test_params_key_action_value_missing(perform_actions, key_action):
+    action = create_key_action(key_action, {"value": "f"}, removals=["value"])
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "key", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("key_action", ["keyDown", "keyUp"])
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_key_action_value_invalid_type(perform_actions, key_action, value):
+    action = create_key_action(key_action, {"value": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "key", "id": "foo", "actions": [action]}])
 
 
 @pytest.mark.parametrize(
     "value",
-    [("fa"), ("\u0BA8\u0BBFb"), ("\u0BA8\u0BBF\u0BA8"), ("\u1100\u1161\u11A8c")],
+    ["fa", "\u0BA8\u0BBFb", "\u0BA8\u0BBF\u0BA8", "\u1100\u1161\u11A8c"],
 )
-async def test_params_actions_invalid_value_multiple_codepoints(
-    bidi_session, top_context, setup_key_test, value
+async def test_params_key_action_value_invalid_multiple_codepoints(perform_actions, value):
+    actions = [
+        create_key_action("keyDown", {"value": value}),
+        create_key_action("keyUp", {"value": value}),
+    ]
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "key", "id": "foo", "actions": actions}])
+
+
+@pytest.mark.parametrize("value", ["", "pointerDowns", "pointerMoves", "pointerUps"])
+async def test_params_pointer_action_subtype_invalid_value(perform_actions, value):
+    if value == "pointerMoves":
+        action = create_pointer_action(value, {"x": 0, "y": 0})
+    else:
+        action = create_pointer_action(value, {"button": 0})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+async def test_params_pointer_action_up_down_button_missing(perform_actions,  coordinate):
+    action = create_pointer_action("pointerMove", removals=[coordinate])
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_move_coordinate_invalid_type(
+    perform_actions, coordinate, value
 ):
-    actions = Actions()
-    actions.add_key().key_down(value).key_up(value)
-    with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-@pytest.mark.parametrize("missing", ["x", "y"])
-async def test_params_actions_missing_coordinates(bidi_session, top_context, missing):
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=0, y=0)
-
-    json_actions = actions.to_json()
-    pointer_input_source_json = json_actions[-1]["actions"]
-    del pointer_input_source_json[-1][missing]
+    action = create_pointer_action(
+        "pointerMove",
+        {
+            "x": value if coordinate == "x" else 0,
+            "y": value if coordinate == "y" else 0,
+        },
+    )
 
     with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=json_actions, context=top_context["context"]
-        )
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_pointer_action_move_coordinate_invalid_value(
+    perform_actions, coordinate, value
+):
+    action = create_pointer_action(
+        "pointerMove",
+        {
+            "x": value if coordinate == "x" else 0,
+            "y": value if coordinate == "y" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_pointer_action_move_origin_invalid_type(perform_actions, value):
+    action = create_pointer_action("pointerMove", {"origin": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+async def test_params_pointer_action_move_origin_invalid_value(perform_actions, value):
+    action = create_pointer_action("pointerMove", {"origin": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+async def test_params_pointer_action_up_down_button_missing(perform_actions,  pointer_action):
+    action = create_pointer_action(pointer_action, removals=["button"])
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_up_down_button_invalid_type(
+    perform_actions, pointer_action, value
+):
+    action = create_pointer_action(pointer_action, {"button": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_pointer_action_up_down_button_invalid_value(
+    perform_actions, pointer_action, value
+):
+    action = create_pointer_action(pointer_action, {"button": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_common_properties_dimensions_invalid_type(
+    perform_actions, dimension, pointer_action, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "width": value if dimension == "width" else 0,
+            "height": value if dimension == "height" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("dimension", ["width", "height"])
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
+async def test_params_pointer_action_common_properties_dimensions_invalid_value(
+    perform_actions, dimension, pointer_action, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "width": value if dimension == "width" else 0,
+            "height": value if dimension == "height" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("pressure", ["pressure", "tangentialPressure"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+async def test_params_pointer_action_common_properties_pressure_invalid_type(
+    perform_actions, pointer_action, pressure, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "pressure": value if pressure == "pressure" else 0.0,
+            "tangentialPressure": value if pressure == "tangentialPressure" else 0.0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_common_properties_twist_invalid_type(
+    perform_actions, pointer_action, value
+):
+    action = create_pointer_action(pointer_action, {"twist": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("value", [-1, 360])
+async def test_params_pointer_action_common_properties_twist_invalid_value(
+    perform_actions, pointer_action, value
+):
+    action = create_pointer_action(pointer_action, {"twist": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("angle", ["altitudeAngle", "azimuthAngle"])
+@pytest.mark.parametrize("value", [None, "foo", True, [], {}])
+async def test_params_pointer_action_common_properties_angle_invalid_type(
+    perform_actions, pointer_action, angle, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "altitudeAngle": value if angle == "altitudeAngle" else 0.0,
+            "azimuthAngle": value if angle == "azimuthAngle" else 0.0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("tilt", ["tiltX", "tiltY"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_pointer_action_common_properties_tilt_invalid_type(
+    perform_actions, pointer_action, tilt, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "tiltX": value if tilt == "tiltX" else 0,
+            "tiltY": value if tilt == "tiltY" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
+@pytest.mark.parametrize("tilt", ["tiltX", "tiltY"])
+@pytest.mark.parametrize("value", [-91, 91])
+async def test_params_pointer_action_common_properties_tilt_invalid_value(
+    perform_actions, pointer_action, tilt, value
+):
+    action = create_pointer_action(
+        pointer_action,
+        {
+            "tiltX": value if tilt == "tiltX" else 0,
+            "tiltY": value if tilt == "tiltY" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "pointer", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_wheel_action_scroll_coordinate_invalid_type(
+    perform_actions, coordinate, value
+):
+    action = create_wheel_action(
+        "scroll",
+        {
+            "x": value if coordinate == "x" else 0,
+            "y": value if coordinate == "y" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("coordinate", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_wheel_action_scroll_coordinate_invalid_value(
+    perform_actions, coordinate, value
+):
+    action = create_wheel_action(
+        "scroll",
+        {
+            "x": value if coordinate == "x" else 0,
+            "y": value if coordinate == "y" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
+async def test_params_wheel_action_scroll_delta_invalid_type(
+    perform_actions, delta, value
+):
+    action = create_wheel_action(
+        "scroll",
+        {
+            "deltaX": value if delta == "x" else 0,
+            "deltaY": value if delta == "y" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("delta", ["x", "y"])
+@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+async def test_params_wheel_action_scroll_delta_invalid_value(
+    perform_actions, delta, value
+):
+    action = create_wheel_action(
+        "scroll",
+        {
+            "deltaX": value if delta == "x" else 0,
+            "deltaY": value if delta == "y" else 0,
+        },
+    )
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", [None, True, 42, [], {}])
+async def test_params_wheel_action_scroll_origin_invalid_type(perform_actions, value):
+    action = create_wheel_action("scroll", {"origin": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("value", ["", "pointers", "viewports"])
+async def test_params_wheel_action_scroll_origin_invalid_value(perform_actions, value):
+    action = create_wheel_action("scroll", {"origin": value})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
+
+
+async def test_params_wheel_action_scroll_origin_pointer_not_supported(perform_actions):
+    # Pointer origin isn't currently supported for wheel input source
+    # See: https://github.com/w3c/webdriver/issues/1758
+    action = create_wheel_action("scroll", {"origin": "pointer"})
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
 
 
 @pytest.mark.parametrize("missing", ["x", "y", "deltaX", "deltaY"])
-async def test_params_actions_missing_wheel_property(
-    bidi_session, top_context, missing
-):
-    actions = Actions()
-    actions.add_wheel().scroll(x=0, y=0, delta_x=5, delta_y=10)
-
-    json_actions = actions.to_json()
-    wheel_input_actions_json = json_actions[-1]["actions"]
-    del wheel_input_actions_json[-1][missing]
+async def test_params_wheel_action_scroll_property_missing(perform_actions, missing):
+    action = create_wheel_action("scroll", removals=[missing])
 
     with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=json_actions, context=top_context["context"]
-        )
+        await perform_actions([{"type": "wheel", "id": "foo", "actions": [action]}])
 
 
-async def test_params_actions_origin_element_outside_viewport(
-    bidi_session, top_context, get_actions_origin_page, get_element
+# Element origin tests for pointer and wheel input sources
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+async def test_params_origin_element_type_invalid_type(
+    perform_actions, input_source, value
 ):
-    url = get_actions_origin_page(
-        """width: 100px; height: 50px; background: green;
-           position: relative; left: -200px; top: -100px;"""
-    )
-    await bidi_session.browsing_context.navigate(
-        context=top_context["context"],
-        url=url,
-        wait="complete",
-    )
+    origin = {"origin": {"type": value}}
 
-    elem = await get_element("#inner")
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
 
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(elem))
-    with pytest.raises(MoveTargetOutOfBoundsException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-@pytest.mark.parametrize("value", [True, 42, []])
-async def test_params_actions_origin_invalid_type(bidi_session, top_context, value):
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=0, y=0, origin=value)
     with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
 
 
-@pytest.mark.parametrize("value", [None, True, 42, {}, [], "foo"])
-async def test_params_actions_origin_invalid_value_type(
-    bidi_session, top_context, get_actions_origin_page, get_element, value
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_element_missing(
+    perform_actions, input_source
+):
+    origin = {"origin": {"type": "element"}}
+
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, "foo", []])
+async def test_params_origin_element_element_invalid_type(
+    perform_actions, input_source, value
+):
+    origin = {"origin": {"type": "element", "element": value}}
+
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_element_sharedid_missing(
+    perform_actions, input_source
+):
+    origin = {"origin": {"type": "element", "element": {}}}
+
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+@pytest.mark.parametrize("value", [None, False, 42, [], {}])
+async def test_params_origin_element_element_sharedid_invalid_type(
+    perform_actions, input_source, value
+):
+    origin = {"origin": {"type": "element", "element": {"sharedId": value}}}
+
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
+
+    with pytest.raises(InvalidArgumentException):
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
+
+
+@pytest.mark.parametrize("input_source", ["pointer", "wheel"])
+async def test_params_origin_element_invalid_with_shared_reference(
+    bidi_session, top_context, get_actions_origin_page, get_element, perform_actions, input_source
 ):
     await bidi_session.browsing_context.navigate(
         context=top_context["context"],
@@ -119,114 +799,12 @@
         wait="complete",
     )
 
-    elem = await get_element("#inner")
-    actions = Actions()
-    actions.add_pointer().pointer_move(
-        x=0, y=0, origin={"type": value, "element": {"sharedId": elem["sharedId"]}}
-    )
+    origin = {"origin": await get_element("#inner")}
+
+    if input_source == "pointer":
+        action = create_pointer_action("pointerMove", origin)
+    elif input_source == "wheel":
+        action = create_wheel_action("scroll", origin)
+
     with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-@pytest.mark.parametrize("value", [None, True, 42, {}, [], "foo"])
-async def test_params_actions_origin_invalid_value_element(
-    bidi_session, top_context, value
-):
-    actions = Actions()
-    actions.add_pointer().pointer_move(
-        x=0, y=0, origin={"type": "element", "element": value}
-    )
-    with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-async def test_params_actions_origin_invalid_value_serialized_element(
-    bidi_session, top_context, get_actions_origin_page, get_element
-):
-    await bidi_session.browsing_context.navigate(
-        context=top_context["context"],
-        url=get_actions_origin_page(""),
-        wait="complete",
-    )
-
-    elem = await get_element("#inner")
-
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=0, y=0, origin=elem)
-    with pytest.raises(InvalidArgumentException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-@pytest.mark.parametrize(
-    "expression",
-    [
-        "document.querySelector('input#button').attributes[0]",
-        "document.querySelector('#with-text-node').childNodes[0]",
-        """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
-        "document.querySelector('#with-comment').childNodes[0]",
-        "document",
-        "document.doctype",
-        "document.createDocumentFragment()",
-        "document.querySelector('#custom-element').shadowRoot",
-    ],
-    ids=[
-        "attribute",
-        "text node",
-        "processing instruction",
-        "comment",
-        "document",
-        "doctype",
-        "document fragment",
-        "shadow root",
-    ]
-)
-async def test_params_actions_origin_no_such_element(
-    bidi_session, top_context, get_test_page, expression
-):
-    await bidi_session.browsing_context.navigate(
-        context=top_context["context"],
-        url=get_test_page(),
-        wait="complete",
-    )
-
-    node = await bidi_session.script.evaluate(
-        expression=expression,
-        target=ContextTarget(top_context["context"]),
-        await_promise=False,
-    )
-
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(node))
-    with pytest.raises(NoSuchElementException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-async def test_params_actions_origin_no_such_node(bidi_session, top_context):
-    actions = Actions()
-    actions.add_pointer().pointer_move(
-        x=0, y=0, origin={"type": "element", "element": {"sharedId": "foo"}}
-    )
-    with pytest.raises(NoSuchNodeException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
-
-
-@pytest.mark.parametrize("origin", ["viewport", "pointer"])
-async def test_params_actions_origin_outside_viewport(
-    bidi_session, top_context, origin
-):
-    actions = Actions()
-    actions.add_pointer().pointer_move(x=-50, y=-50, origin=origin)
-    with pytest.raises(MoveTargetOutOfBoundsException):
-        await bidi_session.input.perform_actions(
-            actions=actions, context=top_context["context"]
-        )
+        await perform_actions([{"type": input_source, "id": "foo", "actions": [action]}])
diff --git a/webdriver/tests/bidi/input/perform_actions/key.py b/webdriver/tests/bidi/input/perform_actions/key.py
index e721393..ac3099a 100644
--- a/webdriver/tests/bidi/input/perform_actions/key.py
+++ b/webdriver/tests/bidi/input/perform_actions/key.py
@@ -1,5 +1,6 @@
 import pytest
 
+from webdriver.bidi.error import NoSuchFrameException
 from webdriver.bidi.modules.input import Actions
 from webdriver.bidi.modules.script import ContextTarget
 
@@ -10,6 +11,14 @@
 pytestmark = pytest.mark.asyncio
 
 
+async def test_invalid_browsing_context(bidi_session):
+    actions = Actions()
+    actions.add_key()
+
+    with pytest.raises(NoSuchFrameException):
+        await bidi_session.input.perform_actions(actions=actions, context="foo")
+
+
 async def test_key_backspace(bidi_session, top_context, setup_key_test):
     actions = Actions()
     actions.add_key().send_keys("efcd").send_keys([Keys.BACKSPACE, Keys.BACKSPACE])
diff --git a/webdriver/tests/bidi/input/perform_actions/pointer.py b/webdriver/tests/bidi/input/perform_actions/pointer.py
new file mode 100644
index 0000000..6109450
--- /dev/null
+++ b/webdriver/tests/bidi/input/perform_actions/pointer.py
@@ -0,0 +1,15 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchFrameException
+from webdriver.bidi.modules.input import Actions
+
+
+pytestmark = pytest.mark.asyncio
+
+
+async def test_invalid_browsing_context(bidi_session):
+    actions = Actions()
+    actions.add_pointer()
+
+    with pytest.raises(NoSuchFrameException):
+        await bidi_session.input.perform_actions(actions=actions, context="foo")
diff --git a/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py b/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py
index a1b2532..c0c4d6f 100644
--- a/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py
+++ b/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py
@@ -1,5 +1,6 @@
 import pytest
 
+from webdriver.bidi.error import MoveTargetOutOfBoundsException
 from webdriver.bidi.modules.input import Actions, get_element_origin
 
 from tests.support.asserts import assert_move_to_coordinates
@@ -53,6 +54,41 @@
     assert expected == filtered_events[1:]
 
 
+@pytest.mark.parametrize("origin", ["pointer", "viewport"])
+async def test_params_actions_origin_outside_viewport(bidi_session, top_context, origin):
+    actions = Actions()
+    actions.add_pointer().pointer_move(x=-50, y=-50, origin=origin)
+
+    with pytest.raises(MoveTargetOutOfBoundsException):
+        await bidi_session.input.perform_actions(
+            actions=actions, context=top_context["context"]
+        )
+
+
+async def test_params_actions_origin_element_outside_viewport(
+    bidi_session, top_context, get_actions_origin_page, get_element
+):
+    url = get_actions_origin_page(
+        """width: 100px; height: 50px; background: green;
+           position: relative; left: -200px; top: -100px;"""
+    )
+    await bidi_session.browsing_context.navigate(
+        context=top_context["context"],
+        url=url,
+        wait="complete",
+    )
+
+    elem = await get_element("#inner")
+
+    actions = Actions()
+    actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(elem))
+
+    with pytest.raises(MoveTargetOutOfBoundsException):
+        await bidi_session.input.perform_actions(
+            actions=actions, context=top_context["context"]
+        )
+
+
 async def test_context_menu_at_coordinates(
     bidi_session, top_context, load_static_test_page
 ):
diff --git a/webdriver/tests/bidi/input/perform_actions/pointer_origin.py b/webdriver/tests/bidi/input/perform_actions/pointer_origin.py
index 8667574..f6721e0 100644
--- a/webdriver/tests/bidi/input/perform_actions/pointer_origin.py
+++ b/webdriver/tests/bidi/input/perform_actions/pointer_origin.py
@@ -1,5 +1,6 @@
 import pytest
 
+from webdriver.bidi.error import NoSuchElementException
 from webdriver.bidi.modules.input import Actions, get_element_origin
 from webdriver.bidi.modules.script import ContextTarget
 
@@ -146,3 +147,50 @@
                                                context=top_context)
     assert click_coords["x"] == pytest.approx(center["x"], abs=1.0)
     assert click_coords["y"] == pytest.approx(center["y"], abs=1.0)
+
+
+@pytest.mark.parametrize(
+    "expression",
+    [
+        "document.querySelector('input#button').attributes[0]",
+        "document.querySelector('#with-text-node').childNodes[0]",
+        """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
+        "document.querySelector('#with-comment').childNodes[0]",
+        "document",
+        "document.doctype",
+        "document.createDocumentFragment()",
+        "document.querySelector('#custom-element').shadowRoot",
+    ],
+    ids=[
+        "attribute",
+        "text node",
+        "processing instruction",
+        "comment",
+        "document",
+        "doctype",
+        "document fragment",
+        "shadow root",
+    ]
+)
+async def test_params_actions_origin_no_such_element(
+    bidi_session, top_context, get_test_page, expression
+):
+    await bidi_session.browsing_context.navigate(
+        context=top_context["context"],
+        url=get_test_page(),
+        wait="complete",
+    )
+
+    node = await bidi_session.script.evaluate(
+        expression=expression,
+        target=ContextTarget(top_context["context"]),
+        await_promise=False,
+    )
+
+    actions = Actions()
+    actions.add_pointer().pointer_move(x=0, y=0, origin=get_element_origin(node))
+
+    with pytest.raises(NoSuchElementException):
+        await bidi_session.input.perform_actions(
+            actions=actions, context=top_context["context"]
+        )
diff --git a/webdriver/tests/bidi/input/perform_actions/wheel.py b/webdriver/tests/bidi/input/perform_actions/wheel.py
index c4999fe..2da3861 100644
--- a/webdriver/tests/bidi/input/perform_actions/wheel.py
+++ b/webdriver/tests/bidi/input/perform_actions/wheel.py
@@ -1,5 +1,6 @@
 import pytest
 
+from webdriver.bidi.error import NoSuchFrameException
 from webdriver.bidi.modules.input import Actions, get_element_origin
 from webdriver.bidi.modules.script import ContextTarget
 
@@ -9,6 +10,14 @@
 pytestmark = pytest.mark.asyncio
 
 
+async def test_invalid_browsing_context(bidi_session):
+    actions = Actions()
+    actions.add_wheel()
+
+    with pytest.raises(NoSuchFrameException):
+        await bidi_session.input.perform_actions(actions=actions, context="foo")
+
+
 @pytest.mark.parametrize("delta_x, delta_y", [(0, 10), (5, 0), (5, 10)])
 async def test_scroll_not_scrollable(
     bidi_session, setup_wheel_test, top_context, get_element, delta_x, delta_y
diff --git a/webdriver/tests/bidi/input/perform_actions/wheel_origin.py b/webdriver/tests/bidi/input/perform_actions/wheel_origin.py
new file mode 100644
index 0000000..999b141
--- /dev/null
+++ b/webdriver/tests/bidi/input/perform_actions/wheel_origin.py
@@ -0,0 +1,55 @@
+import pytest
+
+from webdriver.bidi.error import NoSuchElementException
+from webdriver.bidi.modules.input import Actions, get_element_origin
+from webdriver.bidi.modules.script import ContextTarget
+
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest.mark.parametrize(
+    "expression",
+    [
+        "document.querySelector('input#button').attributes[0]",
+        "document.querySelector('#with-text-node').childNodes[0]",
+        """document.createProcessingInstruction("xml-stylesheet", "href='foo.css'")""",
+        "document.querySelector('#with-comment').childNodes[0]",
+        "document",
+        "document.doctype",
+        "document.createDocumentFragment()",
+        "document.querySelector('#custom-element').shadowRoot",
+    ],
+    ids=[
+        "attribute",
+        "text node",
+        "processing instruction",
+        "comment",
+        "document",
+        "doctype",
+        "document fragment",
+        "shadow root",
+    ]
+)
+async def test_params_actions_origin_no_such_element(
+    bidi_session, top_context, get_test_page, expression
+):
+    await bidi_session.browsing_context.navigate(
+        context=top_context["context"],
+        url=get_test_page(),
+        wait="complete",
+    )
+
+    node = await bidi_session.script.evaluate(
+        expression=expression,
+        target=ContextTarget(top_context["context"]),
+        await_promise=False,
+    )
+
+    actions = Actions()
+    actions.add_wheel().scroll(x=0, y=0, delta_x=5, delta_y=10, origin=get_element_origin(node))
+
+    with pytest.raises(NoSuchElementException):
+        await bidi_session.input.perform_actions(
+            actions=actions, context=top_context["context"]
+        )
diff --git a/webdriver/tests/classic/perform_actions/invalid.py b/webdriver/tests/classic/perform_actions/invalid.py
index 7f34a65..b030213 100644
--- a/webdriver/tests/classic/perform_actions/invalid.py
+++ b/webdriver/tests/classic/perform_actions/invalid.py
@@ -200,7 +200,7 @@
 ):
     actions = [
         {
-            "type": value,
+            "type": action_type,
             "id": "foo",
             "actions": [
                 {
@@ -461,38 +461,22 @@
 @pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
 @pytest.mark.parametrize("value", [None, "foo", True, 0.1, [], {}])
 def test_pointer_action_up_down_button_invalid_type(session, pointer_action, value):
-    actions = [
-        {
-            "type": "pointer",
-            "id": "foo",
-            "actions": [
-                {
-                    "type": pointer_action,
-                    "button": value,
-                }
-            ],
-        }
-    ]
-    response = perform_actions(session, actions)
+    action = create_pointer_common_object(pointer_action, {"button": value})
+
+    response = perform_actions(
+        session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+    )
     assert_error(response, "invalid argument")
 
 
 @pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerUp"])
 @pytest.mark.parametrize("value", [-1, MAX_INT + 1])
 def test_pointer_action_up_down_button_invalid_value(session, pointer_action, value):
-    actions = [
-        {
-            "type": "pointer",
-            "id": "foo",
-            "actions": [
-                {
-                    "type": pointer_action,
-                    "button": value,
-                }
-            ],
-        }
-    ]
-    response = perform_actions(session, actions)
+    action = create_pointer_common_object(pointer_action, {"button": value})
+
+    response = perform_actions(
+        session, [{"type": "pointer", "id": "foo", "actions": [action]}]
+    )
     assert_error(response, "invalid argument")
 
 
@@ -518,7 +502,7 @@
 
 @pytest.mark.parametrize("pointer_action", ["pointerDown", "pointerMove", "pointerUp"])
 @pytest.mark.parametrize("dimension", ["width", "height"])
-@pytest.mark.parametrize("value", [MIN_INT - 1, MAX_INT + 1])
+@pytest.mark.parametrize("value", [-1, MAX_INT + 1])
 def test_pointer_action_common_properties_dimensions_invalid_value(
     session, dimension, pointer_action, value
 ):
@@ -853,9 +837,7 @@
 def test_wheel_action_scroll_missing_property(
     session, test_actions_scroll_page, wheel_chain, missing
 ):
-    target = session.find.css("#scrollable", all=False)
-
-    actions = wheel_chain.scroll(0, 0, 5, 10, origin=target)
+    actions = wheel_chain.scroll(0, 0, 5, 10, origin="viewport")
     del actions._actions[-1][missing]
 
     with pytest.raises(InvalidArgumentException):