moving to scripts

This commit is contained in:
eneller
2021-11-16 23:55:48 +01:00
parent f591ca2077
commit 14bfb7f96f
2575 changed files with 465862 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
# 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 .firefox.webdriver import WebDriver as Firefox # noqa
from .firefox.firefox_profile import FirefoxProfile # noqa
from .firefox.options import Options as FirefoxOptions # noqa
from .chrome.webdriver import WebDriver as Chrome # noqa
from .chrome.options import Options as ChromeOptions # noqa
from .ie.webdriver import WebDriver as Ie # noqa
from .ie.options import Options as IeOptions # noqa
from .edge.webdriver import WebDriver as Edge # noqa
from .edge.webdriver import WebDriver as ChromiumEdge # noqa
from .edge.options import Options as EdgeOptions # noqa
from .opera.webdriver import WebDriver as Opera # noqa
from .safari.webdriver import WebDriver as Safari # noqa
from .webkitgtk.webdriver import WebDriver as WebKitGTK # noqa
from .webkitgtk.options import Options as WebKitGTKOptions # noqa
from .wpewebkit.webdriver import WebDriver as WPEWebKit # noqa
from .wpewebkit.options import Options as WPEWebKitOptions # noqa
from .remote.webdriver import WebDriver as Remote # noqa
from .common.desired_capabilities import DesiredCapabilities # noqa
from .common.action_chains import ActionChains # noqa
from .common.touch_actions import TouchActions # noqa
from .common.proxy import Proxy # noqa
from .common.keys import Keys # noqa
__version__ = '4.0.0'

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,29 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.chromium.options import ChromiumOptions
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
class Options(ChromiumOptions):
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.CHROME.copy()
def enable_mobile(self, android_package="com.android.chrome", android_activity=None, device_serial=None):
super().enable_mobile(android_package, android_activity, device_serial)

View File

@@ -0,0 +1,44 @@
# 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 selenium.webdriver.chromium import service
class Service(service.ChromiumService):
"""
Object that manages the starting and stopping of the ChromeDriver
"""
def __init__(self, executable_path: str, port: int = 0, service_args: List[str] = None,
log_path: str = None, env: str = None):
"""
Creates a new instance of the Service
:Args:
- executable_path : Path to the ChromeDriver
- port : Port the service is running on
- service_args : List of args to pass to the chromedriver service
- log_path : Path for the chromedriver service to log to"""
super(Service, self).__init__(
executable_path,
port,
service_args,
log_path,
env,
"Please see https://chromedriver.chromium.org/home")

View File

