Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Gecko Bug 1792090] Add input module to bidi test client #39291

Merged
merged 3 commits into from Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/webdriver/webdriver/bidi/client.py
Expand Up @@ -92,6 +92,7 @@ def __init__(self,
# For each module, have a property representing that module
self.session = modules.Session(self)
self.browsing_context = modules.BrowsingContext(self)
self.input = modules.Input(self)
self.script = modules.Script(self)

@property
Expand Down
1 change: 1 addition & 0 deletions tools/webdriver/webdriver/bidi/modules/__init__.py
Expand Up @@ -2,4 +2,5 @@

from .session import Session
from .browsing_context import BrowsingContext
from .input import Input
from .script import Script
357 changes: 357 additions & 0 deletions tools/webdriver/webdriver/bidi/modules/input.py
@@ -0,0 +1,357 @@
from collections import defaultdict

from typing import (Any,
ClassVar,
List,
Mapping,
MutableMapping,
Optional,
Sequence,
Set,
Type,
TypeVar,
Union)

from ._module import BidiModule, command


InputSourceType = TypeVar('InputSourceType', bound="InputSource")

class Action:
action_type: ClassVar[str]

def to_json(self) -> MutableMapping[str, Any]:
return {"type": self.action_type}


class PauseAction(Action):
action_type = "pause"

def __init__(self, duration: Optional[int] = None):
self.duration = duration


class KeyAction(Action):
def __init__(self, key: str):
self.value = key

def to_json(self) -> MutableMapping[str, Any]:
rv = super().to_json()
rv["value"] = self.value
return rv

class KeyUpAction(KeyAction):
action_type = "keyUp"


class KeyDownAction(KeyAction):
action_type = "keyDown"


class PointerAction(Action):
def __init__(self,
button: Optional[int] = None,
x: Optional[int] = None,
y: Optional[int] = None,
duration: Optional[int] = None,
origin: Optional[Union[str, Mapping[str, Any]]] = None,
width: Optional[int] = None,
height: Optional[int] = None,
pressure: Optional[float] = None,
tangential_pressure: Optional[float] = None,
tilt_x: Optional[int] = None,
tilt_y: Optional[int] = None,
twist: Optional[int] = None,
altitude_angle: Optional[float] = None,
azimuth_angle: Optional[float] = None):
self.button = button
self.x = x
self.y = y
self.duration = duration
self.origin = origin
self.width = width,
self.height = height
self.pressure = pressure
self.tangential_pressure = tangential_pressure
self.tilt_x = tilt_x,
self.tilt_y = tilt_y
self.twist = twist
self.altitude_angle = altitude_angle
self.azimuth_angle = azimuth_angle

def to_json(self) -> MutableMapping[str, Any]:
rv = super().to_json()
if self.button is not None:
rv["button"] = self.button
if self.x is not None:
rv["x"] = self.x
if self.y is not None:
rv["y"] = self.y
if self.duration is not None:
rv["duration"] = self.duration
if self.origin is not None:
rv["origin"] = self.origin
if self.width is not None:
rv["width"] = self.width
if self.height is not None:
rv["height"] = self.height
if self.pressure is not None:
rv["pressure"] = self.pressure
if self.tangential_pressure is not None:
rv["tangentialPressure"] = self.tangential_pressure
if self.tilt_x is not None:
rv["tiltX"] = self.tilt_x
if self.tilt_y is not None:
rv["tiltY"] = self.tilt_y
if self.twist is not None:
rv["twist"] = self.twist
if self.altitude_angle is not None:
rv["altitudeAngle"] = self.altitude_angle
if self.azimuth_angle is not None:
rv["azimuthAngle"] = self.azimuth_angle
return rv


class PointerDownAction(PointerAction):
type = "pointerDown"

def __init__(self,
button: int,
width: Optional[int] = None,
height: Optional[int] = None,
pressure: Optional[float] = None,
tangential_pressure: Optional[float] = None,
tilt_x: Optional[int] = None,
tilt_y: Optional[int] = None,
twist: Optional[int] = None,
altitude_angle: Optional[float] = None,
azimuth_angle: Optional[float] = None):
super().__init__(button=button, x=None, y=None, duration=None,
origin=None, width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure, tilt_x=tilt_x,
tilt_y=tilt_y, twist=twist, altitude_angle=altitude_angle,
azimuth_angle=azimuth_angle)


class PointerUpAction(PointerAction):
type = "pointerUp"

def __init__(self,
button: int,
width: Optional[int] = None,
height: Optional[int] = None,
pressure: Optional[float] = None,
tangential_pressure: Optional[float] = None,
tilt_x: Optional[int] = None,
tilt_y: Optional[int] = None,
twist: Optional[int] = None,
altitude_angle: Optional[float] = None,
azimuth_angle: Optional[float] = None):
super().__init__(button=button, x=None, y=None, duration=None,
origin=None, width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure, tilt_x=tilt_x,
tilt_y=tilt_y, twist=twist, altitude_angle=altitude_angle,
azimuth_angle=azimuth_angle)


class PointerMoveAction(PointerAction):
type = "pointerMove"

def __init__(self,
x: int,
y: int,
duration:Optional[int] = None,
origin: Optional[Union[str, Mapping[str, Any]]] = None,
width: Optional[int] = None,
height: Optional[int] = None,
pressure: Optional[float] = None,
tangential_pressure: Optional[float] = None,
tilt_x: Optional[int] = None,
tilt_y: Optional[int] = None,
twist: Optional[int] = None,
altitude_angle: Optional[float] = None,
azimuth_angle: Optional[float] = None):
super().__init__(button=None, x=x, y=y, duration=duration,
origin=origin, width=width, height=height, pressure=pressure,
tangential_pressure=tangential_pressure, tilt_x=tilt_x,
tilt_y=tilt_y, twist=twist, altitude_angle=altitude_angle,
azimuth_angle=azimuth_angle)


class WheelScrollAction(Action):
type = "scroll"

def __init__(self,
x: int,
y: int,
delta_x: int,
delta_y: int,
duration: Optional[int] = None,
origin: Optional[Union[str, Mapping[str, Any]]] = None):
self.x = x
self.y = y
self.delta_x = delta_x
self.delta_y = delta_y
self.duration = duration
self.origin = origin

def to_json(self) -> MutableMapping[str, Any]:
rv = super().to_json()
rv.update({
"x": self.x,
"y": self.y,
"deltaX": self.delta_x,
"deltaY": self.delta_y
})
if self.duration is not None:
rv["duration"] = self.duration
if self.origin is not None:
rv["origin"] = self.origin
return rv


class InputSource:
input_type: ClassVar[str]

def __init__(self, input_id: str, **kwargs: Any):
"""Represents a sequence of actions of one type for one input source.

:param input_id: ID of input source.
"""
self.id = input_id
self.actions: List[Action] = []

def __len__(self) -> int:
return len(self.actions)

def to_json(self, total_ticks: int) -> MutableMapping[str, Any]:
actions = [item.to_json() for item in self.actions]
for i in range(total_ticks - len(self)):
actions.append(PauseAction().to_json())

return {"id": self.id,
"type": self.input_type,
"actions": actions}

def done(self) -> List[Action]:
return self.actions

def pause(self: InputSourceType, duration: Optional[int] = None) -> InputSourceType:
self.actions.append(PauseAction(duration))
return self


class KeyInputSource(InputSource):
input_type = "key"

def key_down(self, key: str) -> "KeyInputSource":
self.actions.append(KeyDownAction(key))
return self

def key_up(self, key: str) -> "KeyInputSource":
self.actions.append(KeyUpAction(key))
return self


class PointerInputSource(InputSource):
input_type = "pointer"

def __init__(self, input_id: str, pointer_type: str = "mouse"):
super().__init__(input_id)
self.parameters = {"pointerType": pointer_type}

def to_json(self, total_ticks: int) -> MutableMapping[str, Any]:
rv = super().to_json(total_ticks)
rv["parameters"] = self.parameters
return rv

def pointer_down(self, button: int, **kwargs: Any) -> "PointerInputSource":
self.actions.append(PointerDownAction(button, **kwargs))
return self

def pointer_up(self, button: int, **kwargs: Any) -> "PointerInputSource":
self.actions.append(PointerUpAction(button, **kwargs))
return self

def pointer_move(self,
x: int,
y: int,
duration: Optional[int] = None,
origin: Union[str, Mapping[str, Any]] = "viewport",
**kwargs: Any) -> "PointerInputSource":
self.actions.append(PointerMoveAction(x, y, duration=duration, origin=origin, **kwargs))
return self


class WheelInputSource(InputSource):
input_type = "wheel"

def scroll(self,
x: int,
y: int,
delta_x: int = 0,
delta_y: int = 0,
duration: Optional[int] = None,
origin: Union[str, Mapping[str, Any]] = "viewport") -> "WheelInputSource":
self.actions.append(WheelScrollAction(x,
y,
delta_x=delta_x,
delta_y=delta_y,
duration=duration,
origin=origin))
return self


class Actions:
def __init__(self) -> None:
self.input_sources: List[InputSource] = []
self.seen_names: MutableMapping[str, Set[str]] = defaultdict(Set)

def _add_source(self,
cls: Type[InputSourceType],
input_id: Optional[str] = None,
**kwargs: Any) -> InputSourceType:
input_type = cls.input_type
if input_id is None:
i = 0
input_id = f"{input_type}-{i}"
while input_id in self.seen_names[input_type]:
i += 1
input_id = f"{input_type}-{i}"
else:
if input_id in self.seen_names[input_type]:
raise ValueError(f"Duplicate input id ${input_id}")

self.seen_names[input_type].add(input_id)
rv = cls(input_id, **kwargs)
self.input_sources.append(rv)
return rv

def add_key(self, input_id: Optional[str] = None) -> "KeyInputSource":
return self._add_source(KeyInputSource, input_id)

def add_pointer(self, input_id: Optional[str] = None, pointer_type: str = "mouse") -> "PointerInputSource":
return self._add_source(PointerInputSource, input_id, pointer_type=pointer_type)

def add_wheel(self, input_id: Optional[str] = None) -> "WheelInputSource":
return self._add_source(WheelInputSource, input_id)

def to_json(self) -> Sequence[Mapping[str, Any]]:
num_ticks = max(len(item) for item in self.input_sources)
return [item.to_json(num_ticks) for item in self.input_sources]


class Input(BidiModule):
@command
def perform_actions(self,
actions: Union[Actions, List[Any]],
context: str
) -> Mapping[str, Any]:
params: MutableMapping[str, Any] = {
"context": context
}
if isinstance(actions, Actions):
params["actions"] = actions.to_json()
else:
params["actions"] = actions
return params
14 changes: 14 additions & 0 deletions webdriver/tests/bidi/input/perform_actions/key.py
@@ -0,0 +1,14 @@
import pytest

from webdriver.bidi.modules.input import Actions

pytestmark = pytest.mark.asyncio


async def test_null_response_value(bidi_session, top_context):
actions = Actions()
actions.add_key().key_down("a").key_up("a")
value = await bidi_session.input.perform_actions(actions=actions,
context=top_context["context"])
assert value == {}