initial commit

This commit is contained in:
2026-06-25 21:29:21 +00:00
commit 0d0a7456de
2738 changed files with 542622 additions and 0 deletions
@@ -0,0 +1,16 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
@@ -0,0 +1,168 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any, Union
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.key_actions import KeyActions
from selenium.webdriver.common.actions.key_input import KeyInput
from selenium.webdriver.common.actions.pointer_actions import PointerActions
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.common.actions.wheel_actions import WheelActions
from selenium.webdriver.common.actions.wheel_input import WheelInput
from selenium.webdriver.remote.command import Command
class ActionBuilder:
def __init__(
self,
driver,
mouse: PointerInput | None = None,
wheel: WheelInput | None = None,
keyboard: KeyInput | None = None,
duration: int = 250,
) -> None:
mouse = mouse or PointerInput(interaction.POINTER_MOUSE, "mouse")
keyboard = keyboard or KeyInput(interaction.KEY)
wheel = wheel or WheelInput(interaction.WHEEL)
self.devices: list[PointerInput | KeyInput | WheelInput] = [mouse, keyboard, wheel]
self._key_action = KeyActions(keyboard)
self._pointer_action = PointerActions(mouse, duration=duration)
self._wheel_action = WheelActions(wheel)
self.driver = driver
def get_device_with(self, name: str) -> Union["WheelInput", "PointerInput", "KeyInput"] | None:
"""Get the device with the given name.
Args:
name: The name of the device to get.
Returns:
The device with the given name, or None if not found.
"""
return next(filter(lambda x: x == name, self.devices), None)
@property
def pointer_inputs(self) -> list[PointerInput]:
return [device for device in self.devices if isinstance(device, PointerInput)]
@property
def key_inputs(self) -> list[KeyInput]:
return [device for device in self.devices if isinstance(device, KeyInput)]
@property
def key_action(self) -> KeyActions:
return self._key_action
@property
def pointer_action(self) -> PointerActions:
return self._pointer_action
@property
def wheel_action(self) -> WheelActions:
return self._wheel_action
def add_key_input(self, name: str) -> KeyInput:
"""Add a new key input device to the action builder.
Args:
name: The name of the key input device.
Returns:
The newly created key input device.
Example:
>>> action_builder = ActionBuilder(driver)
>>> action_builder.add_key_input(name="keyboard2")
"""
new_input = KeyInput(name)
self._add_input(new_input)
return new_input
def add_pointer_input(self, kind: str, name: str) -> PointerInput:
"""Add a new pointer input device to the action builder.
Args:
kind: The kind of pointer input device. Valid values are "mouse",
"touch", or "pen".
name: The name of the pointer input device.
Returns:
The newly created pointer input device.
Example:
>>> action_builder = ActionBuilder(driver)
>>> action_builder.add_pointer_input(kind="mouse", name="mouse")
"""
new_input = PointerInput(kind, name)
self._add_input(new_input)
return new_input
def add_wheel_input(self, name: str) -> WheelInput:
"""Add a new wheel input device to the action builder.
Args:
name: The name of the wheel input device.
Returns:
The newly created wheel input device.
Example:
>>> action_builder = ActionBuilder(driver)
>>> action_builder.add_wheel_input(name="wheel2")
"""
new_input = WheelInput(name)
self._add_input(new_input)
return new_input
def perform(self) -> None:
"""Performs all stored actions.
Example:
>>> action_builder = ActionBuilder(driver)
>>> keyboard = action_builder.key_input
>>> el = driver.find_element(id: "some_id")
>>> action_builder.click(el).pause(keyboard).pause(keyboard).pause(keyboard).send_keys("keys").perform()
"""
enc: dict[str, list[Any]] = {"actions": []}
for device in self.devices:
encoded = device.encode()
if encoded["actions"]:
enc["actions"].append(encoded)
device.actions = []
self.driver.execute(Command.W3C_ACTIONS, enc)
def clear_actions(self) -> None:
"""Clears actions that are already stored on the remote end.
Example:
>>> action_builder = ActionBuilder(driver)
>>> keyboard = action_builder.key_input
>>> el = driver.find_element(By.ID, "some_id")
>>> action_builder.click(el).pause(keyboard).pause(keyboard).pause(keyboard).send_keys("keys")
>>> action_builder.clear_actions()
"""
self.driver.execute(Command.W3C_CLEAR_ACTIONS)
def _add_input(self, new_input: KeyInput | PointerInput | WheelInput) -> None:
"""Add a new input device to the action builder.
Args:
new_input: The new input device to add.
"""
self.devices.append(new_input)
@@ -0,0 +1,36 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import uuid
from typing import Any
class InputDevice:
"""Describes the input device being used for the action."""
def __init__(self, name: str | None = None):
self.name = name or uuid.uuid4()
self.actions: list[Any] = []
def add_action(self, action: Any) -> None:
self.actions.append(action)
def clear_actions(self) -> None:
self.actions = []
def create_pause(self, duration: float = 0) -> None:
pass
@@ -0,0 +1,46 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.actions.input_device import InputDevice
KEY = "key"
POINTER = "pointer"
NONE = "none"
WHEEL = "wheel"
SOURCE_TYPES = {KEY, POINTER, WHEEL, NONE}
POINTER_MOUSE = "mouse"
POINTER_TOUCH = "touch"
POINTER_PEN = "pen"
POINTER_KINDS = {POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN}
class Interaction:
PAUSE = "pause"
def __init__(self, source: InputDevice) -> None:
self.source = source
class Pause(Interaction):
def __init__(self, source, duration: float = 0) -> None:
super().__init__(source)
self.duration = duration
def encode(self) -> dict[str, str | int]:
return {"type": self.PAUSE, "duration": int(self.duration * 1000)}
@@ -0,0 +1,54 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
from selenium.webdriver.common.actions.interaction import KEY, Interaction
from selenium.webdriver.common.actions.key_input import KeyInput
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.common.actions.wheel_input import WheelInput
from selenium.webdriver.common.utils import keys_to_typing
class KeyActions(Interaction):
def __init__(self, source: KeyInput | PointerInput | WheelInput | None = None) -> None:
if source is None:
source = KeyInput(KEY)
self.input_source = source
super().__init__(source)
def key_down(self, letter: str) -> KeyActions:
return self._key_action("create_key_down", letter)
def key_up(self, letter: str) -> KeyActions:
return self._key_action("create_key_up", letter)
def pause(self, duration: int = 0) -> KeyActions:
return self._key_action("create_pause", duration)
def send_keys(self, text: str | list) -> KeyActions:
if not isinstance(text, list):
text = keys_to_typing(text)
for letter in text:
self.key_down(letter)
self.key_up(letter)
return self
def _key_action(self, action: str, letter) -> KeyActions:
meth = getattr(self.source, action)
meth(letter)
return self
@@ -0,0 +1,48 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.input_device import InputDevice
from selenium.webdriver.common.actions.interaction import Interaction, Pause
class KeyInput(InputDevice):
def __init__(self, name: str) -> None:
super().__init__()
self.name = name
self.type = interaction.KEY
def encode(self) -> dict:
return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]}
def create_key_down(self, key) -> None:
self.add_action(TypingInteraction(self, "keyDown", key))
def create_key_up(self, key) -> None:
self.add_action(TypingInteraction(self, "keyUp", key))
def create_pause(self, pause_duration: float = 0) -> None:
self.add_action(Pause(self, pause_duration))
class TypingInteraction(Interaction):
def __init__(self, source, type_, key) -> None:
super().__init__(source)
self.type = type_
self.key = key
def encode(self) -> dict:
return {"type": self.type, "value": self.key}
@@ -0,0 +1,24 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
class MouseButton:
LEFT = 0
MIDDLE = 1
RIGHT = 2
BACK = 3
FORWARD = 4
@@ -0,0 +1,206 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.interaction import Interaction
from selenium.webdriver.common.actions.mouse_button import MouseButton
from selenium.webdriver.common.actions.pointer_input import PointerInput
from selenium.webdriver.remote.webelement import WebElement
class PointerActions(Interaction):
def __init__(self, source: PointerInput | None = None, duration: int = 250):
"""Initialize a new PointerActions instance.
Args:
source: Optional PointerInput instance. If not provided, a default
mouse PointerInput will be created.
duration: Override the default 250 msecs of DEFAULT_MOVE_DURATION
in the source.
"""
if source is None:
source = PointerInput(interaction.POINTER_MOUSE, "mouse")
self.source = source
self._duration = duration
super().__init__(source)
def pointer_down(
self,
button=MouseButton.LEFT,
width=None,
height=None,
pressure=None,
tangential_pressure=None,
tilt_x=None,
tilt_y=None,
twist=None,
altitude_angle=None,
azimuth_angle=None,
):
self._button_action(
"create_pointer_down",
button=button,
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,
)
return self
def pointer_up(self, button=MouseButton.LEFT):
self._button_action("create_pointer_up", button=button)
return self
def move_to(
self,
element,
x=0,
y=0,
width=None,
height=None,
pressure=None,
tangential_pressure=None,
tilt_x=None,
tilt_y=None,
twist=None,
altitude_angle=None,
azimuth_angle=None,
):
if not isinstance(element, WebElement):
raise AttributeError("move_to requires a WebElement")
self.source.create_pointer_move(
origin=element,
duration=self._duration,
x=int(x),
y=int(y),
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,
)
return self
def move_by(
self,
x,
y,
width=None,
height=None,
pressure=None,
tangential_pressure=None,
tilt_x=None,
tilt_y=None,
twist=None,
altitude_angle=None,
azimuth_angle=None,
):
self.source.create_pointer_move(
origin=interaction.POINTER,
duration=self._duration,
x=int(x),
y=int(y),
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,
)
return self
def move_to_location(
self,
x,
y,
width=None,
height=None,
pressure=None,
tangential_pressure=None,
tilt_x=None,
tilt_y=None,
twist=None,
altitude_angle=None,
azimuth_angle=None,
):
self.source.create_pointer_move(
origin="viewport",
duration=self._duration,
x=int(x),
y=int(y),
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,
)
return self
def click(self, element: WebElement | None = None, button=MouseButton.LEFT):
if element:
self.move_to(element)
self.pointer_down(button)
self.pointer_up(button)
return self
def context_click(self, element: WebElement | None = None):
return self.click(element=element, button=MouseButton.RIGHT)
def click_and_hold(self, element: WebElement | None = None, button=MouseButton.LEFT):
if element:
self.move_to(element)
self.pointer_down(button=button)
return self
def release(self, button=MouseButton.LEFT):
self.pointer_up(button=button)
return self
def double_click(self, element: WebElement | None = None):
if element:
self.move_to(element)
self.pointer_down(MouseButton.LEFT)
self.pointer_up(MouseButton.LEFT)
self.pointer_down(MouseButton.LEFT)
self.pointer_up(MouseButton.LEFT)
return self
def pause(self, duration: float = 0):
self.source.create_pause(duration)
return self
def _button_action(self, action, **kwargs):
meth = getattr(self.source, action)
meth(**kwargs)
return self
@@ -0,0 +1,79 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import Any
from selenium.common.exceptions import InvalidArgumentException
from selenium.webdriver.common.actions.input_device import InputDevice
from selenium.webdriver.common.actions.interaction import POINTER, POINTER_KINDS
from selenium.webdriver.remote.webelement import WebElement
class PointerInput(InputDevice):
DEFAULT_MOVE_DURATION = 250
def __init__(self, kind, name):
super().__init__()
if kind not in POINTER_KINDS:
raise InvalidArgumentException(f"Invalid PointerInput kind '{kind}'")
self.type = POINTER
self.kind = kind
self.name = name
def create_pointer_move(
self,
duration=DEFAULT_MOVE_DURATION,
x: float = 0,
y: float = 0,
origin: WebElement | None = None,
**kwargs,
):
action = {"type": "pointerMove", "duration": duration, "x": x, "y": y, **kwargs}
if isinstance(origin, WebElement):
action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
elif origin is not None:
action["origin"] = origin
self.add_action(self._convert_keys(action))
def create_pointer_down(self, **kwargs):
data = {"type": "pointerDown", "duration": 0, **kwargs}
self.add_action(self._convert_keys(data))
def create_pointer_up(self, button):
self.add_action({"type": "pointerUp", "duration": 0, "button": button})
def create_pointer_cancel(self):
self.add_action({"type": "pointerCancel"})
def create_pause(self, pause_duration: int | float = 0) -> None:
self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})
def encode(self):
return {"type": self.type, "parameters": {"pointerType": self.kind}, "id": self.name, "actions": self.actions}
def _convert_keys(self, actions: dict[str, Any]):
out = {}
for k, v in actions.items():
if v is None:
continue
if k in ("x", "y"):
out[k] = int(v)
continue
splits = k.split("_")
new_key = splits[0] + "".join(v.title() for v in splits[1:])
out[new_key] = v
return out
@@ -0,0 +1,35 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.actions.interaction import WHEEL, Interaction
from selenium.webdriver.common.actions.wheel_input import WheelInput
class WheelActions(Interaction):
def __init__(self, source: WheelInput | None = None):
if source is None:
source = WheelInput(WHEEL)
super().__init__(source)
def pause(self, duration: float = 0):
self.source.create_pause(duration)
return self
def scroll(self, x=0, y=0, delta_x=0, delta_y=0, duration=0, origin="viewport"):
self.source.create_scroll(x, y, delta_x, delta_y, duration, origin)
return self
@@ -0,0 +1,75 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common.actions import interaction
from selenium.webdriver.common.actions.input_device import InputDevice
from selenium.webdriver.remote.webelement import WebElement
class ScrollOrigin:
def __init__(self, origin: str | WebElement, x_offset: int, y_offset: int) -> None:
self._origin = origin
self._x_offset = x_offset
self._y_offset = y_offset
@classmethod
def from_element(cls, element: WebElement, x_offset: int = 0, y_offset: int = 0):
return cls(element, x_offset, y_offset)
@classmethod
def from_viewport(cls, x_offset: int = 0, y_offset: int = 0):
return cls("viewport", x_offset, y_offset)
@property
def origin(self) -> str | WebElement:
return self._origin
@property
def x_offset(self) -> int:
return self._x_offset
@property
def y_offset(self) -> int:
return self._y_offset
class WheelInput(InputDevice):
def __init__(self, name) -> None:
super().__init__(name=name)
self.name = name
self.type = interaction.WHEEL
def encode(self) -> dict:
return {"type": self.type, "id": self.name, "actions": self.actions}
def create_scroll(self, x: int, y: int, delta_x: int, delta_y: int, duration: int, origin) -> None:
if isinstance(origin, WebElement):
origin = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
self.add_action(
{
"type": "scroll",
"x": x,
"y": y,
"deltaX": delta_x,
"deltaY": delta_y,
"duration": duration,
"origin": origin,
}
)
def create_pause(self, pause_duration: int | float = 0) -> None:
self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})