@@ -0,0 +1,72 @@
# 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 warnings
from selenium.webdriver.chromium.webdriver import ChromiumDriver
from .options import Options
from .service import Service
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
DEFAULT_PORT = 0
DEFAULT_SERVICE_LOG_PATH = None
DEFAULT_KEEP_ALIVE = None
class WebDriver(ChromiumDriver):
"""
Controls the ChromeDriver and allows you to drive the browser.
You will need to download the ChromeDriver executable from
http://chromedriver.storage.googleapis.com/index.html
"""
def __init__(self, executable_path="chromedriver", port=DEFAULT_PORT,
options: Options = None, service_args=None,
desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
chrome_options=None, service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
"""
Creates a new instance of the chrome driver.
Starts the service and then creates new instance of chrome driver.
:Args:
- executable_path - Deprecated: path to the executable. If the default is used it assumes the executable is in the $PATH
- port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
- options - this takes an instance of ChromeOptions
- service_args - Deprecated: List of args to pass to the driver service
- desired_capabilities - Deprecated: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- service_log_path - Deprecated: Where to log information from the driver.
- keep_alive - Deprecated: Whether to configure ChromeRemoteConnection to use HTTP keep-alive.
"""
if executable_path != 'chromedriver':
warnings.warn('executable_path has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if chrome_options:
warnings.warn('use options instead of chrome_options',
DeprecationWarning, stacklevel=2)
options = chrome_options
if keep_alive != DEFAULT_KEEP_ALIVE:
warnings.warn('keep_alive has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
else:
keep_alive = True
if not service:
service = Service(executable_path, port, service_args, service_log_path)
super(WebDriver, self).__init__(DesiredCapabilities.CHROME['browserName'], "goog",
port, options,
service_args, desired_capabilities,
service_log_path, service, keep_alive)

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,176 @@
# 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 base64
import os
from typing import List, NoReturn, Union
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.options import ArgOptions
class ChromiumOptions(ArgOptions):
KEY = "goog:chromeOptions"
def __init__(self):
super(ChromiumOptions, self).__init__()
self._binary_location = ''
self._extension_files = []
self._extensions = []
self._experimental_options = {}
self._debugger_address = None
@property
def binary_location(self) -> str:
"""
:Returns: The location of the binary, otherwise an empty string
"""
return self._binary_location
@binary_location.setter
def binary_location(self, value: str):
"""
Allows you to set where the chromium binary lives
:Args:
- value: path to the Chromium binary
"""
self._binary_location = value
@property
def debugger_address(self: str):
"""
:Returns: The address of the remote devtools instance
"""
return self._debugger_address
@debugger_address.setter
def debugger_address(self, value: str):
"""
Allows you to set the address of the remote devtools instance
that the ChromeDriver instance will try to connect to during an
active wait.
:Args:
- value: address of remote devtools instance if any (hostname[:port])
"""
self._debugger_address = value
@property
def extensions(self) -> List[str]:
"""
:Returns: A list of encoded extensions that will be loaded
"""
encoded_extensions = []
for ext in self._extension_files:
file_ = open(ext, 'rb')
# Should not use base64.encodestring() which inserts newlines every
# 76 characters (per RFC 1521). Chromedriver has to remove those
# unnecessary newlines before decoding, causing performance hit.
encoded_extensions.append(base64.b64encode(file_.read()).decode('UTF-8'))
file_.close()
return encoded_extensions + self._extensions
def add_extension(self, extension: str) -> NoReturn:
"""
Adds the path to the extension to a list that will be used to extract it
to the ChromeDriver
:Args:
- extension: path to the \\*.crx file
"""
if extension:
extension_to_add = os.path.abspath(os.path.expanduser(extension))
if os.path.exists(extension_to_add):
self._extension_files.append(extension_to_add)
else:
raise IOError("Path to the extension doesn't exist")
else:
raise ValueError("argument can not be null")
def add_encoded_extension(self, extension: str) -> NoReturn:
"""
Adds Base64 encoded string with extension data to a list that will be used to extract it
to the ChromeDriver
:Args:
- extension: Base64 encoded string with extension data
"""
if extension:
self._extensions.append(extension)
else:
raise ValueError("argument can not be null")
@property
def experimental_options(self) -> dict:
"""
:Returns: A dictionary of experimental options for chromium
"""
return self._experimental_options
def add_experimental_option(self, name: str, value: Union[str, int, dict, List[str]]):
"""
Adds an experimental option which is passed to chromium.
:Args:
name: The experimental option name.
value: The option value.
"""
self._experimental_options[name] = value
@property
def headless(self) -> bool:
"""
:Returns: True if the headless argument is set, else False
"""
return '--headless' in self._arguments
@headless.setter
def headless(self, value: bool):
"""
Sets the headless argument
:Args:
value: boolean value indicating to set the headless option
"""
args = {'--headless'}
if value is True:
self._arguments.extend(args)
else:
self._arguments = list(set(self._arguments) - args)
def to_capabilities(self) -> dict:
"""
Creates a capabilities with all the options that have been set
:Returns: A dictionary with everything
"""
caps = self._caps
chrome_options = self.experimental_options.copy()
if self.mobile_options:
chrome_options.update(self.mobile_options)
chrome_options["extensions"] = self.extensions
if self.binary_location:
chrome_options["binary"] = self.binary_location
chrome_options["args"] = self._arguments
if self.debugger_address:
chrome_options["debuggerAddress"] = self.debugger_address
caps[self.KEY] = chrome_options
return caps
@property
def default_capabilities(self) -> dict:
return DesiredCapabilities.CHROME.copy()

View File

@@ -0,0 +1,35 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.remote.remote_connection import RemoteConnection
class ChromiumRemoteConnection(RemoteConnection):
def __init__(self, remote_server_addr, vendor_prefix, browser_name, keep_alive=True, ignore_proxy=False):
RemoteConnection.__init__(self, remote_server_addr, keep_alive, ignore_proxy=ignore_proxy)
self.browser_name = browser_name
self._commands["launchApp"] = ('POST', '/session/$sessionId/chromium/launch_app')
self._commands["setPermissions"] = ('POST', '/session/$sessionId/permissions')
self._commands["setNetworkConditions"] = ('POST', '/session/$sessionId/chromium/network_conditions')
self._commands["getNetworkConditions"] = ('GET', '/session/$sessionId/chromium/network_conditions')
self._commands["deleteNetworkConditions"] = ('DELETE', '/session/$sessionId/chromium/network_conditions')
self._commands['executeCdpCommand'] = ('POST', '/session/$sessionId/{}/cdp/execute'.format(vendor_prefix))
self._commands['getSinks'] = ('GET', '/session/$sessionId/{}/cast/get_sinks'.format(vendor_prefix))
self._commands['getIssueMessage'] = ('GET', '/session/$sessionId/{}/cast/get_issue_message'.format(vendor_prefix))
self._commands['setSinkToUse'] = ('POST', '/session/$sessionId/{}/cast/set_sink_to_use'.format(vendor_prefix))
self._commands['startTabMirroring'] = ('POST', '/session/$sessionId/{}/cast/start_tab_mirroring'.format(vendor_prefix))
self._commands['stopCasting'] = ('POST', '/session/$sessionId/{}/cast/stop_casting'.format(vendor_prefix))

View File

@@ -0,0 +1,48 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import List
from selenium.webdriver.common import service
class ChromiumService(service.Service):
"""
Object that manages the starting and stopping the WebDriver instance of the ChromiumDriver
"""
def __init__(self, executable_path: str, port: int = 0, service_args: List[str] = None,
log_path: str = None, env: str = None, start_error_message: str = None):
"""
Creates a new instance of the Service
:Args:
- executable_path : Path to the WebDriver executable
- port : Port the service is running on
- service_args : List of args to pass to the WebDriver service
- log_path : Path for the WebDriver service to log to"""
self.service_args = service_args or []
if log_path:
self.service_args.append('--log-path=%s' % log_path)
if not start_error_message:
raise AttributeError("start_error_message should not be empty")
service.Service.__init__(self, executable_path, port=port, env=env, start_error_message=start_error_message)
def command_line_args(self) -> List[str]:
return ["--port=%d" % self.port] + self.service_args

View File

@@ -0,0 +1,237 @@
# 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 NoReturn
from selenium.webdriver.common.options import BaseOptions
from selenium.webdriver.common.service import Service
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.webdriver.chrome.options import Options as ChromeOptions
import warnings
from selenium.webdriver.chromium.remote_connection import ChromiumRemoteConnection
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
DEFAULT_PORT = 0
DEFAULT_SERVICE_LOG_PATH = None
DEFAULT_KEEP_ALIVE = None
class ChromiumDriver(RemoteWebDriver):
"""
Controls the WebDriver instance of ChromiumDriver and allows you to drive the browser.
"""
def __init__(self, browser_name, vendor_prefix,
port=DEFAULT_PORT, options: BaseOptions = None, service_args=None,
desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
"""
Creates a new WebDriver instance of the ChromiumDriver.
Starts the service and then creates new WebDriver instance of ChromiumDriver.
:Args:
- browser_name - Browser name used when matching capabilities.
- vendor_prefix - Company prefix to apply to vendor-specific WebDriver extension commands.
- port - Deprecated: port you would like the service to run, if left as 0, a free port will be found.
- options - this takes an instance of ChromiumOptions
- service_args - Deprecated: List of args to pass to the driver service
- desired_capabilities - Deprecated: Dictionary object with non-browser specific
capabilities only, such as "proxy" or "loggingPref".
- service_log_path - Deprecated: Where to log information from the driver.
- keep_alive - Deprecated: Whether to configure ChromiumRemoteConnection to use HTTP keep-alive.
"""
if desired_capabilities:
warnings.warn('desired_capabilities has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if port != DEFAULT_PORT:
warnings.warn('port has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
self.port = port
if service_log_path != DEFAULT_SERVICE_LOG_PATH:
warnings.warn('service_log_path has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
if keep_alive != DEFAULT_KEEP_ALIVE and type(self) == __class__:
warnings.warn('keep_alive has been deprecated, please pass in a Service object',
DeprecationWarning, stacklevel=2)
else:
keep_alive = True
self.vendor_prefix = vendor_prefix
_ignore_proxy = None
if not options:
options = self.create_options()
if desired_capabilities:
for key, value in desired_capabilities.items():
options.set_capability(key, value)
if options._ignore_local_proxy:
_ignore_proxy = options._ignore_local_proxy
if not service:
raise AttributeError('service cannot be None')
self.service = service
self.service.start()
try:
RemoteWebDriver.__init__(
self,
command_executor=ChromiumRemoteConnection(
remote_server_addr=self.service.service_url,
browser_name=browser_name, vendor_prefix=vendor_prefix,
keep_alive=keep_alive, ignore_proxy=_ignore_proxy),
options=options)
except Exception:
self.quit()
raise
self._is_remote = False
def launch_app(self, id):
"""Launches Chromium app specified by id."""
return self.execute("launchApp", {'id': id})
def get_network_conditions(self):
"""
Gets Chromium network emulation settings.
:Returns:
A dict. For example:
{'latency': 4, 'download_throughput': 2, 'upload_throughput': 2,
'offline': False}
"""
return self.execute("getNetworkConditions")['value']
def set_network_conditions(self, **network_conditions) -> NoReturn:
"""
Sets Chromium network emulation settings.
:Args:
- network_conditions: A dict with conditions specification.
:Usage:
::
driver.set_network_conditions(
offline=False,
latency=5, # additional latency (ms)
download_throughput=500 * 1024, # maximal throughput
upload_throughput=500 * 1024) # maximal throughput
Note: 'throughput' can be used to set both (for download and upload).
"""
self.execute("setNetworkConditions", {
'network_conditions': network_conditions
})
def delete_network_conditions(self) -> NoReturn:
"""
Resets Chromium network emulation settings.
"""
self.execute("deleteNetworkConditions")
def set_permissions(self, name: str, value: str) -> NoReturn:
"""
Sets Applicable Permission.
:Args:
- name: The item to set the permission on.
- value: The value to set on the item
:Usage:
::
driver.set_permissions('clipboard-read', 'denied')
"""
self.execute("setPermissions", {'descriptor': {'name': name}, 'state': value})
def execute_cdp_cmd(self, cmd: str, cmd_args: dict):
"""
Execute Chrome Devtools Protocol command and get returned result
The command and command args should follow chrome devtools protocol domains/commands, refer to link
https://chromedevtools.github.io/devtools-protocol/
:Args:
- cmd: A str, command name
- cmd_args: A dict, command args. empty dict {} if there is no command args
:Usage:
::
driver.execute_cdp_cmd('Network.getResponseBody', {'requestId': requestId})
:Returns:
A dict, empty dict {} if there is no result to return.
For example to getResponseBody:
{'base64Encoded': False, 'body': 'response body string'}
"""
return self.execute("executeCdpCommand", {'cmd': cmd, 'params': cmd_args})['value']
def get_sinks(self) -> list:
"""
:Returns: A list of sinks avaliable for Cast.
"""
return self.execute('getSinks')['value']
def get_issue_message(self):
"""
:Returns: An error message when there is any issue in a Cast session.
"""
return self.execute('getIssueMessage')['value']
def set_sink_to_use(self, sink_name: str) -> str:
"""
Sets a specific sink, using its name, as a Cast session receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute('setSinkToUse', {'sinkName': sink_name})
def start_tab_mirroring(self, sink_name: str) -> str:
"""
Starts a tab mirroring session on a specific receiver target.
:Args:
- sink_name: Name of the sink to use as the target.
"""
return self.execute('startTabMirroring', {'sinkName': sink_name})
def stop_casting(self, sink_name: str) -> str:
"""
Stops the existing Cast session on a specific receiver target.
:Args:
- sink_name: Name of the sink to stop the Cast session.
"""
return self.execute('stopCasting', {'sinkName': sink_name})
def quit(self) -> NoReturn:
"""
Closes the browser and shuts down the ChromiumDriver executable
that is started when starting the ChromiumDriver
"""
try:
RemoteWebDriver.quit(self)
except Exception:
# We don't care about the message because something probably has gone wrong
pass
finally:
self.service.stop()
def create_options(self) -> BaseOptions:
if self.vendor_prefix == "ms":
return EdgeOptions()
else:
return ChromeOptions()

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,329 @@
# 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 .utils import keys_to_typing
from .actions.action_builder import ActionBuilder
class ActionChains(object):
"""
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, duration=250):
"""
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
self._actions = []
self.w3c_actions = ActionBuilder(driver, duration=duration)
def perform(self):
"""
Performs all stored actions.
"""
self.w3c_actions.perform()
def reset_actions(self):
"""
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()
self._actions = []
def click(self, on_element=None):
"""
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=None):
"""
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=None):
"""
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=None):
"""
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, target):
"""
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, xoffset, yoffset):
"""
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, element=None):
"""
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, element=None):
"""
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, yoffset):
"""
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):
"""
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, xoffset, yoffset):
"""
Move the mouse by an offset of the specified element.
Offsets are relative to the top-left corner of the element.
:Args:
- to_element: The WebElement to move to.
- xoffset: X offset to move to.
- yoffset: Y offset to move to.
"""
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):
""" 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=None):
"""
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):
"""
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, *keys_to_send):
"""
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
# Context manager so ActionChains can be used in a 'with .. as' statements.
def __enter__(self):
return self # Return created instance of self.
def __exit__(self, _type, _value, _traceback):
pass # Do nothing, does not require additional cleanup.

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,86 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from selenium.webdriver.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
class ActionBuilder(object):
def __init__(self, driver, mouse=None, keyboard=None, duration=250):
if not mouse:
mouse = PointerInput(interaction.POINTER_MOUSE, "mouse")
if not keyboard:
keyboard = KeyInput(interaction.KEY)
self.devices = [mouse, keyboard]
self._key_action = KeyActions(keyboard)
self._pointer_action = PointerActions(mouse, duration=duration)
self.driver = driver
def get_device_with(self, name):
try:
idx = self.devices.index(name)
return self.devices[idx]
except Exception:
pass
@property
def pointer_inputs(self):
return [device for device in self.devices if device.type == interaction.POINTER]
@property
def key_inputs(self):
return [device for device in self.devices if device.type == interaction.KEY]
@property
def key_action(self):
return self._key_action
@property
def pointer_action(self):
return self._pointer_action
def add_key_input(self, name):
new_input = KeyInput(name)
self._add_input(new_input)
return new_input
def add_pointer_input(self, kind, name):
new_input = PointerInput(kind, name)
self._add_input(new_input)
return new_input
def perform(self):
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):
"""
Clears actions that are already stored on the remote end
"""
self.driver.execute(Command.W3C_CLEAR_ACTIONS)
def _add_input(self, input):
self.devices.append(input)

