Updated script that can be controled by Nodejs web app

This commit is contained in:
mac OS
2024-11-25 12:24:18 +07:00
parent c440eda1f4
commit 8b0ab2bd3a
8662 changed files with 1803808 additions and 34 deletions

View File

@ -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.

View File

@ -0,0 +1,103 @@
# 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 List
from typing import Optional
from typing import Union
from selenium.webdriver.remote.command import Command
from . import interaction
from .key_actions import KeyActions
from .key_input import KeyInput
from .pointer_actions import PointerActions
from .pointer_input import PointerInput
from .wheel_actions import WheelActions
from .wheel_input import WheelInput
class ActionBuilder:
def __init__(
self,
driver,
mouse: Optional[PointerInput] = None,
wheel: Optional[WheelInput] = None,
keyboard: Optional[KeyInput] = 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 = [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) -> Optional[Union["WheelInput", "PointerInput", "KeyInput"]]:
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 device.type == interaction.POINTER]
@property
def key_inputs(self) -> List[KeyInput]:
return [device for device in self.devices if device.type == interaction.KEY]
@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:
new_input = KeyInput(name)
self._add_input(new_input)
return new_input
def add_pointer_input(self, kind: str, name: str) -> PointerInput:
new_input = PointerInput(kind, name)
self._add_input(new_input)
return new_input
def add_wheel_input(self, name: str) -> WheelInput:
new_input = WheelInput(name)
self._add_input(new_input)
return new_input
def perform(self) -> None:
enc = {"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."""
self.driver.execute(Command.W3C_CLEAR_ACTIONS)
def _add_input(self, new_input: Union[KeyInput, PointerInput, WheelInput]) -> None:
self.devices.append(new_input)

View File

@ -0,0 +1,39 @@
# 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
from typing import List
from typing import Optional
class InputDevice:
"""Describes the input device being used for the action."""
def __init__(self, name: Optional[str] = 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

View File

@ -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 typing import Dict
from typing import Union
KEY = "key"
POINTER = "pointer"
NONE = "none"
WHEEL = "wheel"
SOURCE_TYPES = {KEY, POINTER, 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: str) -> 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, Union[str, int]]:
return {"type": self.PAUSE, "duration": int(self.duration * 1000)}

View File

@ -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 ..utils import keys_to_typing
from .interaction import KEY
from .interaction import Interaction
from .key_input import KeyInput
from .pointer_input import PointerInput
from .wheel_input import WheelInput
class KeyActions(Interaction):
def __init__(self, source: KeyInput | PointerInput | WheelInput | None = None) -> None:
if not source:
source = KeyInput(KEY)
self.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

View File

@ -0,0 +1,49 @@
# 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 . import interaction
from .input_device import InputDevice
from .interaction import Interaction
from .interaction import 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}

View File

@ -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

View File

@ -0,0 +1,205 @@
# 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 Optional
from selenium.webdriver.remote.webelement import WebElement
from . import interaction
from .interaction import Interaction
from .mouse_button import MouseButton
from .pointer_input import PointerInput
class PointerActions(Interaction):
def __init__(self, source: Optional[PointerInput] = None, duration: int = 250):
"""
Args:
- source: PointerInput instance
- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in source
"""
if not source:
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: Optional[WebElement] = 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: Optional[WebElement] = None):
return self.click(element=element, button=MouseButton.RIGHT)
def click_and_hold(self, element: Optional[WebElement] = 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: Optional[WebElement] = 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

View File

@ -0,0 +1,81 @@
# 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 typing
from typing import Union
from selenium.common.exceptions import InvalidArgumentException
from selenium.webdriver.remote.webelement import WebElement
from .input_device import InputDevice
from .interaction import POINTER
from .interaction import POINTER_KINDS
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: typing.Optional[WebElement] = 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: Union[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: typing.Dict[str, typing.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

View File

@ -0,0 +1,33 @@
# 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 .interaction import Interaction
from .wheel_input import WheelInput
class WheelActions(Interaction):
def __init__(self, source: WheelInput = None):
if not source:
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

View File

@ -0,0 +1,77 @@
# 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 Union
from selenium.webdriver.remote.webelement import WebElement
from . import interaction
from .input_device import InputDevice
class ScrollOrigin:
def __init__(self, origin: Union[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) -> Union[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: Union[int, float] = 0) -> None:
self.add_action({"type": "pause", "duration": int(pause_duration * 1000)})