from ast import literal_eval
from functools import partial
from time import sleep
from random import random
import importlib
# import base64
import logging
import os
import re
from xvfbwrapper import Xvfb
from selenium import webdriver
from selenium.common.exceptions import (NoSuchElementException, NoSuchWindowException,
NoAlertPresentException)
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.remote_connection import LOGGER as s_logger
from selenium.webdriver.support.ui import Select, WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
[docs]class WebRunner(object):
"""
WebRunner runs an instance of Selenium and adds a lot of helpful
browser methods.
Generally this class aims to make the experience of using Selenium
better overall.
Many functions take a "selector" argument.
These can be any valid CSS selector..
See https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Selectors
"""
# Variables for configuration
browser = None
extra_verbose = False
silence = open(os.devnull, 'w')
def __init__(self, **kwargs):
self.base_url = kwargs.get('base_url', 'http://127.0.0.1:5000')
self.root_path = kwargs.get('', './')
xvfb = kwargs.get('', True)
driver = kwargs.get('', 'Firefox')
mootools = kwargs.get('', False)
timeout = kwargs.get('', 90)
width = kwargs.get('', 1440)
height = kwargs.get('', 1200)
desired_capabilities = kwargs.get('', 'FIREFOX')
command_executor = kwargs.get('', 'http://127.0.0.1:4444/wd/hub')
# Firefox, PhantomJS (Must be installed...)
self.driver = os.environ.get('WR_DRIVER', driver)
# This is for headless running.
self.xvfb = os.environ.get('WR_XVFB', xvfb)
# Use MooTools instead of jQuery
self.mootools = os.environ.get('WR_MOOTOOLS', mootools)
# Global timeout option for all wait_* functions
self.timeout = os.environ.get('WR_TIMEOUT', timeout)
# XVFB virtual monitor width
self.width = os.environ.get('WR_WIDTH', width)
# XVFB virtual monitor width
self.height = os.environ.get('WR_HEIGHT', height)
self.desired_capabilities = os.environ.get(
'WR_DESIRED_CAPABILITIES', desired_capabilities)
self.command_executor = os.environ.get(
'WR_COMMAND_EXECUTOR', command_executor)
self.yaml_funcs = {}
self.yaml_vars = {}
if os.environ.get('skip_xvfb'):
self.xvfb = False
# Turn off annoying selenium logs
s_logger.setLevel(logging.WARNING)
def _before(self):
pass
[docs] def start(self):
"""Starts Selenium and (optionally) starts XVFB first.
Note:
XVFB is typically used with headless runs. If you do not use XVFB
and the browser you have selected has a GUI (Firefox, Chrome...)
then your browser will launch and you will be able to view the
test as it is being ran.
This is particularly useful for problematic test runs or when building
new tests.
"""
if self.xvfb:
try:
print("\nStarting XVFB display...")
self.display = Xvfb(width=self.width, height=self.height, colordepth=16)
try:
self.display.start()
except OSError:
self.xvfb = False
print("\nUnable to start XVFB. Try running `./build/selenium.sh`")
except EnvironmentError:
print("\nSkipping XVFB run since it is not present.")
self.xvfb = False
print("\nStarting browser ({})...".format(self.driver))
if self.driver == "PhantomJS":
self.browser = webdriver.PhantomJS()
elif self.driver == "Chrome":
self.browser = webdriver.Chrome()
elif self.driver == "Opera":
self.browser = webdriver.Opera()
elif self.driver == "Ie":
self.browser = webdriver.Ie()
elif self.driver == "Remote":
if isinstance(self.desired_capabilities, dict):
dc = self.desired_capabilities
else:
dcu = self.desired_capabilities.upper()
if dcu == 'IE':
dcu = 'INTERNETEXPLORER'
dc = getattr(DesiredCapabilities, dcu)
self.browser = webdriver.Remote(
command_executor=self.command_executor,
desired_capabilities=dc
)
elif self.driver == 'Firefox':
# Get rid of the annoying start page by setting preferences
fp = webdriver.FirefoxProfile()
# Download from: https://github.com/mguillem/JSErrorCollector/raw/master/dist/JSErrorCollector.xpi
# fp.add_extension('JSErrorCollector.xpi')
fp.set_preference("browser.startup.homepage_override.mstone", "ignore")
fp.set_preference("startup.homepage_welcome_url.additional", "about:blank")
self.browser = webdriver.Firefox(firefox_profile=fp)
else:
raise UserWarning('No valid driver detected.')
[docs] def stop(self):
'''
Stops Selenium. Also stops XVFB if it was launched as a part of this
class run.
Note:
Anything you don't stop here will need to be cleaned up with a kill
command if you launched it outside of nosetests.
'''
print("\nStopping the browser...")
self.browser.quit()
if self.xvfb:
print("\nStopping the XVFB display...")
self.display.stop()
# Helper functions:
[docs] def wait_for(self, method, **kwargs):
'''
Wait for the supplied method to return True. A simple wrapper for _wait_for().
Parameters
----------
method: callable
The function that determines the conditions for the wait to finish
timeout: int
Number of seconds to wait for `method` to return True
'''
self._wait_for(method, **kwargs)
[docs] def back(self):
'''
Goes one step back in the browser's history.
Just a convenient wrapper around the browser's back command
'''
self.browser.back()
[docs] def forward(self):
'''
Goes one step forward in the browser's history.
Just a convenient wrapper around the browser's forward command
'''
self.browser.forward()
[docs] def click(self, selector, elem=None):
'''
Clicks an element.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector..
'''
self.wait_for_presence(selector)
self.scroll_to_element(selector)
self.wait_for_clickable(selector)
if not elem:
elem = self.get_element(selector)
elem.click()
def _parse_item(self, tp):
try:
from faker import Faker
fake = Faker()
except ImportError:
fake = None
return_value = ''
parts = tp.split('|')
if parts[0] == 'eval':
return_value = eval(str(parts[1]))
elif parts[0] == 'vars':
# Automatically default to 0 index if not passed.
print(parts)
if len(parts) == 2:
parts.append('0')
return self.yaml_vars[parts[1]][int(parts[2])]
elif self.yaml_funcs.get(parts[0]):
args_regex = '\[([^}]+)\]' # Get list args: [1,2,3]
if len(re.findall(args_regex, parts[1])):
# If this is a list, let's just literal_eval this and use the values.
args = literal_eval(parts[1])
return_value = self.yaml_funcs[parts[0]](args)
else:
# Otherwise, these are positional arguments.
pargs = [p.strip() for p in parts[1].split(',')]
args = []
for parg in pargs:
try:
# Try to convert ints to ints
arg = literal_eval(parg)
except ValueError:
# This is probably a string.
arg = parg
args.append(arg)
return_value = self.yaml_funcs[parts[0]](*args)
if fake and parts[0].startswith('fake'):
base = parts[0].replace('fake_', '').replace('fake-', '')
return_value = getattr(fake, base)()
if parts[0] != 'vars' and not self.yaml_vars.get(parts[0]):
self.yaml_vars[parts[0]] = []
self.yaml_vars[parts[0]].append(return_value)
return return_value
def _import(self, to_import):
try:
parts = to_import.split('.')
if len(parts) == 0:
if not self.yaml_funcs.get(to_import):
self.yaml_funcs[to_import] = importlib.import_module(to_import)
else:
if not self.yaml_funcs.get(parts[-1]):
module = importlib.import_module(*parts)
self.yaml_funcs[parts[-1]] = getattr(module, parts[-1])
except SystemError:
print("Sorry, Python < 3.5 can't import {} via YAML.".format(to_import))
def _run_command(self, command):
'''
Internal method for running a command from a parsed YAML script.
Parameters
----------
command: dict
A dict where the key is a method of this class.
'''
pre_parse_regex = '\(\(([^}]+)\)\)' # Grab things wrapped like so: (( something ))
for k in command:
if hasattr(self, k):
if type(command[k]) is list:
# Loop over the arguments and pre-parse them.
for index, item in enumerate(command[k]):
if type(item) is not list:
if type(item) is not dict:
to_parse = [i.strip() for i in re.findall(pre_parse_regex, item)]
if to_parse:
tp = self._parse_item(to_parse[0])
command[k][index] = tp
else:
for subindex, subitem in enumerate(item):
to_parse = [si.strip() for si in re.findall(pre_parse_regex, subitem)]
if to_parse:
tp = self._parse_item(to_parse[0])
command[k][index][subindex] = tp
if type(command[k][0]) is list:
getattr(self, k)(command[k])
else:
getattr(self, k)(*command[k])
else:
getattr(self, k)(command[k])
else:
if k == 'import':
self._import(command[k])
elif k in ('value_of', 'text_of'):
if not self.yaml_vars.get(k):
self.yaml_vars[k] = []
if k == 'value_of':
value = self.get_value(command[k])
elif k == 'text_of':
value = self.get_text(command[k])
self.yaml_vars[k].append(value)
[docs] def command_script(self, filepath=None, script=None, errors=True, verbose=False):
'''
Runs a script of PyWebRunner command_script
Parameters
----------
script: list of dicts
A list of dicts where the key is the method to execute.
errors: bool
Whether or not to call bail_out on error
verbose:
Print extra debugging information
'''
self._import('random.randint')
self._import('random.choice')
if not script and filepath:
from yaml import load
with open(filepath, 'r') as f:
script = load(f)
for index, command in enumerate(script):
digits = len(str(len(script)))
if verbose:
cmd = list(command.keys())[0]
print('({}) Parsing: {}: {}'.format(str(index + 1).zfill(digits), cmd, command[cmd]))
if errors:
self._run_command(command)
else:
try:
self._run_command(command)
except Exception as e:
self.bail_out(exception=e, caller='command_script')
[docs] def get_log(self, log=None):
'''
Gets the console log for the browser.
'''
if not log:
log = 'browser'
log_list = self.browser.get_log(log)
return log_list
[docs] def get_log_text(self):
'''
Gets the console log text for the browser.
[{u'level': u'SEVERE',
u'message': u'ReferenceError: foo is not defined',
u'timestamp': 1450769357488,
u'type': u''},
{u'level': u'INFO',
u'message': u'The character encoding of the HTML document was not declared. The document will render with garbled text in some browser configurations if the document contains characters from outside the US-ASCII range. The character encoding of the page must be declared in the document or in the transfer protocol.',
u'timestamp': 1450769357498,
u'type': u''}]
'''
log = self.get_log()
log_text = ''
log_items = [item['message'] for item in log]
for item in log_items:
log_text += item + '\n'
return log_text
[docs] def bail_out(self, exception=None, caller=None):
'''
Method for reporting and, optionally, bypassing errors during a command.
Parameters
----------
exception: Exception
The exception object.
caller: str
The method that called the bail_out.
'''
print(caller)
print(exception)
self.stop()
[docs] def click_all(self, selector):
'''
Clicks all elements.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector..
All matching elements will be clicked.
'''
elements = self.get_elements(selector)
for element in elements:
if element.is_displayed:
element.click()
[docs] def get_page_source(self):
'''
Gets the source code of the page.
'''
src = self.browser.page_source
return src
[docs] def is_text_on_page(self, text):
'''
Finds text if it is present on the page.
Parameters
----------
text: str
The text to search for.
'''
src = self.get_page_source()
src = src.encode('ascii', errors='ignore')
return bool(text in src)
[docs] def set_select_by_text(self, select, text):
'''
Set the selected value of a select element by the visible text.
Parameters
----------
select: str or selenium.webdriver.remote.webelement.WebElement
Any valid CSS selector or a selenium element
text: str
The visible text in the select element option. (Not the value)
'''
if isinstance(select, str):
elem = self.get_element(select)
else:
elem = select
sel = Select(elem)
sel.select_by_visible_text(text)
[docs] def set_select_by_value(self, select, value):
'''
Set the selected value of a select element by the value.
Parameters
----------
select: str or selenium.webdriver.remote.webelement.WebElement
Any valid CSS selector or a selenium element
value: str
The value on the select element option. (Not the visible text)
'''
if isinstance(select, str):
elem = self.get_element(select)
else:
elem = select
sel = Select(elem)
sel.select_by_value(value)
[docs] def move_to(self, selector, click=False):
'''
Move to the element matched by selector or passed as argument.
Parameters
----------
selector: str
Any valid CSS selector
click: bool
Whether or not to click the element after hovering
defaults to False
'''
elem = self.get_element(selector)
action = webdriver.ActionChains(self.browser)
action.move_to_element(elem)
if click:
action.click(elem)
action.perform()
[docs] def hover(self, selector, click=False):
'''
Hover over the element matched by selector and optionally click it.
Parameters
----------
selector: str
Any valid CSS selector
click: bool
Whether or not to click the element after hovering
defaults to False
'''
self.move_to(selector, click)
[docs] def get_elements(self, selector):
'''
Gets elements by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
Returns
-------
list of selenium.webdriver.remote.webelement.WebElement
A list of selenium element objects.
'''
elems = self.find_elements(selector)
return elems
[docs] def get_element(self, selector):
'''
Gets element by CSS selector.
Parameters
----------
selector: str
A CSS/XPATH selector to search for. This can be any valid CSS/XPATH selector.
Returns
-------
selenium.webdriver.remote.webelement.WebElement
A selenium element object.
'''
elem = self.find_element(selector)
return elem
[docs] def get_text(self, selector):
'''
Gets text from inside of an element by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
Returns
-------
str
The text from inside of a selenium element object.
'''
elem = self.get_element(selector)
return str(elem.text)
[docs] def get_texts(self, selector):
'''
Gets all the text from all elements found by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
Returns
-------
list of str
A list of text strings from inside of all found selenium element objects.
'''
elems = self.get_elements(selector)
texts = [str(e.text) for e in elems]
return texts
[docs] def get_value(self, selector):
'''
Gets value of an element by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
Returns
-------
str
The value of a selenium element object.
'''
elem = self.get_element(selector)
return elem.get_attribute('value')
[docs] def send_key(self, selector, key, wait_for='presence', **kwargs):
'''
Sets value of an element by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
key: str
A str representation of a special key to send.
Some available keys and their string representations:
::
'ADD' = u'\ue025'
'ALT' = u'\ue00a'
'ARROW_DOWN' = u'\ue015'
'ARROW_LEFT' = u'\ue012'
'ARROW_RIGHT' = u'\ue014'
'ARROW_UP' = u'\ue013'
'BACKSPACE' = u'\ue003'
'BACK_SPACE' = u'\ue003'
'CANCEL' = u'\ue001'
'CLEAR' = u'\ue005'
'COMMAND' = u'\ue03d'
'CONTROL' = u'\ue009'
'DECIMAL' = u'\ue028'
'DELETE' = u'\ue017'
'DIVIDE' = u'\ue029'
'DOWN' = u'\ue015'
'END' = u'\ue010'
'ENTER' = u'\ue007'
'EQUALS' = u'\ue019'
'ESCAPE' = u'\ue00c'
'F1' = u'\ue031'
'F10' = u'\ue03a'
'F11' = u'\ue03b'
'F12' = u'\ue03c'
'F2' = u'\ue032'
'F3' = u'\ue033'
'F4' = u'\ue034'
'F5' = u'\ue035'
'F6' = u'\ue036'
'F7' = u'\ue037'
'F8' = u'\ue038'
'F9' = u'\ue039'
'HELP' = u'\ue002'
'HOME' = u'\ue011'
'INSERT' = u'\ue016'
'LEFT' = u'\ue012'
'LEFT_ALT' = u'\ue00a'
'LEFT_CONTROL' = u'\ue009'
'LEFT_SHIFT' = u'\ue008'
'META' = u'\ue03d'
'MULTIPLY' = u'\ue024'
'NULL' = u'\ue000'
'NUMPAD0' = u'\ue01a'
'NUMPAD1' = u'\ue01b'
'NUMPAD2' = u'\ue01c'
'NUMPAD3' = u'\ue01d'
'NUMPAD4' = u'\ue01e'
'NUMPAD5' = u'\ue01f'
'NUMPAD6' = u'\ue020'
'NUMPAD7' = u'\ue021'
'NUMPAD8' = u'\ue022'
'NUMPAD9' = u'\ue023'
'PAGE_DOWN' = u'\ue00f'
'PAGE_UP' = u'\ue00e'
'PAUSE' = u'\ue00b'
'RETURN' = u'\ue006'
'RIGHT' = u'\ue014'
'SEMICOLON' = u'\ue018'
'SEPARATOR' = u'\ue026'
'SHIFT' = u'\ue008'
'SPACE' = u'\ue00d'
'SUBTRACT' = u'\ue027'
'TAB' = u'\ue004'
'UP' = u'\ue013'
kwargs:
passed on to wait_for_*
'''
self._wait_for_presence_or_visible(selector, wait_for, **kwargs)
elem = self.get_element(selector)
if hasattr(Keys, key.upper()):
elem.send_keys(getattr(Keys, key.upper()))
[docs] def drag_and_drop(self, from_selector, to_selector):
'''
Drags an element into another.
Parameters
----------
from_selector: str
A CSS selector to search for. This can be any valid CSS selector.
Element to be dragged.
to_selector: str
A CSS selector to search for. This can be any valid CSS selector.
Target element to be dragged into.
'''
from_element = self.get_element(from_selector)
to_element = self.get_element(to_selector)
ActionChains(self.browser).drag_and_drop(from_element, to_element).perform()
[docs] def set_values(self, values, clear=True, blur=True, **kwargs):
'''
Sets values of elements by CSS selectors.
Parameters
----------
values: list of list or dict or list of dict
A list of lists where index 0 is a selector string and 1 is a value.
clear: bool
Whether or not we should clear the element's value first.
If false, value will be appended to the current value of the element.
blur: bool
Whether or not we should blur the element after setting the value.
Defaults to True
kwargs:
passed on to wait_for_visible
'''
if isinstance(values, dict):
# If the entire var is a dict, just use all the key/value pairs
for key in values:
self.set_value(key, values[key], clear=clear, blur=blur, **kwargs)
else:
# If not a dict it's a list/tuple of things (dicts or lists / tuples)
for row in values:
if isinstance(row, dict):
# If it is a dict use it's key / value pairs.
for key in row:
self.set_value(key, row[key], clear=clear, blur=blur, **kwargs)
else:
# Otherwise just use the list / tuple positions
self.set_value(row[0], row[1], clear=clear, blur=blur, **kwargs)
[docs] def wait(self, seconds=500):
'''
Sleeps for some amount of time.
Parameters
----------
seconds: int
Seconds to sleep for.
'''
# You probably shouldn't use this for anything
# real in tests. I use this for pausing execution.
sleep(seconds)
[docs] def set_value(self, selector, value, clear=True, blur=True, **kwargs):
'''
Sets value of an element by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
value: str
The value to set on the element matched by the selector.
clear: bool
Whether or not we should clear the element's value first.
If false, value will be appended to the current value of the element.
blur: bool
Whether or not we should blur the element after setting the value.
Defaults to True
kwargs:
passed on to wait_for_visible
'''
typing = kwargs.get('typing', False)
typing_speed = kwargs.get('typing_speed', 3)
typing_max_delay = kwargs.get('typing_max_delay', .33)
self.wait_for_visible(selector, **kwargs)
elem = self.get_element(selector)
if elem.tag_name == 'select':
self.set_select_by_value(elem, value)
else:
if clear:
self.clear(selector)
if typing:
for k in value:
delay = random() / typing_speed
if delay > typing_max_delay:
delay = typing_max_delay
sleep(delay)
elem.send_keys(k)
else:
elem.send_keys(value)
if blur:
elem.send_keys(Keys.TAB)
[docs] def set_selectize(self, selector, value, text=None, clear=True, blur=False):
'''
Sets visible value of a selectize control based on the "selectized" element.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
value: str
The value of the option to select.
(Stored Value)
text: str
The visible value that the user sees.
(Visible value, if different than the stored value)
clear: bool
Whether or not we should clear the selectize value first.
Defaults to True
blur: bool
Whether or not we should blur the element after setting the value.
This corresponds to the 'selectOnTab' selecize setting.
Defaults to False
'''
selectize_control = selector + ' + .selectize-control'
selectize_input = selectize_control + ' input'
# Make sure the selectize control is active so the input is visible
self.click(selectize_control)
input_element = self.get_element(selectize_input)
if clear:
input_element.send_keys(Keys.BACK_SPACE)
input_element.send_keys(text or value)
# Wait for options to be rendered
self.wait_for_visible(selectize_control + ' .has-options')
if blur:
input_element.send_keys(Keys.TAB)
else:
# Click the option for the given value
self.click(selectize_control + ' .option[data-value="{}"]'.format(value))
[docs] def clear(self, selector):
'''
Clears value of an element by CSS selector.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
'''
elem = self.get_element(selector)
elem.clear()
[docs] def current_url(self):
'''
Gets current URL of the browser object.
Returns
-------
str
The curent URL.
'''
return self.browser.current_url
[docs] def go(self, address):
'''
Go to a web address. (self.browser should be available, but not needed.)
Parameters
----------
address: str
The address (URL)
'''
self.browser.get(address)
[docs] def count(self, selector):
'''
Counts the number of elements that match CSS/XPATH selector.
Parameters
----------
selector: str
A CSS/XPATH selector to search for. This can be any valid CSS/XPATH selector.
Returns
-------
int: Number of matching elements.
'''
return len(self.get_elements(selector))
[docs] def find_element(self, selector):
'''
Finds an element by CSS/XPATH selector.
Parameters
----------
selector: str
A CSS/XPATH selector to search for. This can be any valid CSS/XPATH selector.
Returns
-------
selenium.webdriver.remote.webelement.WebElement or None
Returns an element or nothing at all
'''
elem = None
try:
if selector.startswith('/'):
elem = self.browser.find_element_by_xpath(selector)
else:
elem = self.browser.find_element_by_css_selector(selector)
except NoSuchElementException:
pass
return elem
[docs] def find_elements(self, selector):
'''
Finds elements by CSS/XPATH selector.
Parameters
----------
selector: str
A CSS/XPATH selector to search for. This can be any valid CSS/XPATH selector.
Returns
-------
list of selenium.webdriver.remote.webelement.WebElement or list
Returns a list of elements or empty list
'''
elems = []
try:
if selector.startswith('/'):
elems = self.browser.find_elements_by_xpath(selector)
else:
elems = self.browser.find_elements_by_css_selector(selector)
except NoSuchElementException:
pass
return elems
[docs] def refresh_page(self, refresh_method="url"):
'''
Refreshes the current page
Parameters
----------
method: str
The method used to refresh the page.
Defaults to "url" which navigates to the current_url
'''
if refresh_method == "url":
self.browser.get(self.browser.current_url)
elif refresh_method == "js":
self.js('window.location.reload(true);')
[docs] def js(self, js_str, *args):
'''
Run some JavaScript and return the result.
Parameters
----------
js_str: str
A string containing some valid JavaScript to be ran on the page.
Returns
-------
str or bool or list or dict
Returns the result of the JS evaluation.
'''
return self.browser.execute_script(js_str, *args)
def _find_elements(self, row):
'''
Find elements using a name, css selector, class, xpath, or id.
Parameters
----------
row: dict
A dict where the key is the search method
and the value is what is passed to Selenium
Returns
-------
list of selenium.webdriver.remote.webelement.WebElement
A list of selenium element objects.
'''
elems = []
try:
if 'name' in row:
elems = self.browser.find_elements_by_name(row['name'])
elif 'css' in row:
elems = self.browser.find_elements_by_css_selector(row['css'])
elif 'class' in row:
elems = self.browser.find_elements_by_class_name(row['class'])
elif 'xpath' in row:
elems = self.browser.find_elements_by_xpath(row['xpath'])
else:
elems = self.browser.find_elements_by_id(row['id'])
except NoSuchElementException:
pass
finally:
return elems
[docs] def save_page_source(self, path='/tmp/selenium-page-source.html'):
'''
Saves the raw page html in it's current state. Takes a path as a parameter.
Parameters
----------
path: str
Defaults to: /tmp/selenium-page-source.html
'''
page_source = self.browser.page_source
out_file = open(path, 'w')
out_file.write(page_source.encode('utf8'))
out_file.close()
[docs] def screenshot(self, path=None):
'''
Saves a screenshot. Takes a path as a parameter.
Parameters
----------
path: str
Defaults to: /tmp/selenium-screenshot.png
'''
if not path:
path = '/tmp/selenium-screenshot.png'
# if isinstance(self.browser, webdriver.remote.webdriver.WebDriver):
# # Get base64 screenshot from the remote.
# base64_data = self.browser.get_screenshot_as_base64()
# ss_data = base64.decodestring(base64_data)
# with open(path, 'w') as f:
# f.write(ss_data)
# f.close()
# else:
self.browser.save_screenshot(path)
[docs] def fill(self, form_dict):
'''
Fills a form using Selenium. This helper will save a lot of time
and effort because working with form data can be tricky and gross.
Parameters
----------
form_dict: dict
Takes in a dict where the keys are CSS selectors
and the values are what will be applied to them.
'''
form_list = []
for key in form_dict:
form_list.append({'css': key, 'value': form_dict[key]})
self.fill_form(form_list)
# Custom asynchronous wait helpers
def _wait_for(self, wait_function, **kwargs):
'''
Wrapper to handle the boilerplate involved with a custom wait.
Parameters
----------
wait_function: func
This can be a builtin selenium wait_for class,
a special wait_for class that implements the __call__ method,
or a lambda function
timeout: int
The number of seconds to wait for the given condition
before throwing an error.
Overrides WebRunner.timeout
'''
wait = WebDriverWait(self.browser, kwargs.get('timeout') or self.timeout)
wait.until(wait_function)
[docs] def wait_for_alert(self, **kwargs):
'''
Shortcut for waiting for alert. If it not ends with exception, it
returns that alert.
'''
self._wait_for(self.alert_present, **kwargs)
[docs] def wait_for_presence(self, selector='', **kwargs):
'''
Wait for an element to be present. (Does not need to be visible.)
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.presence_of_element_located((by, selector)) or
EC.presence_of_elements_located((by, selector)),
**kwargs)
[docs] def wait_for_clickable(self, selector='', **kwargs):
'''
Wait for an element to be clickable.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.element_to_be_clickable((by, selector)), **kwargs)
[docs] def wait_for_ko(self, selector='', **kwargs):
'''
Wait for an element to be bound by Knockout JS.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
self.wait_for_presence(selector)
ieo_func = 'function __isEmptyObject(obj){var name;for(name in obj){return false;}return true;}'
js_check_bound = "{0}return !__isEmptyObject(ko.dataFor(document.querySelectorAll('{1}')[0]));"
self.wait_for_js(js_check_bound.format(ieo_func, selector), **kwargs)
[docs] def wait_for_url(self, url='', **kwargs):
'''
Wait for the current url to match the given url.
Parameters
----------
url: str
A regular expression to match against the current url
kwargs:
Passed on to _wait_for
'''
self._wait_for(expect_url_match(url), **kwargs)
[docs] def wait_for_visible(self, selector='', **kwargs):
'''
Wait for an element to be visible.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.visibility_of_element_located((by, selector)),
**kwargs)
def _wait_for_presence_or_visible(self, selector, wait_for, **kwargs):
'''
Wrapper around wait_for_presence and wait_for_visible that takes a
string to decide which one to use.
'''
if wait_for == 'presence':
self.wait_for_presence(selector, **kwargs)
elif wait_for == 'visible':
self.wait_for_visible(selector, **kwargs)
[docs] def wait_for_invisible(self, selector='', **kwargs):
'''
Wait for an element to be invisible.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.invisibility_of_element_located((by, selector)),
**kwargs)
[docs] def wait_for_all_invisible(self, selector='', **kwargs):
'''
Wait for all elements that match selector to be invisible.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
kwargs:
Passed on to _wait_for
'''
all_matches = self.get_elements(selector)
for match in all_matches:
self._wait_for(invisibility_of(match), **kwargs)
[docs] def wait_for_js(self, js_script, **kwargs):
'''
Wait for the given JS to return true.
Parameters
----------
js_script: str
valid JS that will run in the page dom
kwargs:
passed on to _wait_for
'''
self._wait_for(lambda browser: bool(browser.execute_script(js_script)),
**kwargs)
[docs] def wait_for_text(self, selector='', text='', **kwargs):
'''
Wait for an element to contain a specific string.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
text: str
The string to look for. This must be precise.
(Case, punctuation, UTF characters... etc.)
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.text_to_be_present_in_element((by, selector),
text), **kwargs)
[docs] def wait_for_text_in_value(self, selector='', text='', **kwargs):
'''
Wait for an element's value to contain a specific string.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
text: str
The string to look for. This must be precise.
(Case, punctuation, UTF characters... etc.)
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.text_to_be_present_in_element_value((by, selector),
text), **kwargs)
[docs] def wait_for_selected(self, selector='', selected=True, **kwargs):
'''
Wait for an element (checkbox/radio) to be selected.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
selected: bool
Whether or not the element should be selected. Default True
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.element_located_selection_state_to_be((by, selector),
selected), **kwargs)
[docs] def wait_for_title(self, title, **kwargs):
'''
Wait for the page title to match given title.
Parameters
----------
title: str
The page title to wait for
kwargs:
Passed on to _wait_for
'''
self._wait_for(EC.title_is(title), **kwargs)
[docs] def wait_for_value(self, selector='', value='', **kwargs):
'''
Wait for an element to contain a specific string.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
value: str
The string to look for. This must be precise.
(Case, punctuation, UTF characters... etc.)
kwargs:
Passed on to _wait_for
'''
if selector.startswith('/'):
by = By.XPATH
else:
by = By.CSS_SELECTOR
self._wait_for(EC.text_to_be_present_in_element_value((by, selector),
value), **kwargs)
[docs] def wait_for_opacity(self, selector, opacity, **kwargs):
'''
Wait for an element to reach a specific opacity.
Parameters
----------
selector: str
A CSS selector to search for. This can be any valid CSS selector.
opacity: float
The opacity to wait for.
kwargs:
Passed on to _wait_for
'''
def _wait_for_opacity(self, browser):
return str(self.get_element(selector).value_of_css_property('opacity')) == str(opacity)
self._wait_for(partial(_wait_for_opacity, self), **kwargs)
[docs] def switch_to_window(self, window_name=None, title=None, url=None):
'''
Switch to window by name, title, or url.
Parameters
----------
window_name: str
The name of the window to switch to.
title: str
The title of the window you wish to switch to.
url: str
URL of the window you want to switch to.
'''
if window_name:
self.browser.switch_to_window(window_name)
return
else:
for window_handle in self.browser.window_handles:
self.browser.switch_to_window(window_handle)
if title and self.browser.title == title:
return
if url and self.browser.current_url == url:
return
raise NoSuchWindowException('Window not found: {}, {}, {}'.format(window_name, title, url))
[docs] def close_window(self, window_name=None, title=None, url=None):
"""
Close window by name, title, or url.
Parameters
----------
window_name: str
The name of the window to switch to.
title: str
The title of the window you wish to switch to.
url: str
URL of the window you want to switch to.
"""
main_window_handle = self.browser.current_window_handle
self.switch_to_window(window_name, title, url)
self.browser.close()
self.switch_to_window(main_window_handle)
[docs] def close_all_other_windows(self):
'''
Closes all windows except for the currently active one.
'''
main_window_handle = self.browser.current_window_handle
for window_handle in self.browser.window_handles:
if window_handle == main_window_handle:
continue
self.switch_to_window(window_handle)
self.browser.close()
self.switch_to_window(main_window_handle)
[docs] def close_alert(self, ignore_exception=False):
'''
Closes any alert that is present. Raises an exception if no alert is found.
Parameters
----------
ignore_exception: bool
Does not throw an exception if an alert is not present.
'''
try:
alert = self.get_alert()
alert.accept()
except NoAlertPresentException:
if not ignore_exception:
raise
[docs] def get_alert(self):
'''
Returns instance of :py:obj:`~selenium.webdriver.common.alert.Alert`.
'''
return Alert(self.browser)
[docs] def alert_present(self):
'''
Checks to see if an alert is present.
'''
alert = Alert(self.browser)
try:
alert.text
return True
except NoAlertPresentException:
return False
class expect_url_match(object):
'''Checks for the current url to match. '''
def __init__(self, url_check):
self.url_check = url_check
def __call__(self, driver):
return re.search(self.url_check, driver.current_url)
class invisibility_of(object):
''' Checks for a known element to be invisible.
Much like the builtin visibility_of:
https://github.com/SeleniumHQ/selenium/search?utf8=%E2%9C%93&q=visibility_of
'''
def __init__(self, element):
self.element = element
def __call__(self, driver):
try:
return (not self.element.is_displayed())
except EC.StaleElementReferenceException:
# If the element reference is no longer valid,
# it was likely removed from the dom and is no longer visible
return True