View File

@@ -0,0 +1,43 @@
# 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
class InputDevice(object):
"""
Describes the input device being used for the action.
"""
def __init__(self, name=None):
if not name:
self.name = uuid.uuid4()
else:
self.name = name
self.actions = []
def add_action(self, action):
"""
"""
self.actions.append(action)
def clear_actions(self):
self.actions = []
def create_pause(self, duration=0):
pass

View File

@@ -0,0 +1,50 @@
# 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.
KEY = "key"
POINTER = "pointer"
NONE = "none"
SOURCE_TYPES = set([KEY, POINTER, NONE])
POINTER_MOUSE = "mouse"
POINTER_TOUCH = "touch"
POINTER_PEN = "pen"
POINTER_KINDS = set([POINTER_MOUSE, POINTER_TOUCH, POINTER_PEN])
class Interaction(object):
PAUSE = "pause"
def __init__(self, source):
self.source = source
class Pause(Interaction):
def __init__(self, source, duration=0):
super(Interaction, self).__init__()
self.source = source
self.duration = duration
def encode(self):
return {
"type": self.PAUSE,
"duration": int(self.duration * 1000)
}

View File

@@ -0,0 +1,50 @@
# 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, KEY
from .key_input import KeyInput
from ..utils import keys_to_typing
class KeyActions(Interaction):
def __init__(self, source=None):
if not source:
source = KeyInput(KEY)
self.source = source
super(KeyActions, self).__init__(source)
def key_down(self, letter):
return self._key_action("create_key_down", letter)
def key_up(self, letter):
return self._key_action("create_key_up", letter)
def pause(self, duration=0):
return self._key_action("create_pause", duration)
def send_keys(self, text):
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, letter):
meth = getattr(self.source, action)
meth(letter)
return self

