first commit
This commit is contained in:
commit
5d3b2f218b
2817 files changed
+516548
No files matched your search
@@ -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.
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,380 @@
|
||||
# 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.
|
||||
"""The ActionChains implementation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
|
||||
from .actions.action_builder import ActionBuilder
|
||||
from .actions.key_input import KeyInput
|
||||
from .actions.pointer_input import PointerInput
|
||||
from .actions.wheel_input import ScrollOrigin
|
||||
from .actions.wheel_input import WheelInput
|
||||
from .utils import keys_to_typing
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
|
||||
AnyDevice = Union[PointerInput, KeyInput, WheelInput]
|
||||
|
||||
|
||||
class ActionChains:
|
||||
"""ActionChains are a way to automate low level interactions such as mouse
|
||||
movements, mouse button actions, key press, and context menu interactions.
|
||||
This is useful for doing more complex actions like hover over and drag and
|
||||
drop.
|
||||
|
||||
Generate user actions.
|
||||
When you call methods for actions on the ActionChains object,
|
||||
the actions are stored in a queue in the ActionChains object.
|
||||
When you call perform(), the events are fired in the order they
|
||||
are queued up.
|
||||
|
||||
ActionChains can be used in a chain pattern::
|
||||
|
||||
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
|
||||
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
|
||||
|
||||
ActionChains(driver).move_to_element(menu).click(hidden_submenu).perform()
|
||||
|
||||
Or actions can be queued up one by one, then performed.::
|
||||
|
||||
menu = driver.find_element(By.CSS_SELECTOR, ".nav")
|
||||
hidden_submenu = driver.find_element(By.CSS_SELECTOR, ".nav #submenu1")
|
||||
|
||||
actions = ActionChains(driver)
|
||||
actions.move_to_element(menu)
|
||||
actions.click(hidden_submenu)
|
||||
actions.perform()
|
||||
|
||||
Either way, the actions are performed in the order they are called, one after
|
||||
another.
|
||||
"""
|
||||
|
||||
def __init__(self, driver: WebDriver, duration: int = 250, devices: list[AnyDevice] | None = None) -> None:
|
||||
"""Creates a new ActionChains.
|
||||
|
||||
:Args:
|
||||
- driver: The WebDriver instance which performs user actions.
|
||||
- duration: override the default 250 msecs of DEFAULT_MOVE_DURATION in PointerInput
|
||||
"""
|
||||
self._driver = driver
|
||||
mouse = None
|
||||
keyboard = None
|
||||
wheel = None
|
||||
if devices is not None and isinstance(devices, list):
|
||||
for device in devices:
|
||||
if isinstance(device, PointerInput):
|
||||
mouse = device
|
||||
if isinstance(device, KeyInput):
|
||||
keyboard = device
|
||||
if isinstance(device, WheelInput):
|
||||
wheel = device
|
||||
self.w3c_actions = ActionBuilder(driver, mouse=mouse, keyboard=keyboard, wheel=wheel, duration=duration)
|
||||
|
||||
def perform(self) -> None:
|
||||
"""Performs all stored actions."""
|
||||
self.w3c_actions.perform()
|
||||
|
||||
def reset_actions(self) -> None:
|
||||
"""Clears actions that are already stored locally and on the remote
|
||||
end."""
|
||||
self.w3c_actions.clear_actions()
|
||||
for device in self.w3c_actions.devices:
|
||||
device.clear_actions()
|
||||
|
||||
def click(self, on_element: WebElement | None = None) -> ActionChains:
|
||||
"""Clicks an element.
|
||||
|
||||
:Args:
|
||||
- on_element: The element to click.
|
||||
If None, clicks on current mouse position.
|
||||
"""
|
||||
if on_element:
|
||||
self.move_to_element(on_element)
|
||||
|
||||
self.w3c_actions.pointer_action.click()
|
||||
self.w3c_actions.key_action.pause()
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def click_and_hold(self, on_element: WebElement | None = None) -> ActionChains:
|
||||
"""Holds down the left mouse button on an element.
|
||||
|
||||
:Args:
|
||||
- on_element: The element to mouse down.
|
||||
If None, clicks on current mouse position.
|
||||
"""
|
||||
if on_element:
|
||||
self.move_to_element(on_element)
|
||||
|
||||
self.w3c_actions.pointer_action.click_and_hold()
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def context_click(self, on_element: WebElement | None = None) -> ActionChains:
|
||||
"""Performs a context-click (right click) on an element.
|
||||
|
||||
:Args:
|
||||
- on_element: The element to context-click.
|
||||
If None, clicks on current mouse position.
|
||||
"""
|
||||
if on_element:
|
||||
self.move_to_element(on_element)
|
||||
|
||||
self.w3c_actions.pointer_action.context_click()
|
||||
self.w3c_actions.key_action.pause()
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def double_click(self, on_element: WebElement | None = None) -> ActionChains:
|
||||
"""Double-clicks an element.
|
||||
|
||||
:Args:
|
||||
- on_element: The element to double-click.
|
||||
If None, clicks on current mouse position.
|
||||
"""
|
||||
if on_element:
|
||||
self.move_to_element(on_element)
|
||||
|
||||
self.w3c_actions.pointer_action.double_click()
|
||||
for _ in range(4):
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def drag_and_drop(self, source: WebElement, target: WebElement) -> ActionChains:
|
||||
"""Holds down the left mouse button on the source element, then moves
|
||||
to the target element and releases the mouse button.
|
||||
|
||||
:Args:
|
||||
- source: The element to mouse down.
|
||||
- target: The element to mouse up.
|
||||
"""
|
||||
self.click_and_hold(source)
|
||||
self.release(target)
|
||||
return self
|
||||
|
||||
def drag_and_drop_by_offset(self, source: WebElement, xoffset: int, yoffset: int) -> ActionChains:
|
||||
"""Holds down the left mouse button on the source element, then moves
|
||||
to the target offset and releases the mouse button.
|
||||
|
||||
:Args:
|
||||
- source: The element to mouse down.
|
||||
- xoffset: X offset to move to.
|
||||
- yoffset: Y offset to move to.
|
||||
"""
|
||||
self.click_and_hold(source)
|
||||
self.move_by_offset(xoffset, yoffset)
|
||||
self.release()
|
||||
return self
|
||||
|
||||
def key_down(self, value: str, element: WebElement | None = None) -> ActionChains:
|
||||
"""Sends a key press only, without releasing it. Should only be used
|
||||
with modifier keys (Control, Alt and Shift).
|
||||
|
||||
:Args:
|
||||
- value: The modifier key to send. Values are defined in `Keys` class.
|
||||
- element: The element to send keys.
|
||||
If None, sends a key to current focused element.
|
||||
|
||||
Example, pressing ctrl+c::
|
||||
|
||||
ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
|
||||
"""
|
||||
if element:
|
||||
self.click(element)
|
||||
|
||||
self.w3c_actions.key_action.key_down(value)
|
||||
self.w3c_actions.pointer_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def key_up(self, value: str, element: WebElement | None = None) -> ActionChains:
|
||||
"""Releases a modifier key.
|
||||
|
||||
:Args:
|
||||
- value: The modifier key to send. Values are defined in Keys class.
|
||||
- element: The element to send keys.
|
||||
If None, sends a key to current focused element.
|
||||
|
||||
Example, pressing ctrl+c::
|
||||
|
||||
ActionChains(driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
|
||||
"""
|
||||
if element:
|
||||
self.click(element)
|
||||
|
||||
self.w3c_actions.key_action.key_up(value)
|
||||
self.w3c_actions.pointer_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def move_by_offset(self, xoffset: int, yoffset: int) -> ActionChains:
|
||||
"""Moving the mouse to an offset from current mouse position.
|
||||
|
||||
:Args:
|
||||
- xoffset: X offset to move to, as a positive or negative integer.
|
||||
- yoffset: Y offset to move to, as a positive or negative integer.
|
||||
"""
|
||||
|
||||
self.w3c_actions.pointer_action.move_by(xoffset, yoffset)
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def move_to_element(self, to_element: WebElement) -> ActionChains:
|
||||
"""Moving the mouse to the middle of an element.
|
||||
|
||||
:Args:
|
||||
- to_element: The WebElement to move to.
|
||||
"""
|
||||
|
||||
self.w3c_actions.pointer_action.move_to(to_element)
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def move_to_element_with_offset(self, to_element: WebElement, xoffset: int, yoffset: int) -> ActionChains:
|
||||
"""Move the mouse by an offset of the specified element. Offsets are
|
||||
relative to the in-view center point of the element.
|
||||
|
||||
:Args:
|
||||
- to_element: The WebElement to move to.
|
||||
- xoffset: X offset to move to, as a positive or negative integer.
|
||||
- yoffset: Y offset to move to, as a positive or negative integer.
|
||||
"""
|
||||
|
||||
self.w3c_actions.pointer_action.move_to(to_element, int(xoffset), int(yoffset))
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def pause(self, seconds: float | int) -> ActionChains:
|
||||
"""Pause all inputs for the specified duration in seconds."""
|
||||
|
||||
self.w3c_actions.pointer_action.pause(seconds)
|
||||
self.w3c_actions.key_action.pause(seconds)
|
||||
|
||||
return self
|
||||
|
||||
def release(self, on_element: WebElement | None = None) -> ActionChains:
|
||||
"""Releasing a held mouse button on an element.
|
||||
|
||||
:Args:
|
||||
- on_element: The element to mouse up.
|
||||
If None, releases on current mouse position.
|
||||
"""
|
||||
if on_element:
|
||||
self.move_to_element(on_element)
|
||||
|
||||
self.w3c_actions.pointer_action.release()
|
||||
self.w3c_actions.key_action.pause()
|
||||
|
||||
return self
|
||||
|
||||
def send_keys(self, *keys_to_send: str) -> ActionChains:
|
||||
"""Sends keys to current focused element.
|
||||
|
||||
:Args:
|
||||
- keys_to_send: The keys to send. Modifier keys constants can be found in the
|
||||
'Keys' class.
|
||||
"""
|
||||
typing = keys_to_typing(keys_to_send)
|
||||
|
||||
for key in typing:
|
||||
self.key_down(key)
|
||||
self.key_up(key)
|
||||
|
||||
return self
|
||||
|
||||
def send_keys_to_element(self, element: WebElement, *keys_to_send: str) -> ActionChains:
|
||||
"""Sends keys to an element.
|
||||
|
||||
:Args:
|
||||
- element: The element to send keys.
|
||||
- keys_to_send: The keys to send. Modifier keys constants can be found in the
|
||||
'Keys' class.
|
||||
"""
|
||||
self.click(element)
|
||||
self.send_keys(*keys_to_send)
|
||||
return self
|
||||
|
||||
def scroll_to_element(self, element: WebElement) -> ActionChains:
|
||||
"""If the element is outside the viewport, scrolls the bottom of the
|
||||
element to the bottom of the viewport.
|
||||
|
||||
:Args:
|
||||
- element: Which element to scroll into the viewport.
|
||||
"""
|
||||
|
||||
self.w3c_actions.wheel_action.scroll(origin=element)
|
||||
return self
|
||||
|
||||
def scroll_by_amount(self, delta_x: int, delta_y: int) -> ActionChains:
|
||||
"""Scrolls by provided amounts with the origin in the top left corner
|
||||
of the viewport.
|
||||
|
||||
:Args:
|
||||
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
|
||||
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
|
||||
"""
|
||||
|
||||
self.w3c_actions.wheel_action.scroll(delta_x=delta_x, delta_y=delta_y)
|
||||
return self
|
||||
|
||||
def scroll_from_origin(self, scroll_origin: ScrollOrigin, delta_x: int, delta_y: int) -> ActionChains:
|
||||
"""Scrolls by provided amount based on a provided origin. The scroll
|
||||
origin is either the center of an element or the upper left of the
|
||||
viewport plus any offsets. If the origin is an element, and the element
|
||||
is not in the viewport, the bottom of the element will first be
|
||||
scrolled to the bottom of the viewport.
|
||||
|
||||
:Args:
|
||||
- origin: Where scroll originates (viewport or element center) plus provided offsets.
|
||||
- delta_x: Distance along X axis to scroll using the wheel. A negative value scrolls left.
|
||||
- delta_y: Distance along Y axis to scroll using the wheel. A negative value scrolls up.
|
||||
|
||||
:Raises: If the origin with offset is outside the viewport.
|
||||
- MoveTargetOutOfBoundsException - If the origin with offset is outside the viewport.
|
||||
"""
|
||||
|
||||
if not isinstance(scroll_origin, ScrollOrigin):
|
||||
raise TypeError(f"Expected object of type ScrollOrigin, got: {type(scroll_origin)}")
|
||||
|
||||
self.w3c_actions.wheel_action.scroll(
|
||||
origin=scroll_origin.origin,
|
||||
x=scroll_origin.x_offset,
|
||||
y=scroll_origin.y_offset,
|
||||
delta_x=delta_x,
|
||||
delta_y=delta_y,
|
||||
)
|
||||
return self
|
||||
|
||||
# Context manager so ActionChains can be used in a 'with .. as' statements.
|
||||
|
||||
def __enter__(self) -> ActionChains:
|
||||
return self # Return created instance of self.
|
||||
|
||||
def __exit__(self, _type, _value, _traceback) -> None:
|
||||
pass # Do nothing, does not require additional cleanup.
|
||||
@@ -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.
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)}
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -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,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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)})
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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.
|
||||
"""The Alert implementation."""
|
||||
|
||||
from selenium.webdriver.common.utils import keys_to_typing
|
||||
from selenium.webdriver.remote.command import Command
|
||||
|
||||
|
||||
class Alert:
|
||||
"""Allows to work with alerts.
|
||||
|
||||
Use this class to interact with alert prompts. It contains methods for dismissing,
|
||||
accepting, inputting, and getting text from alert prompts.
|
||||
|
||||
Accepting / Dismissing alert prompts::
|
||||
|
||||
Alert(driver).accept()
|
||||
Alert(driver).dismiss()
|
||||
|
||||
Inputting a value into an alert prompt::
|
||||
|
||||
name_prompt = Alert(driver)
|
||||
name_prompt.send_keys("Willian Shakesphere")
|
||||
name_prompt.accept()
|
||||
|
||||
|
||||
Reading a the text of a prompt for verification::
|
||||
|
||||
alert_text = Alert(driver).text
|
||||
self.assertEqual("Do you wish to quit?", alert_text)
|
||||
"""
|
||||
|
||||
def __init__(self, driver) -> None:
|
||||
"""Creates a new Alert.
|
||||
|
||||
:Args:
|
||||
- driver: The WebDriver instance which performs user actions.
|
||||
"""
|
||||
self.driver = driver
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
"""Gets the text of the Alert."""
|
||||
return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"]
|
||||
|
||||
def dismiss(self) -> None:
|
||||
"""Dismisses the alert available."""
|
||||
self.driver.execute(Command.W3C_DISMISS_ALERT)
|
||||
|
||||
def accept(self) -> None:
|
||||
"""Accepts the alert available.
|
||||
|
||||
:Usage:
|
||||
::
|
||||
|
||||
Alert(driver).accept() # Confirm a alert dialog.
|
||||
"""
|
||||
self.driver.execute(Command.W3C_ACCEPT_ALERT)
|
||||
|
||||
def send_keys(self, keysToSend: str) -> None:
|
||||
"""Send Keys to the Alert.
|
||||
|
||||
:Args:
|
||||
- keysToSend: The text to be sent to Alert.
|
||||
"""
|
||||
self.driver.execute(Command.W3C_SET_ALERT_VALUE, {"value": keys_to_typing(keysToSend), "text": keysToSend})
|
||||
@@ -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.
|
||||
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,509 @@
|
||||
# The MIT License(MIT)
|
||||
#
|
||||
# Copyright(c) 2018 Hyperion Gray
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# This code comes from https://github.com/HyperionGray/trio-chrome-devtools-protocol/tree/master/trio_cdp
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
import contextvars
|
||||
import importlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import pathlib
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
import trio
|
||||
from trio_websocket import ConnectionClosed as WsConnectionClosed
|
||||
from trio_websocket import connect_websocket_url
|
||||
|
||||
logger = logging.getLogger("trio_cdp")
|
||||
T = typing.TypeVar("T")
|
||||
MAX_WS_MESSAGE_SIZE = 2**24
|
||||
|
||||
devtools = None
|
||||
version = None
|
||||
|
||||
|
||||
def import_devtools(ver):
|
||||
"""Attempt to load the current latest available devtools into the module
|
||||
cache for use later."""
|
||||
global devtools
|
||||
global version
|
||||
version = ver
|
||||
base = "selenium.webdriver.common.devtools.v"
|
||||
try:
|
||||
devtools = importlib.import_module(f"{base}{ver}")
|
||||
return devtools
|
||||
except ModuleNotFoundError:
|
||||
# Attempt to parse and load the 'most recent' devtools module. This is likely
|
||||
# because cdp has been updated but selenium python has not been released yet.
|
||||
devtools_path = pathlib.Path(__file__).parents[1].joinpath("devtools")
|
||||
versions = tuple(f.name for f in devtools_path.iterdir() if f.is_dir())
|
||||
latest = max(int(x[1:]) for x in versions)
|
||||
selenium_logger = logging.getLogger(__name__)
|
||||
selenium_logger.debug("Falling back to loading `devtools`: v%s", latest)
|
||||
devtools = importlib.import_module(f"{base}{latest}")
|
||||
return devtools
|
||||
|
||||
|
||||
_connection_context: contextvars.ContextVar = contextvars.ContextVar("connection_context")
|
||||
_session_context: contextvars.ContextVar = contextvars.ContextVar("session_context")
|
||||
|
||||
|
||||
def get_connection_context(fn_name):
|
||||
"""Look up the current connection.
|
||||
|
||||
If there is no current connection, raise a ``RuntimeError`` with a
|
||||
helpful message.
|
||||
"""
|
||||
try:
|
||||
return _connection_context.get()
|
||||
except LookupError:
|
||||
raise RuntimeError(f"{fn_name}() must be called in a connection context.")
|
||||
|
||||
|
||||
def get_session_context(fn_name):
|
||||
"""Look up the current session.
|
||||
|
||||
If there is no current session, raise a ``RuntimeError`` with a
|
||||
helpful message.
|
||||
"""
|
||||
try:
|
||||
return _session_context.get()
|
||||
except LookupError:
|
||||
raise RuntimeError(f"{fn_name}() must be called in a session context.")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def connection_context(connection):
|
||||
"""This context manager installs ``connection`` as the session context for
|
||||
the current Trio task."""
|
||||
token = _connection_context.set(connection)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_connection_context.reset(token)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def session_context(session):
|
||||
"""This context manager installs ``session`` as the session context for the
|
||||
current Trio task."""
|
||||
token = _session_context.set(session)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_session_context.reset(token)
|
||||
|
||||
|
||||
def set_global_connection(connection):
|
||||
"""Install ``connection`` in the root context so that it will become the
|
||||
default connection for all tasks.
|
||||
|
||||
This is generally not recommended, except it may be necessary in
|
||||
certain use cases such as running inside Jupyter notebook.
|
||||
"""
|
||||
global _connection_context
|
||||
_connection_context = contextvars.ContextVar("_connection_context", default=connection)
|
||||
|
||||
|
||||
def set_global_session(session):
|
||||
"""Install ``session`` in the root context so that it will become the
|
||||
default session for all tasks.
|
||||
|
||||
This is generally not recommended, except it may be necessary in
|
||||
certain use cases such as running inside Jupyter notebook.
|
||||
"""
|
||||
global _session_context
|
||||
_session_context = contextvars.ContextVar("_session_context", default=session)
|
||||
|
||||
|
||||
class BrowserError(Exception):
|
||||
"""This exception is raised when the browser's response to a command
|
||||
indicates that an error occurred."""
|
||||
|
||||
def __init__(self, obj):
|
||||
self.code = obj.get("code")
|
||||
self.message = obj.get("message")
|
||||
self.detail = obj.get("data")
|
||||
|
||||
def __str__(self):
|
||||
return f"BrowserError<code={self.code} message={self.message}> {self.detail}"
|
||||
|
||||
|
||||
class CdpConnectionClosed(WsConnectionClosed):
|
||||
"""Raised when a public method is called on a closed CDP connection."""
|
||||
|
||||
def __init__(self, reason):
|
||||
"""Constructor.
|
||||
|
||||
:param reason:
|
||||
:type reason: wsproto.frame_protocol.CloseReason
|
||||
"""
|
||||
self.reason = reason
|
||||
|
||||
def __repr__(self):
|
||||
"""Return representation."""
|
||||
return f"{self.__class__.__name__}<{self.reason}>"
|
||||
|
||||
|
||||
class InternalError(Exception):
|
||||
"""This exception is only raised when there is faulty logic in TrioCDP or
|
||||
the integration with PyCDP."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CmEventProxy:
|
||||
"""A proxy object returned by :meth:`CdpBase.wait_for()``.
|
||||
|
||||
After the context manager executes, this proxy object will have a
|
||||
value set that contains the returned event.
|
||||
"""
|
||||
|
||||
value: typing.Any = None
|
||||
|
||||
|
||||
class CdpBase:
|
||||
def __init__(self, ws, session_id, target_id):
|
||||
self.ws = ws
|
||||
self.session_id = session_id
|
||||
self.target_id = target_id
|
||||
self.channels = defaultdict(set)
|
||||
self.id_iter = itertools.count()
|
||||
self.inflight_cmd = {}
|
||||
self.inflight_result = {}
|
||||
|
||||
async def execute(self, cmd: typing.Generator[dict, T, typing.Any]) -> T:
|
||||
"""Execute a command on the server and wait for the result.
|
||||
|
||||
:param cmd: any CDP command
|
||||
:returns: a CDP result
|
||||
"""
|
||||
cmd_id = next(self.id_iter)
|
||||
cmd_event = trio.Event()
|
||||
self.inflight_cmd[cmd_id] = cmd, cmd_event
|
||||
request = next(cmd)
|
||||
request["id"] = cmd_id
|
||||
if self.session_id:
|
||||
request["sessionId"] = self.session_id
|
||||
request_str = json.dumps(request)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(f"Sending CDP message: {cmd_id} {cmd_event}: {request_str}")
|
||||
try:
|
||||
await self.ws.send_message(request_str)
|
||||
except WsConnectionClosed as wcc:
|
||||
raise CdpConnectionClosed(wcc.reason) from None
|
||||
await cmd_event.wait()
|
||||
response = self.inflight_result.pop(cmd_id)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(f"Received CDP message: {response}")
|
||||
if isinstance(response, Exception):
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(f"Exception raised by {cmd_event} message: {type(response).__name__}")
|
||||
raise response
|
||||
return response
|
||||
|
||||
def listen(self, *event_types, buffer_size=10):
|
||||
"""Return an async iterator that iterates over events matching the
|
||||
indicated types."""
|
||||
sender, receiver = trio.open_memory_channel(buffer_size)
|
||||
for event_type in event_types:
|
||||
self.channels[event_type].add(sender)
|
||||
return receiver
|
||||
|
||||
@asynccontextmanager
|
||||
async def wait_for(self, event_type: typing.Type[T], buffer_size=10) -> typing.AsyncGenerator[CmEventProxy, None]:
|
||||
"""Wait for an event of the given type and return it.
|
||||
|
||||
This is an async context manager, so you should open it inside
|
||||
an async with block. The block will not exit until the indicated
|
||||
event is received.
|
||||
"""
|
||||
sender: trio.MemorySendChannel
|
||||
receiver: trio.MemoryReceiveChannel
|
||||
sender, receiver = trio.open_memory_channel(buffer_size)
|
||||
self.channels[event_type].add(sender)
|
||||
proxy = CmEventProxy()
|
||||
yield proxy
|
||||
async with receiver:
|
||||
event = await receiver.receive()
|
||||
proxy.value = event
|
||||
|
||||
def _handle_data(self, data):
|
||||
"""Handle incoming WebSocket data.
|
||||
|
||||
:param dict data: a JSON dictionary
|
||||
"""
|
||||
if "id" in data:
|
||||
self._handle_cmd_response(data)
|
||||
else:
|
||||
self._handle_event(data)
|
||||
|
||||
def _handle_cmd_response(self, data):
|
||||
"""Handle a response to a command. This will set an event flag that
|
||||
will return control to the task that called the command.
|
||||
|
||||
:param dict data: response as a JSON dictionary
|
||||
"""
|
||||
cmd_id = data["id"]
|
||||
try:
|
||||
cmd, event = self.inflight_cmd.pop(cmd_id)
|
||||
except KeyError:
|
||||
logger.warning("Got a message with a command ID that does not exist: %s", data)
|
||||
return
|
||||
if "error" in data:
|
||||
# If the server reported an error, convert it to an exception and do
|
||||
# not process the response any further.
|
||||
self.inflight_result[cmd_id] = BrowserError(data["error"])
|
||||
else:
|
||||
# Otherwise, continue the generator to parse the JSON result
|
||||
# into a CDP object.
|
||||
try:
|
||||
_ = cmd.send(data["result"])
|
||||
raise InternalError("The command's generator function did not exit when expected!")
|
||||
except StopIteration as exit:
|
||||
return_ = exit.value
|
||||
self.inflight_result[cmd_id] = return_
|
||||
event.set()
|
||||
|
||||
def _handle_event(self, data):
|
||||
"""Handle an event.
|
||||
|
||||
:param dict data: event as a JSON dictionary
|
||||
"""
|
||||
global devtools
|
||||
event = devtools.util.parse_json_event(data)
|
||||
logger.debug("Received event: %s", event)
|
||||
to_remove = set()
|
||||
for sender in self.channels[type(event)]:
|
||||
try:
|
||||
sender.send_nowait(event)
|
||||
except trio.WouldBlock:
|
||||
logger.error('Unable to send event "%r" due to full channel %s', event, sender)
|
||||
except trio.BrokenResourceError:
|
||||
to_remove.add(sender)
|
||||
if to_remove:
|
||||
self.channels[type(event)] -= to_remove
|
||||
|
||||
|
||||
class CdpSession(CdpBase):
|
||||
"""Contains the state for a CDP session.
|
||||
|
||||
Generally you should not instantiate this object yourself; you should call
|
||||
:meth:`CdpConnection.open_session`.
|
||||
"""
|
||||
|
||||
def __init__(self, ws, session_id, target_id):
|
||||
"""Constructor.
|
||||
|
||||
:param trio_websocket.WebSocketConnection ws:
|
||||
:param devtools.target.SessionID session_id:
|
||||
:param devtools.target.TargetID target_id:
|
||||
"""
|
||||
super().__init__(ws, session_id, target_id)
|
||||
|
||||
self._dom_enable_count = 0
|
||||
self._dom_enable_lock = trio.Lock()
|
||||
self._page_enable_count = 0
|
||||
self._page_enable_lock = trio.Lock()
|
||||
|
||||
@asynccontextmanager
|
||||
async def dom_enable(self):
|
||||
"""A context manager that executes ``dom.enable()`` when it enters and
|
||||
then calls ``dom.disable()``.
|
||||
|
||||
This keeps track of concurrent callers and only disables DOM
|
||||
events when all callers have exited.
|
||||
"""
|
||||
global devtools
|
||||
async with self._dom_enable_lock:
|
||||
self._dom_enable_count += 1
|
||||
if self._dom_enable_count == 1:
|
||||
await self.execute(devtools.dom.enable())
|
||||
|
||||
yield
|
||||
|
||||
async with self._dom_enable_lock:
|
||||
self._dom_enable_count -= 1
|
||||
if self._dom_enable_count == 0:
|
||||
await self.execute(devtools.dom.disable())
|
||||
|
||||
@asynccontextmanager
|
||||
async def page_enable(self):
|
||||
"""A context manager that executes ``page.enable()`` when it enters and
|
||||
then calls ``page.disable()`` when it exits.
|
||||
|
||||
This keeps track of concurrent callers and only disables page
|
||||
events when all callers have exited.
|
||||
"""
|
||||
global devtools
|
||||
async with self._page_enable_lock:
|
||||
self._page_enable_count += 1
|
||||
if self._page_enable_count == 1:
|
||||
await self.execute(devtools.page.enable())
|
||||
|
||||
yield
|
||||
|
||||
async with self._page_enable_lock:
|
||||
self._page_enable_count -= 1
|
||||
if self._page_enable_count == 0:
|
||||
await self.execute(devtools.page.disable())
|
||||
|
||||
|
||||
class CdpConnection(CdpBase, trio.abc.AsyncResource):
|
||||
"""Contains the connection state for a Chrome DevTools Protocol server.
|
||||
|
||||
CDP can multiplex multiple "sessions" over a single connection. This
|
||||
class corresponds to the "root" session, i.e. the implicitly created
|
||||
session that has no session ID. This class is responsible for
|
||||
reading incoming WebSocket messages and forwarding them to the
|
||||
corresponding session, as well as handling messages targeted at the
|
||||
root session itself. You should generally call the
|
||||
:func:`open_cdp()` instead of instantiating this class directly.
|
||||
"""
|
||||
|
||||
def __init__(self, ws):
|
||||
"""Constructor.
|
||||
|
||||
:param trio_websocket.WebSocketConnection ws:
|
||||
"""
|
||||
super().__init__(ws, session_id=None, target_id=None)
|
||||
self.sessions = {}
|
||||
|
||||
async def aclose(self):
|
||||
"""Close the underlying WebSocket connection.
|
||||
|
||||
This will cause the reader task to gracefully exit when it tries
|
||||
to read the next message from the WebSocket. All of the public
|
||||
APIs (``execute()``, ``listen()``, etc.) will raise
|
||||
``CdpConnectionClosed`` after the CDP connection is closed. It
|
||||
is safe to call this multiple times.
|
||||
"""
|
||||
await self.ws.aclose()
|
||||
|
||||
@asynccontextmanager
|
||||
async def open_session(self, target_id) -> typing.AsyncIterator[CdpSession]:
|
||||
"""This context manager opens a session and enables the "simple" style
|
||||
of calling CDP APIs.
|
||||
|
||||
For example, inside a session context, you can call ``await
|
||||
dom.get_document()`` and it will execute on the current session
|
||||
automatically.
|
||||
"""
|
||||
session = await self.connect_session(target_id)
|
||||
with session_context(session):
|
||||
yield session
|
||||
|
||||
async def connect_session(self, target_id) -> "CdpSession":
|
||||
"""Returns a new :class:`CdpSession` connected to the specified
|
||||
target."""
|
||||
global devtools
|
||||
session_id = await self.execute(devtools.target.attach_to_target(target_id, True))
|
||||
session = CdpSession(self.ws, session_id, target_id)
|
||||
self.sessions[session_id] = session
|
||||
return session
|
||||
|
||||
async def _reader_task(self):
|
||||
"""Runs in the background and handles incoming messages: dispatching
|
||||
responses to commands and events to listeners."""
|
||||
global devtools
|
||||
while True:
|
||||
try:
|
||||
message = await self.ws.get_message()
|
||||
except WsConnectionClosed:
|
||||
# If the WebSocket is closed, we don't want to throw an
|
||||
# exception from the reader task. Instead we will throw
|
||||
# exceptions from the public API methods, and we can quietly
|
||||
# exit the reader task here.
|
||||
break
|
||||
try:
|
||||
data = json.loads(message)
|
||||
except json.JSONDecodeError:
|
||||
raise BrowserError({"code": -32700, "message": "Client received invalid JSON", "data": message})
|
||||
logger.debug("Received message %r", data)
|
||||
if "sessionId" in data:
|
||||
session_id = devtools.target.SessionID(data["sessionId"])
|
||||
try:
|
||||
session = self.sessions[session_id]
|
||||
except KeyError:
|
||||
raise BrowserError(
|
||||
{
|
||||
"code": -32700,
|
||||
"message": "Browser sent a message for an invalid session",
|
||||
"data": f"{session_id!r}",
|
||||
}
|
||||
)
|
||||
session._handle_data(data)
|
||||
else:
|
||||
self._handle_data(data)
|
||||
|
||||
for _, session in self.sessions.items():
|
||||
for _, senders in session.channels.items():
|
||||
for sender in senders:
|
||||
sender.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def open_cdp(url) -> typing.AsyncIterator[CdpConnection]:
|
||||
"""This async context manager opens a connection to the browser specified
|
||||
by ``url`` before entering the block, then closes the connection when the
|
||||
block exits.
|
||||
|
||||
The context manager also sets the connection as the default
|
||||
connection for the current task, so that commands like ``await
|
||||
target.get_targets()`` will run on this connection automatically. If
|
||||
you want to use multiple connections concurrently, it is recommended
|
||||
to open each on in a separate task.
|
||||
"""
|
||||
|
||||
async with trio.open_nursery() as nursery:
|
||||
conn = await connect_cdp(nursery, url)
|
||||
try:
|
||||
with connection_context(conn):
|
||||
yield conn
|
||||
finally:
|
||||
await conn.aclose()
|
||||
|
||||
|
||||
async def connect_cdp(nursery, url) -> CdpConnection:
|
||||
"""Connect to the browser specified by ``url`` and spawn a background task
|
||||
in the specified nursery.
|
||||
|
||||
The ``open_cdp()`` context manager is preferred in most situations.
|
||||
You should only use this function if you need to specify a custom
|
||||
nursery. This connection is not automatically closed! You can either
|
||||
use the connection object as a context manager (``async with
|
||||
conn:``) or else call ``await conn.aclose()`` on it when you are
|
||||
done with it. If ``set_context`` is True, then the returned
|
||||
connection will be installed as the default connection for the
|
||||
current task. This argument is for unusual use cases, such as
|
||||
running inside of a notebook.
|
||||
"""
|
||||
ws = await connect_websocket_url(nursery, url, max_message_size=MAX_WS_MESSAGE_SIZE)
|
||||
cdp_conn = CdpConnection(ws)
|
||||
nursery.start_soon(cdp_conn._reader_task)
|
||||
return cdp_conn
|
||||
@@ -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.
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Console(Enum):
|
||||
ALL = "all"
|
||||
LOG = "log"
|
||||
ERROR = "error"
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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 dataclasses import dataclass
|
||||
|
||||
from .session import session_subscribe
|
||||
from .session import session_unsubscribe
|
||||
|
||||
|
||||
class Script:
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.log_entry_subscribed = False
|
||||
|
||||
def add_console_message_handler(self, handler):
|
||||
self._subscribe_to_log_entries()
|
||||
return self.conn.add_callback(LogEntryAdded, self._handle_log_entry("console", handler))
|
||||
|
||||
def add_javascript_error_handler(self, handler):
|
||||
self._subscribe_to_log_entries()
|
||||
return self.conn.add_callback(LogEntryAdded, self._handle_log_entry("javascript", handler))
|
||||
|
||||
def remove_console_message_handler(self, id):
|
||||
self.conn.remove_callback(LogEntryAdded, id)
|
||||
self._unsubscribe_from_log_entries()
|
||||
|
||||
remove_javascript_error_handler = remove_console_message_handler
|
||||
|
||||
def _subscribe_to_log_entries(self):
|
||||
if not self.log_entry_subscribed:
|
||||
self.conn.execute(session_subscribe(LogEntryAdded.event_class))
|
||||
self.log_entry_subscribed = True
|
||||
|
||||
def _unsubscribe_from_log_entries(self):
|
||||
if self.log_entry_subscribed and LogEntryAdded.event_class not in self.conn.callbacks:
|
||||
self.conn.execute(session_unsubscribe(LogEntryAdded.event_class))
|
||||
self.log_entry_subscribed = False
|
||||
|
||||
def _handle_log_entry(self, type, handler):
|
||||
def _handle_log_entry(log_entry):
|
||||
if log_entry.type_ == type:
|
||||
handler(log_entry)
|
||||
|
||||
return _handle_log_entry
|
||||
|
||||
|
||||
class LogEntryAdded:
|
||||
event_class = "log.entryAdded"
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json):
|
||||
if json["type"] == "console":
|
||||
return ConsoleLogEntry.from_json(json)
|
||||
elif json["type"] == "javascript":
|
||||
return JavaScriptLogEntry.from_json(json)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConsoleLogEntry:
|
||||
level: str
|
||||
text: str
|
||||
timestamp: str
|
||||
method: str
|
||||
args: typing.List[dict]
|
||||
type_: str
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json):
|
||||
return cls(
|
||||
level=json["level"],
|
||||
text=json["text"],
|
||||
timestamp=json["timestamp"],
|
||||
method=json["method"],
|
||||
args=json["args"],
|
||||
type_=json["type"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class JavaScriptLogEntry:
|
||||
level: str
|
||||
text: str
|
||||
timestamp: str
|
||||
stacktrace: dict
|
||||
type_: str
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json):
|
||||
return cls(
|
||||
level=json["level"],
|
||||
text=json["text"],
|
||||
timestamp=json["timestamp"],
|
||||
stacktrace=json["stackTrace"],
|
||||
type_=json["type"],
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
|
||||
def session_subscribe(*events, browsing_contexts=None):
|
||||
cmd_dict = {
|
||||
"method": "session.subscribe",
|
||||
"params": {
|
||||
"events": events,
|
||||
},
|
||||
}
|
||||
if browsing_contexts is None:
|
||||
browsing_contexts = []
|
||||
if browsing_contexts:
|
||||
cmd_dict["params"]["browsingContexts"] = browsing_contexts
|
||||
_ = yield cmd_dict
|
||||
return None
|
||||
|
||||
|
||||
def session_unsubscribe(*events, browsing_contexts=None):
|
||||
cmd_dict = {
|
||||
"method": "session.unsubscribe",
|
||||
"params": {
|
||||
"events": events,
|
||||
},
|
||||
}
|
||||
if browsing_contexts is None:
|
||||
browsing_contexts = []
|
||||
if browsing_contexts:
|
||||
cmd_dict["params"]["browsingContexts"] = browsing_contexts
|
||||
_ = yield cmd_dict
|
||||
return None
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
"""The By implementation."""
|
||||
|
||||
from typing import Dict
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class By:
|
||||
"""Set of supported locator strategies."""
|
||||
|
||||
ID = "id"
|
||||
XPATH = "xpath"
|
||||
LINK_TEXT = "link text"
|
||||
PARTIAL_LINK_TEXT = "partial link text"
|
||||
NAME = "name"
|
||||
TAG_NAME = "tag name"
|
||||
CLASS_NAME = "class name"
|
||||
CSS_SELECTOR = "css selector"
|
||||
|
||||
_custom_finders: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def register_custom_finder(cls, name: str, strategy: str) -> None:
|
||||
cls._custom_finders[name] = strategy
|
||||
|
||||
@classmethod
|
||||
def get_finder(cls, name: str) -> Optional[str]:
|
||||
return cls._custom_finders.get(name) or getattr(cls, name.upper(), None)
|
||||
|
||||
@classmethod
|
||||
def clear_custom_finders(cls) -> None:
|
||||
cls._custom_finders.clear()
|
||||
|
||||
|
||||
ByType = Literal["id", "xpath", "link text", "partial link text", "name", "tag name", "class name", "css selector"]
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
"""The Desired Capabilities implementation."""
|
||||
|
||||
|
||||
class DesiredCapabilities:
|
||||
"""Set of default supported desired capabilities.
|
||||
|
||||
Use this as a starting point for creating a desired capabilities object for
|
||||
requesting remote webdrivers for connecting to selenium server or selenium grid.
|
||||
|
||||
Usage Example::
|
||||
|
||||
from selenium import webdriver
|
||||
|
||||
selenium_grid_url = "http://198.0.0.1:4444/wd/hub"
|
||||
|
||||
# Create a desired capabilities object as a starting point.
|
||||
capabilities = DesiredCapabilities.FIREFOX.copy()
|
||||
capabilities['platform'] = "WINDOWS"
|
||||
capabilities['version'] = "10"
|
||||
|
||||
# Instantiate an instance of Remote WebDriver with the desired capabilities.
|
||||
driver = webdriver.Remote(desired_capabilities=capabilities,
|
||||
command_executor=selenium_grid_url)
|
||||
|
||||
Note: Always use '.copy()' on the DesiredCapabilities object to avoid the side
|
||||
effects of altering the Global class instance.
|
||||
"""
|
||||
|
||||
FIREFOX = {
|
||||
"browserName": "firefox",
|
||||
"acceptInsecureCerts": True,
|
||||
"moz:debuggerAddress": True,
|
||||
}
|
||||
|
||||
INTERNETEXPLORER = {
|
||||
"browserName": "internet explorer",
|
||||
"platformName": "windows",
|
||||
}
|
||||
|
||||
EDGE = {
|
||||
"browserName": "MicrosoftEdge",
|
||||
}
|
||||
|
||||
CHROME = {
|
||||
"browserName": "chrome",
|
||||
}
|
||||
|
||||
SAFARI = {
|
||||
"browserName": "safari",
|
||||
"platformName": "mac",
|
||||
}
|
||||
|
||||
HTMLUNIT = {
|
||||
"browserName": "htmlunit",
|
||||
"version": "",
|
||||
"platform": "ANY",
|
||||
}
|
||||
|
||||
HTMLUNITWITHJS = {
|
||||
"browserName": "htmlunit",
|
||||
"version": "firefox",
|
||||
"platform": "ANY",
|
||||
"javascriptEnabled": True,
|
||||
}
|
||||
|
||||
IPHONE = {
|
||||
"browserName": "iPhone",
|
||||
"version": "",
|
||||
"platform": "mac",
|
||||
}
|
||||
|
||||
IPAD = {
|
||||
"browserName": "iPad",
|
||||
"version": "",
|
||||
"platform": "mac",
|
||||
}
|
||||
|
||||
WEBKITGTK = {
|
||||
"browserName": "MiniBrowser",
|
||||
}
|
||||
|
||||
WPEWEBKIT = {
|
||||
"browserName": "MiniBrowser",
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# DO NOT EDIT THIS FILE!
|
||||
#
|
||||
# This file is generated from the CDP specification. If you need to make
|
||||
# changes, edit the generator and regenerate all of the modules.
|
||||
from . import accessibility
|
||||
from . import animation
|
||||
from . import audits
|
||||
from . import autofill
|
||||
from . import background_service
|
||||
from . import bluetooth_emulation
|
||||
from . import browser
|
||||
from . import css
|
||||
from . import cache_storage
|
||||
from . import cast
|
||||
from . import console
|
||||
from . import dom
|
||||
from . import dom_debugger
|
||||
from . import dom_snapshot
|
||||
from . import dom_storage
|
||||
from . import database
|
||||
from . import debugger
|
||||
from . import device_access
|
||||
from . import device_orientation
|
||||
from . import emulation
|
||||
from . import event_breakpoints
|
||||
from . import extensions
|
||||
from . import fed_cm
|
||||
from . import fetch
|
||||
from . import file_system
|
||||
from . import headless_experimental
|
||||
from . import heap_profiler
|
||||
from . import io
|
||||
from . import indexed_db
|
||||
from . import input_
|
||||
from . import inspector
|
||||
from . import layer_tree
|
||||
from . import log
|
||||
from . import media
|
||||
from . import memory
|
||||
from . import network
|
||||
from . import overlay
|
||||
from . import pwa
|
||||
from . import page
|
||||
from . import performance
|
||||
from . import performance_timeline
|
||||
from . import preload
|
||||
from . import profiler
|
||||
from . import runtime
|
||||
from . import schema
|
||||
from . import security
|
||||
from . import service_worker
|
||||
from . import storage
|
||||
from . import system_info
|
||||
from . import target
|
||||
from . import tethering
|
||||
from . import tracing
|
||||
from . import web_audio
|
||||
from . import web_authn
|
||||
from . import util
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Loaded 100 of 2817 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user