SeleniumBase/seleniumbase/core/sb_cdp.py

2199 lines
83 KiB
Python

"""Add CDP methods to extend the driver"""
import asyncio
import fasteners
import os
import re
import sys
import time
from contextlib import suppress
from seleniumbase import config as sb_config
from seleniumbase.config import settings
from seleniumbase.fixtures import constants
from seleniumbase.fixtures import js_utils
from seleniumbase.fixtures import page_utils
from seleniumbase.fixtures import shared_utils
from seleniumbase.undetected.cdp_driver import cdp_util
class CDPMethods():
def __init__(self, loop, page, driver):
self.loop = loop
self.page = page
self.driver = driver
def _swap_driver(self, driver):
self.driver = driver
self.page = driver.cdp.page
self.loop = driver.cdp.loop
def __slow_mode_pause_if_set(self):
if (
(hasattr(sb_config, "demo_mode") and sb_config.demo_mode)
or "--demo" in sys.argv
):
time.sleep(0.48)
elif (
(hasattr(sb_config, "slow_mode") and sb_config.slow_mode)
or "--slow" in sys.argv
):
time.sleep(0.24)
def __add_light_pause(self):
time.sleep(0.007)
def __convert_to_css_if_xpath(self, selector):
if page_utils.is_xpath_selector(selector):
with suppress(Exception):
css = js_utils.convert_to_css_selector(selector, "xpath")
if css:
return css
return selector
def __add_sync_methods(self, element):
if not element:
return element
element.clear_input = lambda: self.__clear_input(element)
element.click = lambda: self.__click(element)
element.flash = lambda *args, **kwargs: self.__flash(
element, *args, **kwargs
)
element.focus = lambda: self.__focus(element)
element.gui_click = (
lambda *args, **kwargs: self.__gui_click(element, *args, **kwargs)
)
element.highlight_overlay = lambda: self.__highlight_overlay(element)
element.mouse_click = lambda: self.__mouse_click(element)
element.mouse_drag = (
lambda destination: self.__mouse_drag(element, destination)
)
element.mouse_move = lambda: self.__mouse_move(element)
element.press_keys = lambda text: self.__press_keys(element, text)
element.query_selector = (
lambda selector: self.__query_selector(element, selector)
)
element.querySelector = element.query_selector
element.query_selector_all = (
lambda selector: self.__query_selector_all(element, selector)
)
element.querySelectorAll = element.query_selector_all
element.remove_from_dom = lambda: self.__remove_from_dom(element)
element.save_screenshot = (
lambda *args, **kwargs: self.__save_screenshot(
element, *args, **kwargs)
)
element.save_to_dom = lambda: self.__save_to_dom(element)
element.scroll_into_view = lambda: self.__scroll_into_view(element)
element.select_option = lambda: self.__select_option(element)
element.send_file = (
lambda *file_paths: self.__send_file(element, *file_paths)
)
element.send_keys = lambda text: self.__send_keys(element, text)
element.set_text = lambda value: self.__set_text(element, value)
element.set_value = lambda value: self.__set_value(element, value)
element.type = lambda text: self.__type(element, text)
element.get_position = lambda: self.__get_position(element)
element.get_html = lambda: self.__get_html(element)
element.get_js_attributes = lambda: self.__get_js_attributes(element)
element.get_attribute = (
lambda attribute: self.__get_attribute(element, attribute)
)
# element.get_parent() should come last
element.get_parent = lambda: self.__get_parent(element)
return element
def get(self, url):
url = shared_utils.fix_url_as_needed(url)
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
self.loop.run_until_complete(self.page.get(url))
url_protocol = url.split(":")[0]
safe_url = True
if url_protocol not in ["about", "data", "chrome"]:
safe_url = False
if not safe_url:
time.sleep(constants.UC.CDP_MODE_OPEN_WAIT)
if shared_utils.is_windows():
time.sleep(constants.UC.EXTRA_WINDOWS_WAIT)
else:
time.sleep(0.012)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def open(self, url):
self.get(url)
def reload(self, ignore_cache=True, script_to_evaluate_on_load=None):
self.loop.run_until_complete(
self.page.reload(
ignore_cache=ignore_cache,
script_to_evaluate_on_load=script_to_evaluate_on_load,
)
)
def refresh(self, *args, **kwargs):
self.reload(*args, **kwargs)
def get_event_loop(self):
return self.loop
def add_handler(self, event, handler):
self.page.add_handler(event, handler)
def find_element(self, selector, best_match=False, timeout=None):
"""Similar to select(), but also finds elements by text content.
When using text-based searches, if best_match=False, then will
find the first element with the text. If best_match=True, then
if multiple elements have that text, then will use the element
with the closest text-length to the text being searched for."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__add_light_pause()
selector = self.__convert_to_css_if_xpath(selector)
early_failure = False
if (":contains(") in selector:
selector, _ = page_utils.recalculate_selector(
selector, by="css selector", xp_ok=True
)
failure = False
try:
if early_failure:
raise Exception("Failed!")
element = self.loop.run_until_complete(
self.page.find(
selector, best_match=best_match, timeout=timeout
)
)
except Exception:
failure = True
plural = "s"
if timeout == 1:
plural = ""
message = "\n Element {%s} was not found after %s second%s!" % (
selector,
timeout,
plural,
)
if failure:
raise Exception(message)
element = self.__add_sync_methods(element)
self.__slow_mode_pause_if_set()
return element
def find_element_by_text(self, text, tag_name=None, timeout=None):
"""Returns an element by matching text.
Optionally, provide a tag_name to narrow down the search to an
element with the given tag. (Eg: a, button, div, script, span)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__add_light_pause()
time_now = time.time()
self.assert_text(text, timeout=timeout)
spent = int(time.time() - time_now)
remaining = 1 + timeout - spent
if tag_name:
self.assert_element(tag_name, timeout=remaining)
elements = self.loop.run_until_complete(
self.page.find_elements_by_text(text=text)
)
if tag_name:
tag_name = tag_name.lower().strip()
for element in elements:
if element and not tag_name:
element = self.__add_sync_methods(element)
return self.__add_sync_methods(element)
elif (
element
and tag_name in element.tag_name.lower()
and text.strip() in element.text
):
element = self.__add_sync_methods(element)
return self.__add_sync_methods(element)
elif (
element
and element.parent
and tag_name in element.parent.tag_name.lower()
and text.strip() in element.parent.text
):
element = self.__add_sync_methods(element.parent)
return self.__add_sync_methods(element)
elif (
element
and element.parent
and element.parent.parent
and tag_name in element.parent.parent.tag_name.lower()
and text.strip() in element.parent.parent.text
):
element = self.__add_sync_methods(element.parent.parent)
return self.__add_sync_methods(element)
plural = "s"
if timeout == 1:
plural = ""
raise Exception(
"Text {%s} with tag {%s} was not found after %s second%s!"
% (text, tag_name, timeout, plural)
)
def find_all(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__add_light_pause()
selector = self.__convert_to_css_if_xpath(selector)
elements = self.loop.run_until_complete(
self.page.find_all(selector, timeout=timeout)
)
updated_elements = []
for element in elements:
element = self.__add_sync_methods(element)
updated_elements.append(element)
return updated_elements
def find_elements_by_text(self, text, tag_name=None):
"""Returns a list of elements by matching text.
Optionally, provide a tag_name to narrow down the search to only
elements with the given tag. (Eg: a, button, div, script, span)"""
self.__add_light_pause()
elements = self.loop.run_until_complete(
self.page.find_elements_by_text(text=text)
)
updated_elements = []
if tag_name:
tag_name = tag_name.lower().strip()
for element in elements:
if element and not tag_name:
element = self.__add_sync_methods(element)
if element not in updated_elements:
updated_elements.append(element)
elif (
element
and tag_name in element.tag_name.lower()
and text.strip() in element.text
):
element = self.__add_sync_methods(element)
if element not in updated_elements:
updated_elements.append(element)
elif (
element
and element.parent
and tag_name in element.parent.tag_name.lower()
and text.strip() in element.parent.text
):
element = self.__add_sync_methods(element.parent)
if element not in updated_elements:
updated_elements.append(element)
elif (
element
and element.parent
and element.parent.parent
and tag_name in element.parent.parent.tag_name.lower()
and text.strip() in element.parent.parent.text
):
element = self.__add_sync_methods(element.parent.parent)
if element not in updated_elements:
updated_elements.append(element)
return updated_elements
def select(self, selector, timeout=None):
"""Similar to find_element()."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__add_light_pause()
selector = self.__convert_to_css_if_xpath(selector)
if (":contains(" in selector):
tag_name = selector.split(":contains(")[0].split(" ")[-1]
text = selector.split(":contains(")[1].split(")")[0][1:-1]
with suppress(Exception):
new_timeout = timeout
if new_timeout < 1:
new_timeout = 1
self.loop.run_until_complete(
self.page.select(tag_name, timeout=new_timeout)
)
self.loop.run_until_complete(
self.page.find(text, timeout=new_timeout)
)
elements = self.find_elements_by_text(text, tag_name=tag_name)
if not elements:
plural = "s"
if timeout == 1:
plural = ""
msg = "\n Element {%s} was not found after %s second%s!"
message = msg % (selector, timeout, plural)
raise Exception(message)
element = self.__add_sync_methods(elements[0])
return element
failure = False
try:
element = self.loop.run_until_complete(
self.page.select(selector, timeout=timeout)
)
except Exception:
failure = True
plural = "s"
if timeout == 1:
plural = ""
msg = "\n Element {%s} was not found after %s second%s!"
message = msg % (selector, timeout, plural)
if failure:
raise Exception(message)
element = self.__add_sync_methods(element)
self.__slow_mode_pause_if_set()
return element
def select_all(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__add_light_pause()
selector = self.__convert_to_css_if_xpath(selector)
elements = self.loop.run_until_complete(
self.page.select_all(selector, timeout=timeout)
)
updated_elements = []
for element in elements:
element = self.__add_sync_methods(element)
updated_elements.append(element)
return updated_elements
def find_elements(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
return self.select_all(selector, timeout=timeout)
def find_visible_elements(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
visible_elements = []
elements = self.select_all(selector, timeout=timeout)
for element in elements:
with suppress(Exception):
position = element.get_position()
if (position.width != 0 or position.height != 0):
visible_elements.append(element)
return visible_elements
def click_nth_element(self, selector, number):
elements = self.select_all(selector)
if len(elements) < number:
raise Exception(
"Not enough matching {%s} elements to "
"click number %s!" % (selector, number)
)
number = number - 1
if number < 0:
number = 0
element = elements[number]
element.scroll_into_view()
element.click()
def click_nth_visible_element(self, selector, number):
"""Finds all matching page elements and clicks the nth visible one.
Example: self.click_nth_visible_element('[type="checkbox"]', 5)
(Clicks the 5th visible checkbox on the page.)"""
elements = self.find_visible_elements(selector)
if len(elements) < number:
raise Exception(
"Not enough matching {%s} elements to "
"click number %s!" % (selector, number)
)
number = number - 1
if number < 0:
number = 0
element = elements[number]
element.scroll_into_view()
element.click()
def click_link(self, link_text):
self.find_elements_by_text(link_text, "a")[0].click()
def go_back(self):
self.loop.run_until_complete(self.page.back())
def go_forward(self):
self.loop.run_until_complete(self.page.forward())
def get_navigation_history(self):
return self.loop.run_until_complete(self.page.get_navigation_history())
def __clear_input(self, element):
return (
self.loop.run_until_complete(element.clear_input_async())
)
def __click(self, element):
result = (
self.loop.run_until_complete(element.click_async())
)
self.loop.run_until_complete(self.page.wait())
return result
def __flash(self, element, *args, **kwargs):
element.scroll_into_view()
if len(args) < 3 and "x_offset" not in kwargs:
x_offset = self.__get_x_scroll_offset()
kwargs["x_offset"] = x_offset
if len(args) < 3 and "y_offset" not in kwargs:
y_offset = self.__get_y_scroll_offset()
kwargs["y_offset"] = y_offset
return (
self.loop.run_until_complete(
element.flash_async(*args, **kwargs)
)
)
def __focus(self, element):
return (
self.loop.run_until_complete(element.focus_async())
)
def __gui_click(self, element, timeframe=None):
element.scroll_into_view()
self.__add_light_pause()
position = element.get_position()
x = position.x
y = position.y
e_width = position.width
e_height = position.height
# Relative to window
element_rect = {"height": e_height, "width": e_width, "x": x, "y": y}
window_rect = self.get_window_rect()
w_bottom_y = window_rect["y"] + window_rect["height"]
viewport_height = window_rect["innerHeight"]
x = window_rect["x"] + element_rect["x"]
y = w_bottom_y - viewport_height + element_rect["y"]
y_scroll_offset = window_rect["pageYOffset"]
y = y - y_scroll_offset
x = x + window_rect["scrollX"]
y = y + window_rect["scrollY"]
# Relative to screen
element_rect = {"height": e_height, "width": e_width, "x": x, "y": y}
e_width = element_rect["width"]
e_height = element_rect["height"]
e_x = element_rect["x"]
e_y = element_rect["y"]
x, y = ((e_x + e_width / 2.0) + 0.5), ((e_y + e_height / 2.0) + 0.5)
if not timeframe or not isinstance(timeframe, (int, float)):
timeframe = 0.25
if timeframe > 3:
timeframe = 3
self.gui_click_x_y(x, y, timeframe=timeframe)
return self.loop.run_until_complete(self.page.wait())
def __highlight_overlay(self, element):
return (
self.loop.run_until_complete(element.highlight_overlay_async())
)
def __mouse_click(self, element):
result = (
self.loop.run_until_complete(element.mouse_click_async())
)
self.loop.run_until_complete(self.page.wait())
return result
def __mouse_drag(self, element, destination):
return (
self.loop.run_until_complete(element.mouse_drag_async(destination))
)
def __mouse_move(self, element):
return (
self.loop.run_until_complete(element.mouse_move_async())
)
def __press_keys(self, element, text):
element.scroll_into_view()
submit = False
if text.endswith("\n") or text.endswith("\r"):
submit = True
text = text[:-1]
for key in text:
element.send_keys(key)
time.sleep(0.044)
if submit:
element.send_keys("\r\n")
time.sleep(0.044)
self.__slow_mode_pause_if_set()
return self.loop.run_until_complete(self.page.sleep(0.025))
def __query_selector(self, element, selector):
selector = self.__convert_to_css_if_xpath(selector)
element2 = self.loop.run_until_complete(
element.query_selector_async(selector)
)
element2 = self.__add_sync_methods(element2)
return element2
def __query_selector_all(self, element, selector):
selector = self.__convert_to_css_if_xpath(selector)
elements = self.loop.run_until_complete(
element.query_selector_all_async(selector)
)
updated_elements = []
for element in elements:
element = self.__add_sync_methods(element)
updated_elements.append(element)
self.__slow_mode_pause_if_set()
return updated_elements
def __remove_from_dom(self, element):
return (
self.loop.run_until_complete(element.remove_from_dom_async())
)
def __save_screenshot(self, element, *args, **kwargs):
return (
self.loop.run_until_complete(
element.save_screenshot_async(*args, **kwargs)
)
)
def __save_to_dom(self, element):
return (
self.loop.run_until_complete(element.save_to_dom_async())
)
def __scroll_into_view(self, element):
self.loop.run_until_complete(element.scroll_into_view_async())
self.__add_light_pause()
return None
def __select_option(self, element):
return (
self.loop.run_until_complete(element.select_option_async())
)
def __send_file(self, element, *file_paths):
return (
self.loop.run_until_complete(element.send_file_async(*file_paths))
)
def __send_keys(self, element, text):
return (
self.loop.run_until_complete(element.send_keys_async(text))
)
def __set_text(self, element, value):
return (
self.loop.run_until_complete(element.set_text_async(value))
)
def __set_value(self, element, value):
return (
self.loop.run_until_complete(element.set_value_async(value))
)
def __type(self, element, text):
with suppress(Exception):
element.clear_input()
element.send_keys(text)
def __get_position(self, element):
return (
self.loop.run_until_complete(element.get_position_async())
)
def __get_html(self, element):
return (
self.loop.run_until_complete(element.get_html_async())
)
def __get_js_attributes(self, element):
return (
self.loop.run_until_complete(element.get_js_attributes_async())
)
def __get_attribute(self, element, attribute):
try:
return element.get_js_attributes()[attribute]
except Exception:
if not attribute:
raise
try:
attribute_str = element.get_js_attributes()
locate = ' %s="' % attribute
if locate in attribute_str.outerHTML:
outer_html = attribute_str.outerHTML
attr_start = outer_html.find(locate) + len(locate)
attr_end = outer_html.find('"', attr_start)
value = outer_html[attr_start:attr_end]
return value
except Exception:
pass
return None
def __get_parent(self, element):
return self.__add_sync_methods(element.parent)
def __get_x_scroll_offset(self):
x_scroll_offset = self.loop.run_until_complete(
self.page.evaluate("window.pageXOffset")
)
return x_scroll_offset or 0
def __get_y_scroll_offset(self):
y_scroll_offset = self.loop.run_until_complete(
self.page.evaluate("window.pageYOffset")
)
return y_scroll_offset or 0
def tile_windows(self, windows=None, max_columns=0):
"""Tile windows and return the grid of tiled windows."""
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.tile_windows(windows, max_columns)
)
def get_all_cookies(self, *args, **kwargs):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.cookies.get_all(*args, **kwargs)
)
def set_all_cookies(self, *args, **kwargs):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.cookies.set_all(*args, **kwargs)
)
def save_cookies(self, *args, **kwargs):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.cookies.save(*args, **kwargs)
)
def load_cookies(self, *args, **kwargs):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.cookies.load(*args, **kwargs)
)
def clear_cookies(self):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return self.loop.run_until_complete(
driver.cookies.clear()
)
def sleep(self, seconds):
time.sleep(seconds)
def bring_active_window_to_front(self):
self.loop.run_until_complete(self.page.bring_to_front())
self.__add_light_pause()
def get_active_element(self):
return self.loop.run_until_complete(
self.page.js_dumps("document.activeElement")
)
def get_active_element_css(self):
from seleniumbase.js_code import active_css_js
js_code = active_css_js.get_active_element_css
js_code = js_code.replace("return getBestSelector", "getBestSelector")
return self.loop.run_until_complete(
self.page.evaluate(js_code)
)
def click(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.find_element(selector, timeout=timeout)
element.scroll_into_view()
element.click()
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def click_active_element(self):
self.loop.run_until_complete(
self.page.evaluate("document.activeElement.click()")
)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def click_if_visible(self, selector):
if self.is_element_visible(selector):
with suppress(Exception):
element = self.find_element(selector, timeout=0)
element.scroll_into_view()
element.click()
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def click_visible_elements(self, selector, limit=0):
"""Finds all matching page elements and clicks visible ones in order.
If a click reloads or opens a new page, the clicking will stop.
If no matching elements appear, an Exception will be raised.
If "limit" is set and > 0, will only click that many elements.
Also clicks elements that become visible from previous clicks.
Works best for actions such as clicking all checkboxes on a page.
Example: self.click_visible_elements('input[type="checkbox"]')"""
elements = self.select_all(selector)
click_count = 0
for element in elements:
if limit and limit > 0 and click_count >= limit:
return
try:
width = 0
height = 0
try:
position = element.get_position()
width = position.width
height = position.height
except Exception:
continue
if (width != 0 or height != 0):
element.scroll_into_view()
element.click()
click_count += 1
time.sleep(0.042)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
except Exception:
break
def mouse_click(self, selector, timeout=None):
"""(Attempt simulating a mouse click)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.find_element(selector, timeout=timeout)
element.scroll_into_view()
element.mouse_click()
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def nested_click(self, parent_selector, selector):
"""
Find parent element and click on child element inside it.
(This can be used to click on elements inside an iframe.)
"""
element = self.find_element(parent_selector)
element.query_selector(selector).mouse_click()
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def get_nested_element(self, parent_selector, selector):
"""(Can be used to find an element inside an iframe)"""
element = self.find_element(parent_selector)
return element.query_selector(selector)
def select_option_by_text(self, dropdown_selector, option):
element = self.find_element(dropdown_selector)
element.scroll_into_view()
options = element.query_selector_all("option")
for found_option in options:
if found_option.text.strip() == option.strip():
found_option.select_option()
return
raise Exception(
"Unable to find text option {%s} in dropdown {%s}!"
% (dropdown_selector, option)
)
def flash(
self,
selector, # The CSS Selector to flash
duration=1, # (seconds) flash duration
color="44CC88", # RGB hex flash color
pause=0, # (seconds) If 0, the next action starts during flash
):
"""Paint a quickly-vanishing dot over an element."""
selector = self.__convert_to_css_if_xpath(selector)
element = self.find_element(selector)
element.scroll_into_view()
x_offset = self.__get_x_scroll_offset()
y_offset = self.__get_y_scroll_offset()
element.flash(duration, color, x_offset, y_offset)
if pause and isinstance(pause, (int, float)):
time.sleep(pause)
def highlight(self, selector):
"""Highlight an element with multi-colors."""
selector = self.__convert_to_css_if_xpath(selector)
element = self.find_element(selector)
element.scroll_into_view()
x_offset = self.__get_x_scroll_offset()
y_offset = self.__get_y_scroll_offset()
element.flash(0.46, "44CC88", x_offset, y_offset)
time.sleep(0.15)
element.flash(0.42, "8844CC", x_offset, y_offset)
time.sleep(0.15)
element.flash(0.38, "CC8844", x_offset, y_offset)
time.sleep(0.15)
element.flash(0.30, "44CC88", x_offset, y_offset)
time.sleep(0.30)
def focus(self, selector):
element = self.find_element(selector)
element.scroll_into_view()
element.focus()
def highlight_overlay(self, selector):
self.find_element(selector).highlight_overlay()
def get_parent(self, element):
if isinstance(element, str):
element = self.select(element)
return self.__add_sync_methods(element.parent)
def remove_element(self, selector):
self.select(selector).remove_from_dom()
def remove_from_dom(self, selector):
self.select(selector).remove_from_dom()
def remove_elements(self, selector):
"""Remove all elements on the page that match the selector."""
css_selector = self.__convert_to_css_if_xpath(selector)
css_selector = re.escape(css_selector) # Add "\\" to special chars
css_selector = js_utils.escape_quotes_if_needed(css_selector)
js_code = (
"""var $elements = document.querySelectorAll('%s');
var index = 0, length = $elements.length;
for(; index < length; index++){
$elements[index].remove();}"""
% css_selector
)
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
def send_keys(self, selector, text, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.select(selector, timeout=timeout)
element.scroll_into_view()
if text.endswith("\n") or text.endswith("\r"):
text = text[:-1] + "\r\n"
element.send_keys(text)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def press_keys(self, selector, text, timeout=None):
"""Similar to send_keys(), but presses keys at human speed."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.select(selector, timeout=timeout)
element.scroll_into_view()
submit = False
if text.endswith("\n") or text.endswith("\r"):
submit = True
text = text[:-1]
for key in text:
element.send_keys(key)
time.sleep(0.044)
if submit:
element.send_keys("\r\n")
time.sleep(0.044)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def type(self, selector, text, timeout=None):
"""Similar to send_keys(), but clears the text field first."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
element = self.select(selector, timeout=timeout)
element.scroll_into_view()
with suppress(Exception):
element.clear_input()
if text.endswith("\n") or text.endswith("\r"):
text = text[:-1] + "\r\n"
element.send_keys(text)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def set_value(self, selector, text, timeout=None):
"""Similar to send_keys(), but clears the text field first."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
self.__slow_mode_pause_if_set()
selector = self.__convert_to_css_if_xpath(selector)
element = self.select(selector, timeout=timeout)
element.scroll_into_view()
press_enter = False
if text.endswith("\n"):
text = text[:-1]
press_enter = True
value = js_utils.escape_quotes_if_needed(re.escape(text))
css_selector = re.escape(selector)
css_selector = js_utils.escape_quotes_if_needed(css_selector)
set_value_script = (
"""m_elm = document.querySelector('%s');"""
"""m_elm.value = '%s';""" % (css_selector, value)
)
self.loop.run_until_complete(self.page.evaluate(set_value_script))
the_type = self.get_element_attribute(selector, "type")
if the_type == "range":
# Some input sliders need a mouse event to trigger listeners.
with suppress(Exception):
mouse_move_script = (
"""m_elm = document.querySelector('%s');"""
"""m_evt = new Event('mousemove');"""
"""m_elm.dispatchEvent(m_evt);""" % css_selector
)
self.loop.run_until_complete(
self.page.evaluate(mouse_move_script)
)
elif press_enter:
self.__add_light_pause()
self.send_keys(selector, "\n")
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def submit(self, selector):
submit_script = (
"""elm = document.querySelector('%s');
const event = new KeyboardEvent("keydown", {
key: "Enter",
keyCode: 13,
code: "Enter",
which: 13,
bubbles: true
});
elm.dispatchEvent(event);""" % selector
)
self.loop.run_until_complete(self.page.evaluate(submit_script))
def evaluate(self, expression):
"""Run a JavaScript expression and return the result."""
expression = expression.strip()
exp_list = expression.split("\n")
if exp_list and exp_list[-1].strip().startswith("return "):
expression = (
"\n".join(exp_list[0:-1]) + "\n"
+ exp_list[-1].strip()[len("return "):]
).strip()
return self.loop.run_until_complete(
self.page.evaluate(expression)
)
def js_dumps(self, obj_name):
"""Similar to evaluate(), but for dictionary results."""
if obj_name.startswith("return "):
obj_name = obj_name[len("return "):]
return self.loop.run_until_complete(
self.page.js_dumps(obj_name)
)
def maximize(self):
if self.get_window()[1].window_state.value == "maximized":
return
elif self.get_window()[1].window_state.value == "minimized":
self.loop.run_until_complete(self.page.maximize())
time.sleep(0.044)
return self.loop.run_until_complete(self.page.maximize())
def minimize(self):
if self.get_window()[1].window_state.value != "minimized":
return self.loop.run_until_complete(self.page.minimize())
def medimize(self):
if self.get_window()[1].window_state.value == "minimized":
self.loop.run_until_complete(self.page.medimize())
time.sleep(0.044)
return self.loop.run_until_complete(self.page.medimize())
def set_window_rect(self, x, y, width, height):
if self.get_window()[1].window_state.value == "minimized":
self.loop.run_until_complete(
self.page.set_window_size(
left=x, top=y, width=width, height=height)
)
time.sleep(0.044)
return self.loop.run_until_complete(
self.page.set_window_size(
left=x, top=y, width=width, height=height)
)
def reset_window_size(self):
x = settings.WINDOW_START_X
y = settings.WINDOW_START_Y
width = settings.CHROME_START_WIDTH
height = settings.CHROME_START_HEIGHT
self.set_window_rect(x, y, width, height)
self.__add_light_pause()
def open_new_window(self, url=None, switch_to=True):
return self.open_new_tab(url=url, switch_to=switch_to)
def switch_to_window(self, window):
self.switch_to_tab(window)
def switch_to_newest_window(self):
self.switch_to_tab(-1)
def open_new_tab(self, url=None, switch_to=True):
if not isinstance(url, str):
url = "about:blank"
self.loop.run_until_complete(self.page.get(url, new_tab=True))
if switch_to:
self.switch_to_newest_tab()
def switch_to_tab(self, tab):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
if isinstance(tab, int):
self.page = driver.tabs[tab]
elif isinstance(tab, cdp_util.Tab):
self.page = tab
else:
raise Exception("`tab` must be an int or a Tab type!")
self.bring_active_window_to_front()
def switch_to_newest_tab(self):
self.switch_to_tab(-1)
def close_active_tab(self):
"""Close the active tab.
The active tab is the one currenly controlled by CDP.
The active tab MIGHT NOT be the currently visible tab!
(If a page opens a new tab, the new tab WON'T be active)
To switch the active tab, call: sb.switch_to_tab(tab)"""
return self.loop.run_until_complete(self.page.close())
def get_active_tab(self):
"""Return the active tab.
The active tab is the one currenly controlled by CDP.
The active tab MIGHT NOT be the currently visible tab!
(If a page opens a new tab, the new tab WON'T be active)
To switch the active tab, call: sb.switch_to_tab(tab)"""
return self.page
def get_tabs(self):
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
return driver.tabs
def get_window(self):
return self.loop.run_until_complete(self.page.get_window())
def get_text(self, selector):
return self.find_element(selector).text_all
def get_title(self):
return self.loop.run_until_complete(
self.page.evaluate("document.title")
)
def get_current_url(self):
return self.loop.run_until_complete(
self.page.evaluate("window.location.href")
)
def get_origin(self):
return self.loop.run_until_complete(
self.page.evaluate("window.location.origin")
)
def get_page_source(self):
try:
source = self.loop.run_until_complete(
self.page.evaluate("document.documentElement.outerHTML")
)
except Exception:
time.sleep(constants.UC.CDP_MODE_OPEN_WAIT)
source = self.loop.run_until_complete(
self.page.evaluate("document.documentElement.outerHTML")
)
return source
def get_user_agent(self):
return self.loop.run_until_complete(
self.page.evaluate("navigator.userAgent")
)
def get_cookie_string(self):
return self.loop.run_until_complete(
self.page.evaluate("document.cookie")
)
def get_locale_code(self):
return self.loop.run_until_complete(
self.page.evaluate("navigator.language || navigator.languages[0]")
)
def get_local_storage_item(self, key):
js_code = """localStorage.getItem('%s');""" % key
with suppress(Exception):
return self.loop.run_until_complete(self.page.evaluate(js_code))
def get_session_storage_item(self, key):
js_code = """sessionStorage.getItem('%s');""" % key
with suppress(Exception):
return self.loop.run_until_complete(self.page.evaluate(js_code))
def get_screen_rect(self):
coordinates = self.loop.run_until_complete(
self.page.js_dumps("window.screen")
)
return coordinates
def get_window_rect(self):
coordinates = {}
innerWidth = self.loop.run_until_complete(
self.page.evaluate("window.innerWidth")
)
innerHeight = self.loop.run_until_complete(
self.page.evaluate("window.innerHeight")
)
outerWidth = self.loop.run_until_complete(
self.page.evaluate("window.outerWidth")
)
outerHeight = self.loop.run_until_complete(
self.page.evaluate("window.outerHeight")
)
pageXOffset = self.loop.run_until_complete(
self.page.evaluate("window.pageXOffset")
)
pageYOffset = self.loop.run_until_complete(
self.page.evaluate("window.pageYOffset")
)
scrollX = self.loop.run_until_complete(
self.page.evaluate("window.scrollX")
)
scrollY = self.loop.run_until_complete(
self.page.evaluate("window.scrollY")
)
screenLeft = self.loop.run_until_complete(
self.page.evaluate("window.screenLeft")
)
screenTop = self.loop.run_until_complete(
self.page.evaluate("window.screenTop")
)
x = self.loop.run_until_complete(
self.page.evaluate("window.screenX")
)
y = self.loop.run_until_complete(
self.page.evaluate("window.screenY")
)
coordinates["innerWidth"] = innerWidth
coordinates["innerHeight"] = innerHeight
coordinates["outerWidth"] = outerWidth
coordinates["outerHeight"] = outerHeight
coordinates["width"] = outerWidth
coordinates["height"] = outerHeight
coordinates["pageXOffset"] = pageXOffset if pageXOffset else 0
coordinates["pageYOffset"] = pageYOffset if pageYOffset else 0
coordinates["scrollX"] = scrollX if scrollX else 0
coordinates["scrollY"] = scrollY if scrollY else 0
coordinates["screenLeft"] = screenLeft if screenLeft else 0
coordinates["screenTop"] = screenTop if screenTop else 0
coordinates["x"] = x if x else 0
coordinates["y"] = y if y else 0
return coordinates
def get_window_size(self):
coordinates = {}
outerWidth = self.loop.run_until_complete(
self.page.evaluate("window.outerWidth")
)
outerHeight = self.loop.run_until_complete(
self.page.evaluate("window.outerHeight")
)
coordinates["width"] = outerWidth
coordinates["height"] = outerHeight
return coordinates
def get_window_position(self):
coordinates = {}
x = self.loop.run_until_complete(
self.page.evaluate("window.screenX")
)
y = self.loop.run_until_complete(
self.page.evaluate("window.screenY")
)
coordinates["x"] = x if x else 0
coordinates["y"] = y if y else 0
return coordinates
def get_element_rect(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
selector = self.__convert_to_css_if_xpath(selector)
self.select(selector, timeout=timeout)
self.__add_light_pause()
coordinates = self.loop.run_until_complete(
self.page.js_dumps(
"""document.querySelector"""
"""('%s').getBoundingClientRect()"""
% js_utils.escape_quotes_if_needed(re.escape(selector))
)
)
return coordinates
def get_element_size(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
element_rect = self.get_element_rect(selector, timeout=timeout)
coordinates = {}
coordinates["width"] = element_rect["width"]
coordinates["height"] = element_rect["height"]
return coordinates
def get_element_position(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
element_rect = self.get_element_rect(selector, timeout=timeout)
coordinates = {}
coordinates["x"] = element_rect["x"]
coordinates["y"] = element_rect["y"]
return coordinates
def get_gui_element_rect(self, selector, timeout=None):
"""(Coordinates are relative to the screen. Not the window.)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
element_rect = self.get_element_rect(selector, timeout=timeout)
e_width = element_rect["width"]
e_height = element_rect["height"]
window_rect = self.get_window_rect()
w_bottom_y = window_rect["y"] + window_rect["height"]
viewport_height = window_rect["innerHeight"]
x = window_rect["x"] + element_rect["x"]
y = w_bottom_y - viewport_height + element_rect["y"]
y_scroll_offset = window_rect["pageYOffset"]
y = y - y_scroll_offset
x = x + window_rect["scrollX"]
y = y + window_rect["scrollY"]
return ({"height": e_height, "width": e_width, "x": x, "y": y})
def get_gui_element_center(self, selector, timeout=None):
"""(Coordinates are relative to the screen. Not the window.)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
element_rect = self.get_gui_element_rect(selector, timeout=timeout)
e_width = element_rect["width"]
e_height = element_rect["height"]
e_x = element_rect["x"]
e_y = element_rect["y"]
return ((e_x + e_width / 2.0) + 0.5, (e_y + e_height / 2.0) + 0.5)
def get_document(self):
return self.loop.run_until_complete(self.page.get_document())
def get_flattened_document(self):
return self.loop.run_until_complete(self.page.get_flattened_document())
def get_element_attributes(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
return self.loop.run_until_complete(
self.page.js_dumps(
"""document.querySelector('%s')"""
% js_utils.escape_quotes_if_needed(re.escape(selector))
)
)
def get_element_attribute(self, selector, attribute):
attributes = self.get_element_attributes(selector)
with suppress(Exception):
return attributes[attribute]
locate = ' %s="' % attribute
value = self.get_attribute(selector, attribute)
if not value and locate not in attributes:
raise KeyError(attribute)
return value
def get_attribute(self, selector, attribute):
return self.find_element(selector).get_attribute(attribute)
def get_element_html(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
return self.loop.run_until_complete(
self.page.evaluate(
"""document.querySelector('%s').outerHTML"""
% js_utils.escape_quotes_if_needed(re.escape(selector))
)
)
def set_locale(self, locale):
"""(Settings will take effect on the next page load)"""
self.loop.run_until_complete(self.page.set_locale(locale))
def set_local_storage_item(self, key, value):
js_code = """localStorage.setItem('%s','%s');""" % (key, value)
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
def set_session_storage_item(self, key, value):
js_code = """sessionStorage.setItem('%s','%s');""" % (key, value)
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
def set_attributes(self, selector, attribute, value):
"""This method uses JavaScript to set/update a common attribute.
All matching selectors from querySelectorAll() are used.
Example => (Make all links on a website redirect to Google):
self.set_attributes("a", "href", "https://google.com")"""
attribute = re.escape(attribute)
attribute = js_utils.escape_quotes_if_needed(attribute)
value = re.escape(value)
value = js_utils.escape_quotes_if_needed(value)
css_selector = self.__convert_to_css_if_xpath(selector)
css_selector = re.escape(css_selector) # Add "\\" to special chars
css_selector = js_utils.escape_quotes_if_needed(css_selector)
js_code = """var $elements = document.querySelectorAll('%s');
var index = 0, length = $elements.length;
for(; index < length; index++){
$elements[index].setAttribute('%s','%s');}""" % (
css_selector,
attribute,
value,
)
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
def __make_sure_pyautogui_lock_is_writable(self):
with suppress(Exception):
shared_utils.make_writable(constants.MultiBrowser.PYAUTOGUILOCK)
def __verify_pyautogui_has_a_headed_browser(self):
"""PyAutoGUI requires a headed browser so that it can
focus on the correct element when performing actions."""
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
if driver.config.headless:
raise Exception(
"PyAutoGUI can't be used in headless mode!"
)
def __install_pyautogui_if_missing(self):
self.__verify_pyautogui_has_a_headed_browser()
driver = self.driver
if hasattr(driver, "cdp_base"):
driver = driver.cdp_base
pip_find_lock = fasteners.InterProcessLock(
constants.PipInstall.FINDLOCK
)
with pip_find_lock: # Prevent issues with multiple processes
with suppress(Exception):
shared_utils.make_writable(constants.PipInstall.FINDLOCK)
try:
import pyautogui
with suppress(Exception):
use_pyautogui_ver = constants.PyAutoGUI.VER
if pyautogui.__version__ != use_pyautogui_ver:
del pyautogui
shared_utils.pip_install(
"pyautogui", version=use_pyautogui_ver
)
import pyautogui
except Exception:
print("\nPyAutoGUI required! Installing now...")
shared_utils.pip_install(
"pyautogui", version=constants.PyAutoGUI.VER
)
try:
import pyautogui
except Exception:
if (
shared_utils.is_linux()
and (not sb_config.headed or sb_config.xvfb)
and not driver.config.headless
):
from sbvirtualdisplay import Display
xvfb_width = 1366
xvfb_height = 768
if (
hasattr(sb_config, "_xvfb_width")
and sb_config._xvfb_width
and isinstance(sb_config._xvfb_width, int)
and hasattr(sb_config, "_xvfb_height")
and sb_config._xvfb_height
and isinstance(sb_config._xvfb_height, int)
):
xvfb_width = sb_config._xvfb_width
xvfb_height = sb_config._xvfb_height
if xvfb_width < 1024:
xvfb_width = 1024
sb_config._xvfb_width = xvfb_width
if xvfb_height < 768:
xvfb_height = 768
sb_config._xvfb_height = xvfb_height
with suppress(Exception):
xvfb_display = Display(
visible=True,
size=(xvfb_width, xvfb_height),
backend="xvfb",
use_xauth=True,
)
xvfb_display.start()
def __get_configured_pyautogui(self, pyautogui_copy):
if (
shared_utils.is_linux()
and hasattr(pyautogui_copy, "_pyautogui_x11")
and "DISPLAY" in os.environ.keys()
):
if (
hasattr(sb_config, "_pyautogui_x11_display")
and sb_config._pyautogui_x11_display
and hasattr(pyautogui_copy._pyautogui_x11, "_display")
and (
sb_config._pyautogui_x11_display
== pyautogui_copy._pyautogui_x11._display
)
):
pass
else:
import Xlib.display
pyautogui_copy._pyautogui_x11._display = (
Xlib.display.Display(os.environ['DISPLAY'])
)
sb_config._pyautogui_x11_display = (
pyautogui_copy._pyautogui_x11._display
)
return pyautogui_copy
def gui_press_key(self, key):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock:
self.__make_sure_pyautogui_lock_is_writable()
pyautogui.press(key)
time.sleep(0.044)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def gui_press_keys(self, keys):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock:
self.__make_sure_pyautogui_lock_is_writable()
for key in keys:
pyautogui.press(key)
time.sleep(0.044)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def gui_write(self, text):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock:
self.__make_sure_pyautogui_lock_is_writable()
pyautogui.write(text)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.sleep(0.025))
def __gui_click_x_y(self, x, y, timeframe=0.25, uc_lock=False):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
screen_width, screen_height = pyautogui.size()
if x < 0 or y < 0 or x > screen_width or y > screen_height:
raise Exception(
"PyAutoGUI cannot click on point (%s, %s)"
" outside screen. (Width: %s, Height: %s)"
% (x, y, screen_width, screen_height)
)
if uc_lock:
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
self.__make_sure_pyautogui_lock_is_writable()
pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
if timeframe >= 0.25:
time.sleep(0.056) # Wait if moving at human-speed
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y))
pyautogui.click(x=x, y=y)
else:
# Called from a method where the gui_lock is already active
pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
if timeframe >= 0.25:
time.sleep(0.056) # Wait if moving at human-speed
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.click(%s, %s)" % (x, y))
pyautogui.click(x=x, y=y)
def gui_click_x_y(self, x, y, timeframe=0.25):
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
self.__make_sure_pyautogui_lock_is_writable()
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
width_ratio = 1.0
if shared_utils.is_windows():
window_rect = self.get_window_rect()
width = window_rect["width"]
height = window_rect["height"]
win_x = window_rect["x"]
win_y = window_rect["y"]
scr_width = pyautogui.size().width
self.maximize()
self.__add_light_pause()
win_width = self.get_window_size()["width"]
width_ratio = round(float(scr_width) / float(win_width), 2)
width_ratio += 0.01
if width_ratio < 0.45 or width_ratio > 2.55:
width_ratio = 1.01
sb_config._saved_width_ratio = width_ratio
self.minimize()
self.__add_light_pause()
self.set_window_rect(win_x, win_y, width, height)
self.__add_light_pause()
x = x * width_ratio
y = y * width_ratio
self.bring_active_window_to_front()
self.__gui_click_x_y(x, y, timeframe=timeframe, uc_lock=False)
def gui_click_element(self, selector, timeframe=0.25):
self.__slow_mode_pause_if_set()
x, y = self.get_gui_element_center(selector)
self.__add_light_pause()
self.gui_click_x_y(x, y, timeframe=timeframe)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def __gui_drag_drop(self, x1, y1, x2, y2, timeframe=0.25, uc_lock=False):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
screen_width, screen_height = pyautogui.size()
if x1 < 0 or y1 < 0 or x1 > screen_width or y1 > screen_height:
raise Exception(
"PyAutoGUI cannot drag-drop from point (%s, %s)"
" outside screen. (Width: %s, Height: %s)"
% (x1, y1, screen_width, screen_height)
)
if x2 < 0 or y2 < 0 or x2 > screen_width or y2 > screen_height:
raise Exception(
"PyAutoGUI cannot drag-drop to point (%s, %s)"
" outside screen. (Width: %s, Height: %s)"
% (x2, y2, screen_width, screen_height)
)
if uc_lock:
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad)
self.__add_light_pause()
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x1, y1))
pyautogui.dragTo(x2, y2, button="left", duration=timeframe)
else:
# Called from a method where the gui_lock is already active
pyautogui.moveTo(x1, y1, 0.25, pyautogui.easeOutQuad)
self.__add_light_pause()
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.dragTo(%s, %s)" % (x2, y2))
pyautogui.dragTo(x2, y2, button="left", duration=timeframe)
def gui_drag_drop_points(self, x1, y1, x2, y2, timeframe=0.35):
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
width_ratio = 1.0
if shared_utils.is_windows():
window_rect = self.get_window_rect()
width = window_rect["width"]
height = window_rect["height"]
win_x = window_rect["x"]
win_y = window_rect["y"]
scr_width = pyautogui.size().width
self.maximize()
self.__add_light_pause()
win_width = self.get_window_size()["width"]
width_ratio = round(float(scr_width) / float(win_width), 2)
width_ratio += 0.01
if width_ratio < 0.45 or width_ratio > 2.55:
width_ratio = 1.01
sb_config._saved_width_ratio = width_ratio
self.minimize()
self.__add_light_pause()
self.set_window_rect(win_x, win_y, width, height)
self.__add_light_pause()
x1 = x1 * width_ratio
y1 = y1 * width_ratio
x2 = x2 * width_ratio
y2 = y2 * width_ratio
self.bring_active_window_to_front()
self.__gui_drag_drop(
x1, y1, x2, y2, timeframe=timeframe, uc_lock=False
)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def gui_drag_and_drop(self, drag_selector, drop_selector, timeframe=0.35):
self.__slow_mode_pause_if_set()
self.bring_active_window_to_front()
x1, y1 = self.get_gui_element_center(drag_selector)
self.__add_light_pause()
x2, y2 = self.get_gui_element_center(drop_selector)
self.__add_light_pause()
self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=timeframe)
def __gui_hover_x_y(self, x, y, timeframe=0.25, uc_lock=False):
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
screen_width, screen_height = pyautogui.size()
if x < 0 or y < 0 or x > screen_width or y > screen_height:
raise Exception(
"PyAutoGUI cannot hover on point (%s, %s)"
" outside screen. (Width: %s, Height: %s)"
% (x, y, screen_width, screen_height)
)
if uc_lock:
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
time.sleep(0.056)
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y))
else:
# Called from a method where the gui_lock is already active
pyautogui.moveTo(x, y, timeframe, pyautogui.easeOutQuad)
time.sleep(0.056)
if "--debug" in sys.argv:
print(" <DEBUG> pyautogui.moveTo(%s, %s)" % (x, y))
def gui_hover_x_y(self, x, y, timeframe=0.25):
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock: # Prevent issues with multiple processes
self.__install_pyautogui_if_missing()
import pyautogui
pyautogui = self.__get_configured_pyautogui(pyautogui)
width_ratio = 1.0
if (
shared_utils.is_windows()
and (
not hasattr(sb_config, "_saved_width_ratio")
or not sb_config._saved_width_ratio
)
):
window_rect = self.get_window_rect()
width = window_rect["width"]
height = window_rect["height"]
win_x = window_rect["x"]
win_y = window_rect["y"]
if (
hasattr(sb_config, "_saved_width_ratio")
and sb_config._saved_width_ratio
):
width_ratio = sb_config._saved_width_ratio
else:
scr_width = pyautogui.size().width
self.maximize()
self.__add_light_pause()
win_width = self.get_window_size()["width"]
width_ratio = round(float(scr_width) / float(win_width), 2)
width_ratio += 0.01
if width_ratio < 0.45 or width_ratio > 2.55:
width_ratio = 1.01
sb_config._saved_width_ratio = width_ratio
self.set_window_rect(win_x, win_y, width, height)
self.__add_light_pause()
self.bring_active_window_to_front()
elif (
shared_utils.is_windows()
and hasattr(sb_config, "_saved_width_ratio")
and sb_config._saved_width_ratio
):
width_ratio = sb_config._saved_width_ratio
self.bring_active_window_to_front()
if shared_utils.is_windows():
x = x * width_ratio
y = y * width_ratio
self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False)
return
self.bring_active_window_to_front()
self.__gui_hover_x_y(x, y, timeframe=timeframe, uc_lock=False)
def gui_hover_element(self, selector, timeframe=0.25):
self.__slow_mode_pause_if_set()
element_rect = self.get_gui_element_rect(selector)
width = element_rect["width"]
height = element_rect["height"]
if width > 0 and height > 0:
x, y = self.get_gui_element_center(selector)
self.bring_active_window_to_front()
self.__gui_hover_x_y(x, y, timeframe=timeframe)
self.__slow_mode_pause_if_set()
self.loop.run_until_complete(self.page.wait())
def gui_hover_and_click(self, hover_selector, click_selector):
gui_lock = fasteners.InterProcessLock(
constants.MultiBrowser.PYAUTOGUILOCK
)
with gui_lock:
self.__make_sure_pyautogui_lock_is_writable()
self.bring_active_window_to_front()
self.gui_hover_element(hover_selector)
time.sleep(0.15)
self.gui_hover_element(click_selector)
self.click(click_selector)
def internalize_links(self):
"""All `target="_blank"` links become `target="_self"`.
This prevents those links from opening in a new tab."""
self.set_attributes('[target="_blank"]', "target", "_self")
def is_checked(self, selector):
"""Return True if checkbox (or radio button) is checked."""
selector = self.__convert_to_css_if_xpath(selector)
self.find_element(selector, timeout=settings.SMALL_TIMEOUT)
return bool(self.get_element_attribute(selector, "checked"))
def is_selected(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
return self.is_checked(selector)
def check_if_unchecked(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
if not self.is_checked(selector):
self.click(selector)
def select_if_unselected(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
self.check_if_unchecked(selector)
def uncheck_if_checked(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
if self.is_checked(selector):
self.click(selector)
def unselect_if_selected(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
self.uncheck_if_checked(selector)
def is_element_present(self, selector):
try:
self.select(selector, timeout=0.01)
return True
except Exception:
return False
def is_element_visible(self, selector):
selector = self.__convert_to_css_if_xpath(selector)
element = None
if ":contains(" not in selector:
try:
element = self.select(selector, timeout=0.01)
except Exception:
return False
if not element:
return False
try:
position = element.get_position()
return (position.width != 0 or position.height != 0)
except Exception:
return False
else:
with suppress(Exception):
tag_name = selector.split(":contains(")[0].split(" ")[-1]
text = selector.split(":contains(")[1].split(")")[0][1:-1]
self.loop.run_until_complete(
self.page.select(tag_name, timeout=0.1)
)
self.loop.run_until_complete(self.page.find(text, timeout=0.1))
return True
return False
def is_text_visible(self, text, selector="body"):
selector = self.__convert_to_css_if_xpath(selector)
text = text.strip()
element = None
try:
element = self.find_element(selector, timeout=0.1)
except Exception:
return False
with suppress(Exception):
if text in element.text_all:
return True
return False
def is_exact_text_visible(self, text, selector="body"):
selector = self.__convert_to_css_if_xpath(selector)
text = text.strip()
element = None
try:
element = self.find_element(selector, timeout=0.1)
except Exception:
return False
with suppress(Exception):
if text == element.text_all.strip():
return True
return False
def wait_for_text(self, text, selector="body", timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
start_ms = time.time() * 1000.0
stop_ms = start_ms + (timeout * 1000.0)
text = text.strip()
element = None
try:
element = self.find_element(selector, timeout=timeout)
except Exception:
raise Exception("Element {%s} not found!" % selector)
for i in range(int(timeout * 10)):
with suppress(Exception):
element = self.find_element(selector, timeout=0.1)
if text in element.text_all:
return True
now_ms = time.time() * 1000.0
if now_ms >= stop_ms:
break
time.sleep(0.1)
raise Exception(
"Text {%s} not found in {%s}! Actual text: {%s}"
% (text, selector, element.text_all)
)
def wait_for_text_not_visible(self, text, selector="body", timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
text = text.strip()
start_ms = time.time() * 1000.0
stop_ms = start_ms + (timeout * 1000.0)
for i in range(int(timeout * 10)):
if not self.is_text_visible(text, selector):
return True
now_ms = time.time() * 1000.0
if now_ms >= stop_ms:
break
time.sleep(0.1)
plural = "s"
if timeout == 1:
plural = ""
raise Exception(
"Text {%s} in {%s} was still visible after %s second%s!"
% (text, selector, timeout, plural)
)
def wait_for_element_visible(self, selector, timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
try:
self.select(selector, timeout=timeout)
except Exception:
raise Exception("Element {%s} was not found!" % selector)
for i in range(30):
if self.is_element_visible(selector):
return self.select(selector)
time.sleep(0.1)
raise Exception("Element {%s} was not visible!" % selector)
def wait_for_element_not_visible(self, selector, timeout=None):
"""Wait for element to not be visible on page. (May still be in DOM)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
start_ms = time.time() * 1000.0
stop_ms = start_ms + (timeout * 1000.0)
for i in range(int(timeout * 10)):
if not self.is_element_present(selector):
return True
elif not self.is_element_visible(selector):
return True
now_ms = time.time() * 1000.0
if now_ms >= stop_ms:
break
time.sleep(0.1)
plural = "s"
if timeout == 1:
plural = ""
raise Exception(
"Element {%s} was still visible after %s second%s!"
% (selector, timeout, plural)
)
def wait_for_element_absent(self, selector, timeout=None):
"""Wait for element to not be present in the DOM."""
if not timeout:
timeout = settings.SMALL_TIMEOUT
start_ms = time.time() * 1000.0
stop_ms = start_ms + (timeout * 1000.0)
for i in range(int(timeout * 10)):
if not self.is_element_present(selector):
return True
now_ms = time.time() * 1000.0
if now_ms >= stop_ms:
break
time.sleep(0.1)
plural = "s"
if timeout == 1:
plural = ""
raise Exception(
"Element {%s} was still present after %s second%s!"
% (selector, timeout, plural)
)
def assert_element(self, selector, timeout=None):
"""Same as assert_element_visible()"""
self.assert_element_visible(selector, timeout=timeout)
return True
def assert_element_visible(self, selector, timeout=None):
"""Same as assert_element()"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
try:
self.select(selector, timeout=timeout)
except Exception:
raise Exception("Element {%s} was not found!" % selector)
for i in range(30):
if self.is_element_visible(selector):
return True
time.sleep(0.1)
raise Exception("Element {%s} was not visible!" % selector)
def assert_element_present(self, selector, timeout=None):
"""Assert element is present in the DOM. (Visibility NOT required)"""
if not timeout:
timeout = settings.SMALL_TIMEOUT
try:
self.select(selector, timeout=timeout)
except Exception:
raise Exception("Element {%s} was not found!" % selector)
return True
def assert_element_absent(self, selector, timeout=None):
"""Assert element is not present in the DOM."""
self.wait_for_element_absent(selector, timeout=timeout)
return True
def assert_element_not_visible(self, selector, timeout=None):
"""Assert element is not visible on page. (May still be in DOM)"""
self.wait_for_element_not_visible(selector, timeout=timeout)
return True
def assert_element_attribute(self, selector, attribute, value=None):
attributes = self.get_element_attributes(selector)
if attribute not in attributes:
raise Exception(
"Attribute {%s} was not found in element {%s}!"
% (attribute, selector)
)
if value and attributes[attribute] != value:
raise Exception(
"Expected value {%s} of attribute {%s} "
"was not found in element {%s}! "
"(Actual value was {%s})"
% (value, attribute, selector, attributes[attribute])
)
def assert_title(self, title):
expected = title.strip()
actual = self.get_title().strip()
error = (
"Expected page title [%s] does not match the actual title [%s]!"
)
try:
if expected != actual:
raise Exception(error % (expected, actual))
except Exception:
time.sleep(2)
actual = self.get_title().strip()
if expected != actual:
raise Exception(error % (expected, actual))
def assert_title_contains(self, substring):
expected = substring.strip()
actual = self.get_title().strip()
error = (
"Expected title substring [%s] does not appear "
"in the actual page title [%s]!"
)
try:
if expected not in actual:
raise Exception(error % (expected, actual))
except Exception:
time.sleep(2)
actual = self.get_title().strip()
if expected not in actual:
raise Exception(error % (expected, actual))
def assert_url(self, url):
expected = url.strip()
actual = self.get_current_url().strip()
error = "Expected URL [%s] does not match the actual URL [%s]!"
try:
if expected != actual:
raise Exception(error % (expected, actual))
except Exception:
time.sleep(2)
actual = self.get_current_url().strip()
if expected != actual:
raise Exception(error % (expected, actual))
def assert_url_contains(self, substring):
expected = substring.strip()
actual = self.get_current_url().strip()
error = (
"Expected URL substring [%s] does not appear "
"in the full URL [%s]!"
)
try:
if expected not in actual:
raise Exception(error % (expected, actual))
except Exception:
time.sleep(2)
actual = self.get_current_url().strip()
if expected not in actual:
raise Exception(error % (expected, actual))
def assert_text(self, text, selector="body", timeout=None):
"""Same as wait_for_text()"""
self.wait_for_text(text, selector=selector, timeout=timeout)
return True
def assert_exact_text(self, text, selector="body", timeout=None):
if not timeout:
timeout = settings.SMALL_TIMEOUT
start_ms = time.time() * 1000.0
stop_ms = start_ms + (timeout * 1000.0)
text = text.strip()
element = None
try:
element = self.select(selector, timeout=timeout)
except Exception:
raise Exception("Element {%s} not found!" % selector)
for i in range(int(timeout * 10)):
with suppress(Exception):
element = self.select(selector, timeout=0.1)
if (
self.is_element_visible(selector)
and text.strip() == element.text_all.strip()
):
return True
now_ms = time.time() * 1000.0
if now_ms >= stop_ms:
break
time.sleep(0.1)
raise Exception(
"Expected Text {%s}, is not equal to {%s} in {%s}!"
% (text, element.text_all, selector)
)
def assert_text_not_visible(self, text, selector="body", timeout=None):
"""Raises an exception if the text is still visible after timeout."""
self.wait_for_text_not_visible(
text, selector=selector, timeout=timeout
)
return True
def assert_true(self, expression):
if not expression:
raise AssertionError("%s is not true" % expression)
def assert_false(self, expression):
if expression:
raise AssertionError("%s is not false" % expression)
def assert_equal(self, first, second):
if first != second:
raise AssertionError("%s is not equal to %s" % (first, second))
def assert_not_equal(self, first, second):
if first == second:
raise AssertionError("%s is equal to %s" % (first, second))
def assert_in(self, first, second):
if first not in second:
raise AssertionError("%s is not in %s" % (first, second))
def assert_not_in(self, first, second):
if first in second:
raise AssertionError("%s is in %s" % (first, second))
def scroll_into_view(self, selector):
self.find_element(selector).scroll_into_view()
self.loop.run_until_complete(self.page.wait())
def scroll_to_y(self, y):
y = int(y)
js_code = "window.scrollTo(0, %s);" % y
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
self.loop.run_until_complete(self.page.wait())
def scroll_to_top(self):
js_code = "window.scrollTo(0, 0);"
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
self.loop.run_until_complete(self.page.wait())
def scroll_to_bottom(self):
js_code = "window.scrollTo(0, 10000);"
with suppress(Exception):
self.loop.run_until_complete(self.page.evaluate(js_code))
self.loop.run_until_complete(self.page.wait())
def scroll_up(self, amount=25):
self.loop.run_until_complete(self.page.scroll_up(amount))
self.loop.run_until_complete(self.page.wait())
def scroll_down(self, amount=25):
self.loop.run_until_complete(self.page.scroll_down(amount))
self.loop.run_until_complete(self.page.wait())
def save_screenshot(self, name, folder=None, selector=None):
filename = name
if folder:
filename = os.path.join(folder, name)
if not selector:
self.loop.run_until_complete(
self.page.save_screenshot(filename)
)
else:
self.select(selector).save_screenshot(filename)
def print_to_pdf(self, name, folder=None):
filename = name
if folder:
filename = os.path.join(folder, name)
self.loop.run_until_complete(self.page.print_to_pdf(filename))
class Chrome(CDPMethods):
def __init__(self, url=None, **kwargs):
if not url:
url = "about:blank"
loop = asyncio.new_event_loop()
driver = cdp_util.start_sync(**kwargs)
page = loop.run_until_complete(driver.get(url))
super().__init__(loop, page, driver)