View File

@@ -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.
from . import interaction
from .input_device import InputDevice
from .interaction import (Interaction,
Pause)
class KeyInput(InputDevice):
def __init__(self, name):
super(KeyInput, self).__init__()
self.name = name
self.type = interaction.KEY
def encode(self):
return {"type": self.type, "id": self.name, "actions": [acts.encode() for acts in self.actions]}
def create_key_down(self, key):
self.add_action(TypingInteraction(self, "keyDown", key))
def create_key_up(self, key):
self.add_action(TypingInteraction(self, "keyUp", key))
def create_pause(self, pause_duration=0):
self.add_action(Pause(self, pause_duration))
class TypingInteraction(Interaction):
def __init__(self, source, type_, key):
super(TypingInteraction, self).__init__(source)
self.type = type_
self.key = key
def encode(self):
return {"type": self.type, "value": self.key}

View File

@@ -0,0 +1,23 @@
# 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(object):
LEFT = 0
MIDDLE = 1
RIGHT = 2

View File

@@ -0,0 +1,109 @@
# 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 .interaction import Interaction
from .mouse_button import MouseButton
from .pointer_input import PointerInput
from selenium.webdriver.remote.webelement import WebElement
class PointerActions(Interaction):
def __init__(self, source=None, duration=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(PointerActions, self).__init__(source)
def pointer_down(self, button=MouseButton.LEFT):
self._button_action("create_pointer_down", button=button)
def pointer_up(self, button=MouseButton.LEFT):
self._button_action("create_pointer_up", button=button)
def move_to(self, element, x=None, y=None):
if not isinstance(element, WebElement):
raise AttributeError("move_to requires a WebElement")
if x or y:
el_rect = element.rect
left_offset = el_rect['width'] / 2
top_offset = el_rect['height'] / 2
left = -left_offset + (x or 0)
top = -top_offset + (y or 0)
else:
left = 0
top = 0
self.source.create_pointer_move(origin=element, duration=self._duration, x=int(left), y=int(top))
return self
def move_by(self, x, y):
self.source.create_pointer_move(origin=interaction.POINTER, duration=self._duration, x=int(x), y=int(y))
return self
def move_to_location(self, x, y):
self.source.create_pointer_move(origin='viewport', duration=self._duration, x=int(x), y=int(y))
return self
def click(self, element=None):
if element:
self.move_to(element)
self.pointer_down(MouseButton.LEFT)
self.pointer_up(MouseButton.LEFT)
return self
def context_click(self, element=None):
if element:
self.move_to(element)
self.pointer_down(MouseButton.RIGHT)
self.pointer_up(MouseButton.RIGHT)
return self
def click_and_hold(self, element=None):
if element:
self.move_to(element)
self.pointer_down()
return self
def release(self):
self.pointer_up()
return self
def double_click(self, element=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=0):
self.source.create_pause(duration)
return self
def _button_action(self, action, button=MouseButton.LEFT):
meth = getattr(self.source, action)
meth(button)
return self

View File

@@ -0,0 +1,63 @@
# 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 .input_device import InputDevice
from .interaction import POINTER, POINTER_KINDS
from selenium.common.exceptions import InvalidArgumentException
from selenium.webdriver.remote.webelement import WebElement
class PointerInput(InputDevice):
DEFAULT_MOVE_DURATION = 250
def __init__(self, kind, name):
super(PointerInput, self).__init__()
if kind not in POINTER_KINDS:
raise InvalidArgumentException("Invalid PointerInput kind '%s'" % kind)
self.type = POINTER
self.kind = kind
self.name = name
def create_pointer_move(self, duration=DEFAULT_MOVE_DURATION, x=None, y=None, origin=None):
action = dict(type="pointerMove", duration=duration)
action["x"] = x
action["y"] = y
if isinstance(origin, WebElement):
action["origin"] = {"element-6066-11e4-a52e-4f735466cecf": origin.id}
elif origin:
action["origin"] = origin
self.add_action(action)
def create_pointer_down(self, button):
self.add_action({"type": "pointerDown", "duration": 0, "button": button})
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):
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": [acts for acts in self.actions]}

View File

@@ -0,0 +1,90 @@
# 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(object):
"""
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):
"""
Creates a new Alert.
:Args:
- driver: The WebDriver instance which performs user actions.
"""
self.driver = driver
@property
def text(self):
"""
Gets the text of the Alert.
"""
return self.driver.execute(Command.W3C_GET_ALERT_TEXT)["value"]
def dismiss(self):
"""
Dismisses the alert available.
"""
self.driver.execute(Command.W3C_DISMISS_ALERT)
def accept(self):
"""
Accepts the alert available.
Usage::
Alert(driver).accept() # Confirm a alert dialog.
"""
self.driver.execute(Command.W3C_ACCEPT_ALERT)
def send_keys(self, keysToSend):
"""
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})

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,483 @@
# 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
from trio_websocket import (
ConnectionClosed as WsConnectionClosed,
connect_websocket_url,
)
import trio
from collections import defaultdict
from contextlib import (contextmanager, asynccontextmanager)
from dataclasses import dataclass
import contextvars
import importlib
import itertools
import json
import logging
import typing
logger = logging.getLogger('trio_cdp')
T = typing.TypeVar('T')
MAX_WS_MESSAGE_SIZE = 2**24
devtools = None
version = None
def import_devtools(ver):
global devtools
global version
version = ver
devtools = importlib.import_module("selenium.webdriver.common.devtools.v{}".format(version))
_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['code']
self.message = obj['message']
self.detail = obj.get('data')
def __str__(self):
return 'BrowserError<code={} message={}> {}'.format(self.code,
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 '{}<{}>'.format(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 = dict()
self.inflight_result = dict()
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)
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 isinstance(response, Exception):
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, 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: {}'.format(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:
response = 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 = dict()
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('Browser sent a message for an invalid '
'session: {!r}'.format(session_id))
session._handle_data(data)
else:
self._handle_data(data)
@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

View File

@@ -0,0 +1,25 @@
# 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"

View File

@@ -0,0 +1,35 @@
# Licensed to the Software Freedom Conservancy (SFC) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The SFC licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
The By implementation.
"""
class By(object):
"""
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"

View File

@@ -0,0 +1,113 @@
# 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(object):
"""
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",
}
OPERA = {
"browserName": "opera",
}
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",
"version": "",
"platform": "ANY",
}
WPEWEBKIT = {
"browserName": "MiniBrowser",
"version": "",
"platform": "ANY",
}

View File

@@ -0,0 +1,51 @@
# 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 application_cache
from . import audits
from . import background_service
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_orientation
from . import emulation
from . import fetch
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 page
from . import performance
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

Some files were not shown because too many files have changed in this diff Show More