16780 lines
709 KiB
Python
16780 lines
709 KiB
Python
r"""----------------------------------------------------------------->
|
||
| ______ __ _ ____ |
|
||
| / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ |
|
||
| \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \ / /_) / __ \/ ___/ _ \ |
|
||
| ___/ / __/ / __/ / / / / /_/ / / / / / // /_) / (_/ /__ / __/ |
|
||
| /____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_//_____/\__,_/____/\___/ |
|
||
| |
|
||
--------------------------------------------------------------------->
|
||
|
||
The BaseCase class is the main gateway for using The SeleniumBase Framework.
|
||
It inherits Python's unittest.TestCase class and runs with pytest or pynose.
|
||
All tests using BaseCase automatically launch WebDriver browsers for tests.
|
||
|
||
Example Test:
|
||
|
||
# --------------------------------------------------------------
|
||
from seleniumbase import BaseCase
|
||
BaseCase.main(__name__, __file__)
|
||
class MyTestClass(BaseCase):
|
||
def test_anything(self):
|
||
# Write your code here. Example:
|
||
self.open("https://github.com/")
|
||
self.click('span[data-target*="inputButtonText"]')
|
||
self.type("input#query-builder-test", "SeleniumBase\n")
|
||
self.click('a[href="/seleniumbase/SeleniumBase"]')
|
||
self.assert_element("div.repository-content")
|
||
self.assert_text("SeleniumBase", "strong a")
|
||
# --------------------------------------------------------------
|
||
|
||
SeleniumBase methods expand and improve on existing WebDriver commands.
|
||
Improvements include making WebDriver more robust, reliable, and flexible.
|
||
Page elements are given enough time to load before WebDriver acts on them.
|
||
Code becomes greatly simplified and easier to maintain."""
|
||
|
||
import codecs
|
||
import colorama
|
||
import fasteners
|
||
import json
|
||
import logging
|
||
import math
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import textwrap
|
||
import time
|
||
import unittest
|
||
import urllib3
|
||
from contextlib import contextmanager, suppress
|
||
from selenium.common.exceptions import (
|
||
ElementClickInterceptedException as ECI_Exception,
|
||
ElementNotInteractableException as ENI_Exception,
|
||
InvalidArgumentException,
|
||
MoveTargetOutOfBoundsException,
|
||
NoSuchElementException,
|
||
NoSuchWindowException,
|
||
StaleElementReferenceException as Stale_Exception,
|
||
TimeoutException,
|
||
WebDriverException,
|
||
)
|
||
from selenium.webdriver.common.action_chains import ActionChains
|
||
from selenium.webdriver.common.by import By
|
||
from selenium.webdriver.common.keys import Keys
|
||
from selenium.webdriver.remote.remote_connection import LOGGER
|
||
from selenium.webdriver.remote.webelement import WebElement
|
||
from seleniumbase import config as sb_config
|
||
from seleniumbase.__version__ import __version__
|
||
from seleniumbase.common import decorators
|
||
from seleniumbase.common.exceptions import (
|
||
NotConnectedException,
|
||
NotUsingChromeException,
|
||
NotUsingChromiumException,
|
||
ProxyConnectionException,
|
||
OutOfScopeException,
|
||
VisualException,
|
||
)
|
||
from seleniumbase.config import settings
|
||
from seleniumbase.core import browser_launcher
|
||
from seleniumbase.core import download_helper
|
||
from seleniumbase.core import log_helper
|
||
from seleniumbase.core import session_helper
|
||
from seleniumbase.core import visual_helper
|
||
from seleniumbase.fixtures import constants
|
||
from seleniumbase.fixtures import css_to_xpath
|
||
from seleniumbase.fixtures import js_utils
|
||
from seleniumbase.fixtures import page_actions
|
||
from seleniumbase.fixtures import page_utils
|
||
from seleniumbase.fixtures import shared_utils
|
||
from seleniumbase.fixtures import unittest_helper
|
||
from seleniumbase.fixtures import xpath_to_css
|
||
|
||
__all__ = ["BaseCase"]
|
||
|
||
logging.getLogger("requests").setLevel(logging.ERROR)
|
||
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
||
urllib3.disable_warnings()
|
||
LOGGER.setLevel(logging.WARNING)
|
||
is_linux = shared_utils.is_linux()
|
||
is_windows = shared_utils.is_windows()
|
||
python3_11_or_newer = False
|
||
if sys.version_info >= (3, 11):
|
||
python3_11_or_newer = True
|
||
py311_patch2 = constants.PatchPy311.PATCH
|
||
|
||
|
||
class BaseCase(unittest.TestCase):
|
||
"""<Class seleniumbase.BaseCase>"""
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
self.__initialize_variables()
|
||
|
||
def __initialize_variables(self):
|
||
self.driver = None
|
||
self.environment = None
|
||
self.env = None # Add a shortened version of self.environment
|
||
self.version_list = [
|
||
int(i) for i in __version__.split(".") if i.isdigit()
|
||
]
|
||
self.version_tuple = tuple(self.version_list)
|
||
self.version_info = self.version_tuple
|
||
self.time = time.time
|
||
self.__page_sources = []
|
||
self.__extra_actions = []
|
||
self.__js_start_time = 0
|
||
self.__set_c_from_switch = False
|
||
self.__frame_switch_layer = 0 # Used by Recorder-Mode
|
||
self.__frame_switch_multi = False # Used by Recorder-Mode
|
||
self.__last_saved_url = None # Used by Recorder-Mode
|
||
self.__uc_frame_layer = 0
|
||
self.__called_setup = False
|
||
self.__called_teardown = False
|
||
self.__start_time_ms = int(time.time() * 1000.0)
|
||
self.__requests_timeout = None
|
||
self.__screenshot_count = 0
|
||
self.__logs_data_count = 0
|
||
self.__last_data_file = None
|
||
self.__level_0_visual_f = False
|
||
self.__will_be_skipped = False
|
||
self.__passed_then_skipped = False
|
||
self.__visual_baseline_copies = []
|
||
self.__last_url_of_deferred_assert = "about:blank"
|
||
self.__last_page_load_url = "about:blank"
|
||
self.__last_page_screenshot = None
|
||
self.__last_page_screenshot_png = None
|
||
self.__last_page_url = None
|
||
self.__last_page_source = None
|
||
self.__skip_reason = None
|
||
self.__origins_to_save = []
|
||
self.__actions_to_save = []
|
||
self.__dont_record_open = False
|
||
self.__dont_record_js_click = False
|
||
self.__new_window_on_rec_open = True
|
||
self.__overrided_default_timeouts = False
|
||
self.__added_pytest_html_extra = None
|
||
self.__deferred_assert_count = 0
|
||
self.__deferred_assert_failures = []
|
||
self.__device_width = None
|
||
self.__device_height = None
|
||
self.__device_pixel_ratio = None
|
||
self.__changed_jqc_theme = False
|
||
self.__jqc_default_theme = None
|
||
self.__jqc_default_color = None
|
||
self.__jqc_default_width = None
|
||
# Requires self._* instead of self.__* for external class use
|
||
self._language = "English"
|
||
self._presentation_slides = {}
|
||
self._presentation_transition = {}
|
||
self._output_file_saves = True # For Presenter / ChartMaker
|
||
self._rec_overrides_switch = True # Recorder-Mode uses set_c vs switch
|
||
self._sb_test_identifier = None
|
||
self._html_report_extra = [] # (Used by pytest_plugin.py)
|
||
self._last_page_screenshot = None
|
||
self._last_page_url = None
|
||
self._final_debug = None
|
||
self._default_driver = None
|
||
self._drivers_list = []
|
||
self._drivers_browser_map = {}
|
||
self._was_skipped = False
|
||
self._chart_data = {}
|
||
self._chart_count = 0
|
||
self._chart_label = {}
|
||
self._chart_xcount = 0
|
||
self._chart_first_series = {}
|
||
self._chart_series_count = {}
|
||
self._tour_steps = {}
|
||
self._xvfb_display = None
|
||
self._xvfb_width = None
|
||
self._xvfb_height = None
|
||
|
||
@classmethod
|
||
def main(self, name, file, *args):
|
||
"""Run pytest if file was called with "python".
|
||
Usage example:
|
||
|
||
from seleniumbase import BaseCase
|
||
BaseCase.main(__name__, __file__)
|
||
|
||
class MyTestClass(BaseCase):
|
||
def test_example(self):
|
||
pass
|
||
|
||
The run command:
|
||
python my_test.py # (Instead of "pytest my_test.py")
|
||
|
||
This is useful when sharing code with people who may not be aware
|
||
that SeleniumBase tests are run with "pytest" instead of "python".
|
||
Now, if they accidentally type "python", the tests will still run.
|
||
Eg. "python my_test.py" instead of "pytest my_test.py"."""
|
||
if name == "__main__": # Test called with "python"
|
||
import subprocess
|
||
all_args = []
|
||
for arg in args:
|
||
all_args.append(arg)
|
||
for arg in sys.argv[1:]:
|
||
all_args.append(arg)
|
||
# See: https://stackoverflow.com/a/54666289/7058266
|
||
# from pytest import main as pytest_main
|
||
# pytest_main([file, "-s", *all_args])
|
||
subprocess.call(
|
||
[sys.executable, "-m", "pytest", file, "-s", *all_args]
|
||
)
|
||
|
||
def open(self, url):
|
||
"""Navigates the current browser window to the specified page."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.open(url)
|
||
return
|
||
self._check_browser()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
url = str(url).strip() # Remove leading and trailing whitespace
|
||
if not self.__looks_like_a_page_url(url):
|
||
# url should start with one of the following:
|
||
# "http:", "https:", "://", "data:", "file:",
|
||
# "about:", "chrome:", or "edge:".
|
||
if page_utils.is_valid_url("https://" + url):
|
||
url = "https://" + url
|
||
else:
|
||
raise Exception('Invalid URL: "%s"!' % url)
|
||
self.__last_page_load_url = None
|
||
js_utils.clear_out_console_logs(self.driver)
|
||
if url.startswith("://"):
|
||
# Convert URLs such as "://google.com" into "https://google.com"
|
||
url = "https" + url
|
||
if self.recorder_mode and not self.__dont_record_open:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["o_url", origin, url, str(int(time_stamp) - 1)]
|
||
self.__extra_actions.append(action)
|
||
action = ["_url_", origin, url, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if self.recorder_mode and self.__new_window_on_rec_open:
|
||
c_url = self.driver.current_url
|
||
if ("http:") in c_url or ("https:") in c_url or ("file:") in c_url:
|
||
if self.get_domain_url(url) != self.get_domain_url(c_url):
|
||
self.open_new_window(switch_to=True)
|
||
try:
|
||
self.driver.get(url)
|
||
except Exception as e:
|
||
if not hasattr(e, "msg") and hasattr(self.driver, "default_get"):
|
||
try:
|
||
self._check_browser()
|
||
time.sleep(0.4)
|
||
except Exception:
|
||
logging.debug("Browser crashed! Will open new browser!")
|
||
self.driver = self.get_new_driver()
|
||
self.driver.default_get(url)
|
||
elif (
|
||
"ERR_CONNECTION_TIMED_OUT" in e.msg
|
||
or "ERR_CONNECTION_CLOSED" in e.msg
|
||
or "ERR_CONNECTION_RESET" in e.msg
|
||
or "ERR_NAME_NOT_RESOLVED" in e.msg
|
||
):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
self._check_browser()
|
||
time.sleep(0.8)
|
||
self.driver.get(url)
|
||
elif (
|
||
"ERR_INTERNET_DISCONNECTED" in e.msg
|
||
or "neterror?e=dnsNotFound" in e.msg
|
||
or "ERR_PROXY_CONNECTION_FAILED" in e.msg
|
||
):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
self._check_browser()
|
||
time.sleep(1.05)
|
||
try:
|
||
self.driver.get(url)
|
||
except Exception as e2:
|
||
if (
|
||
"ERR_INTERNET_DISCONNECTED" in e2.msg
|
||
or "neterror?e=dnsNotFound" in e2.msg
|
||
):
|
||
message = "ERR_INTERNET_DISCONNECTED: "
|
||
message += "Internet unreachable!"
|
||
raise NotConnectedException(message)
|
||
elif "ERR_PROXY_CONNECTION_FAILED" in e2.msg:
|
||
message = "ERR_PROXY_CONNECTION_FAILED: "
|
||
message += "Internet unreachable and/or invalid proxy!"
|
||
raise ProxyConnectionException(message)
|
||
else:
|
||
raise
|
||
elif "Timed out receiving message from renderer" in e.msg:
|
||
page_load_timeout = self.driver.timeouts.page_load
|
||
logging.info(
|
||
"The page load timed out after %s seconds! Will retry..."
|
||
% page_load_timeout
|
||
)
|
||
try:
|
||
self.driver.get(url)
|
||
except Exception as e:
|
||
if "Timed out receiving message from renderer" in e.msg:
|
||
raise Exception(
|
||
"Retry of page load timed out after %s seconds!"
|
||
% page_load_timeout
|
||
)
|
||
else:
|
||
raise
|
||
elif (
|
||
"cannot determine loading status" in e.msg
|
||
or "unexpected command response" in e.msg
|
||
):
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.2)
|
||
self.driver.get(url)
|
||
else:
|
||
pass # Odd issue where the open did happen. Continue.
|
||
elif "invalid session id" in e.msg:
|
||
logging.debug("Invalid session id. Will open new browser.")
|
||
self.driver = self.get_new_driver()
|
||
self.driver.get(url)
|
||
else:
|
||
raise
|
||
try:
|
||
if (
|
||
self.driver.current_url == pre_action_url
|
||
and pre_action_url != url
|
||
):
|
||
time.sleep(0.1) # Make sure load happens
|
||
except Exception:
|
||
time.sleep(0.1) # First see if waiting helps
|
||
try:
|
||
self._check_browser()
|
||
if not self.driver.current_url:
|
||
raise Exception("No current URL!")
|
||
except Exception:
|
||
# Spin up a new driver with the URL
|
||
self.driver = self.get_new_driver()
|
||
self.driver.get(url)
|
||
self._check_browser()
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
if not self.undetectable:
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.08) # Force a minimum wait, even if skipping waits.
|
||
if self.undetectable:
|
||
self.__uc_frame_layer = 0
|
||
if self.demo_mode:
|
||
if self.driver.current_url.startswith(("http", "file", "data")):
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
with suppress(Exception):
|
||
js_utils.add_js_link(
|
||
self.driver, constants.JQuery.MIN_JS
|
||
)
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def get(self, url):
|
||
"""If "url" looks like a page URL, open the URL in the web browser.
|
||
Otherwise, return self.get_element(URL_AS_A_SELECTOR)
|
||
Examples:
|
||
self.get("https://seleniumbase.io") # Navigates to the URL
|
||
self.get("input.class") # Finds and returns the WebElement """
|
||
self.__check_scope()
|
||
if self.__looks_like_a_page_url(url):
|
||
self.open(url)
|
||
else:
|
||
return self.get_element(url) # url is treated like a selector
|
||
|
||
def click(
|
||
self, selector, by="css selector", timeout=None, delay=0, scroll=True
|
||
):
|
||
self.__check_scope()
|
||
self.__skip_if_esc()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
original_by = by
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click(selector, timeout=timeout)
|
||
return
|
||
if delay and (type(delay) in [int, float]) and delay > 0:
|
||
time.sleep(delay)
|
||
if page_utils.is_link_text_selector(selector) or by == By.LINK_TEXT:
|
||
if not self.is_link_text_visible(selector):
|
||
# Handle a special case of links hidden in dropdowns
|
||
self.click_link_text(selector, timeout=timeout)
|
||
return
|
||
if (
|
||
page_utils.is_partial_link_text_selector(selector)
|
||
or by == By.PARTIAL_LINK_TEXT
|
||
):
|
||
if not self.is_partial_link_text_visible(selector):
|
||
# Handle a special case of partial links hidden in dropdowns
|
||
self.click_partial_link_text(selector, timeout=timeout)
|
||
return
|
||
if self.__is_shadow_selector(selector):
|
||
self.__shadow_click(selector, timeout)
|
||
return
|
||
if self.__needs_minimum_wait() or self.browser == "safari":
|
||
time.sleep(0.05)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
if scroll and not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
try:
|
||
if (
|
||
by == By.LINK_TEXT
|
||
and (self.browser == "ie" or self.browser == "safari")
|
||
):
|
||
self.__jquery_click(selector, by=by)
|
||
elif self.browser == "safari":
|
||
self.execute_script("arguments[0].click();", element)
|
||
else:
|
||
href = None
|
||
new_tab = False
|
||
onclick = None
|
||
with suppress(Exception):
|
||
if self.headless and element.tag_name.lower() == "a":
|
||
# Handle a special case of opening a new tab (headless)
|
||
href = element.get_attribute("href").strip()
|
||
onclick = element.get_attribute("onclick")
|
||
target = element.get_attribute("target")
|
||
if target == "_blank":
|
||
new_tab = True
|
||
if new_tab and self.__looks_like_a_page_url(href):
|
||
if onclick:
|
||
with suppress(Exception):
|
||
self.execute_script(onclick)
|
||
current_window = self.driver.current_window_handle
|
||
self.open_new_window()
|
||
with suppress(Exception):
|
||
self.open(href)
|
||
self.switch_to_window(current_window)
|
||
return
|
||
# Normal click
|
||
self.__element_click(element)
|
||
except Stale_Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = page_actions.wait_for_element_clickable(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
with suppress(Exception):
|
||
self.__scroll_to_element(element, selector, by)
|
||
if self.browser == "safari" and by == By.LINK_TEXT:
|
||
self.__jquery_click(selector, by=by)
|
||
elif self.browser == "safari":
|
||
self.execute_script("arguments[0].click();", element)
|
||
else:
|
||
self.__element_click(element)
|
||
except ENI_Exception as e:
|
||
with suppress(Exception):
|
||
if (
|
||
"element has zero size" in e.msg
|
||
and element.tag_name.lower() == "a"
|
||
):
|
||
if "contains(" not in selector:
|
||
self.js_click(selector, by=by)
|
||
else:
|
||
self.jquery_click(selector, by=by)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.1)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
if not page_actions.is_element_clickable(
|
||
self.driver, selector, by
|
||
):
|
||
with suppress(Exception):
|
||
self.wait_for_element_clickable(
|
||
selector, by, timeout=1.8
|
||
)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
href = None
|
||
new_tab = False
|
||
onclick = None
|
||
with suppress(Exception):
|
||
if element.tag_name.lower() == "a":
|
||
# Handle a special case of opening a new tab (non-headless)
|
||
href = element.get_attribute("href").strip()
|
||
onclick = element.get_attribute("onclick")
|
||
target = element.get_attribute("target")
|
||
if target == "_blank":
|
||
new_tab = True
|
||
if new_tab and self.__looks_like_a_page_url(href):
|
||
if onclick:
|
||
with suppress(Exception):
|
||
self.execute_script(onclick)
|
||
current_window = self.driver.current_window_handle
|
||
self.open_new_window()
|
||
with suppress(Exception):
|
||
self.open(href)
|
||
self.switch_to_window(current_window)
|
||
return
|
||
if scroll and not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
if self.browser == "firefox" or self.browser == "safari":
|
||
if by == By.LINK_TEXT or "contains(" in selector:
|
||
self.__jquery_click(selector, by=by)
|
||
else:
|
||
self.__js_click(selector, by=by)
|
||
else:
|
||
try:
|
||
self.__element_click(element)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__element_click(element)
|
||
except MoveTargetOutOfBoundsException:
|
||
self.wait_for_ready_state_complete()
|
||
try:
|
||
self.__js_click(selector, by=by)
|
||
except Exception:
|
||
try:
|
||
self.__jquery_click(selector, by=by)
|
||
except Exception:
|
||
# One more attempt to click on the element
|
||
element = page_actions.wait_for_element_clickable(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__element_click(element)
|
||
except WebDriverException as e:
|
||
if (
|
||
"cannot determine loading status" in e.msg
|
||
or "unexpected command response" in e.msg
|
||
):
|
||
pass # Odd issue where the click did happen. Continue.
|
||
else:
|
||
self.wait_for_ready_state_complete()
|
||
try:
|
||
self.__js_click(selector, by=by)
|
||
except Exception:
|
||
try:
|
||
self.__jquery_click(selector, by=by)
|
||
except Exception:
|
||
# One more attempt to click on the element
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__element_click(element)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
elif (
|
||
latest_window_count == pre_window_count - 1
|
||
and latest_window_count > 0
|
||
):
|
||
# If a click closes the active window,
|
||
# switch to the last one if it exists.
|
||
self.switch_to_window(-1)
|
||
if settings.WAIT_FOR_RSC_ON_CLICKS:
|
||
if not self.undetectable:
|
||
with suppress(Exception):
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait() or self.browser == "safari":
|
||
time.sleep(0.05)
|
||
else:
|
||
time.sleep(0.08)
|
||
else:
|
||
if not self.undetectable:
|
||
# A smaller subset of self.wait_for_ready_state_complete()
|
||
with suppress(Exception):
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.__needs_minimum_wait() or self.browser == "safari":
|
||
time.sleep(0.045)
|
||
try:
|
||
if self.driver.current_url != pre_action_url:
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.075)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
else:
|
||
time.sleep(0.085)
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
self.__set_esc_skip()
|
||
|
||
def slow_click(self, selector, by="css selector", timeout=None):
|
||
"""Similar to click(), but pauses for a brief moment before clicking.
|
||
When used in combination with setting the user-agent, you can often
|
||
bypass bot-detection by tricking websites into thinking that you're
|
||
not a bot. (Useful on websites that block web automation tools.)
|
||
To set the user-agent, use: ``--agent=AGENT``.
|
||
Here's an example message from GitHub's bot-blocker:
|
||
``You have triggered an abuse detection mechanism...`` """
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.click(selector, by=by, timeout=timeout, delay=1.05)
|
||
elif self.slow_mode:
|
||
self.click(selector, by=by, timeout=timeout, delay=0.65)
|
||
else:
|
||
# Demo Mode already includes a small delay
|
||
self.click(selector, by=by, timeout=timeout, delay=0.25)
|
||
|
||
def double_click(self, selector, by="css selector", timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
original_by = by
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
# Find the element one more time in case scrolling hid it
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
try:
|
||
if self.browser == "safari":
|
||
# Jump to the "except" block where the other script should work
|
||
raise Exception("This Exception will be caught.")
|
||
actions = ActionChains(self.driver)
|
||
actions.double_click(element).perform()
|
||
except Exception:
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
double_click_script = (
|
||
"""var targetElement1 = document.querySelector('%s');
|
||
var clickEvent1 = document.createEvent('MouseEvents');
|
||
clickEvent1.initEvent('dblclick', true, true);
|
||
targetElement1.dispatchEvent(clickEvent1);"""
|
||
% css_selector
|
||
)
|
||
if ":contains\\(" not in css_selector:
|
||
self.execute_script(double_click_script)
|
||
else:
|
||
double_click_script = (
|
||
"""var targetElement1 = arguments[0];
|
||
var clickEvent1 = document.createEvent('MouseEvents');
|
||
clickEvent1.initEvent('dblclick', true, true);
|
||
targetElement1.dispatchEvent(clickEvent1);"""
|
||
)
|
||
self.execute_script(double_click_script, element)
|
||
if settings.WAIT_FOR_RSC_ON_CLICKS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
# A smaller subset of self.wait_for_ready_state_complete()
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.driver.current_url != pre_action_url:
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
elif self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
|
||
def context_click(self, selector, by="css selector", timeout=None):
|
||
"""(A context click is a right-click that opens a context menu.)"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
original_by = by
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
# Find the element one more time in case scrolling hid it
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
try:
|
||
if self.browser == "safari":
|
||
# Jump to the "except" block where the other script should work
|
||
raise Exception("This Exception will be caught.")
|
||
actions = ActionChains(self.driver)
|
||
actions.context_click(element).perform()
|
||
except Exception:
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
right_click_script = (
|
||
"""var targetElement1 = document.querySelector('%s');
|
||
var clickEvent1 = document.createEvent('MouseEvents');
|
||
clickEvent1.initEvent('contextmenu', true, true);
|
||
targetElement1.dispatchEvent(clickEvent1);"""
|
||
% css_selector
|
||
)
|
||
if ":contains\\(" not in css_selector:
|
||
self.execute_script(right_click_script)
|
||
else:
|
||
right_click_script = (
|
||
"""var targetElement1 = arguments[0];
|
||
var clickEvent1 = document.createEvent('MouseEvents');
|
||
clickEvent1.initEvent('contextmenu', true, true);
|
||
targetElement1.dispatchEvent(clickEvent1);"""
|
||
)
|
||
self.execute_script(right_click_script, element)
|
||
if settings.WAIT_FOR_RSC_ON_CLICKS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
# A smaller subset of self.wait_for_ready_state_complete()
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.driver.current_url != pre_action_url:
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
elif self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["r_clk", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def click_chain(
|
||
self, selectors_list, by="css selector", timeout=None, spacing=0
|
||
):
|
||
"""This method clicks on a list of elements in succession.
|
||
@Params
|
||
selectors_list - The list of selectors to click on.
|
||
by - The type of selector to search by (Default: "css selector").
|
||
timeout - How long to wait for the selector to be visible.
|
||
spacing - The amount of time to wait between clicks (in seconds)."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
for selector in selectors_list:
|
||
self.click(selector, by=by, timeout=timeout)
|
||
if spacing > 0:
|
||
time.sleep(spacing)
|
||
|
||
def update_text(
|
||
self, selector, text, by="css selector", timeout=None, retry=False
|
||
):
|
||
"""This method updates an element's text field with new text.
|
||
Has multiple parts:
|
||
* Waits for the element to be visible.
|
||
* Waits for the element to be interactive.
|
||
* Clears the text field.
|
||
* Types in the new text.
|
||
* Hits Enter/Submit (if the text ends in "\n").
|
||
@Params
|
||
selector - The selector of the text field.
|
||
text - The new text to type into the text field.
|
||
by - The type of selector to search by. (Default: "css selector")
|
||
timeout - How long to wait for the selector to be visible.
|
||
retry - If True, use JS if the Selenium text update fails."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.type(selector, text, timeout=timeout)
|
||
return
|
||
if self.__is_shadow_selector(selector):
|
||
self.__shadow_type(selector, text, timeout)
|
||
return
|
||
element = self.wait_for_element_clickable(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
try:
|
||
element.clear() # May need https://stackoverflow.com/a/50691625
|
||
backspaces = Keys.BACK_SPACE * 42 # Is the answer to everything
|
||
element.send_keys(backspaces) # In case autocomplete keeps text
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_element_clickable(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
with suppress(Exception):
|
||
element.clear()
|
||
except Exception:
|
||
pass # Clearing the text field first might not be necessary
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
pre_action_url = None
|
||
if self.demo_mode:
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
text = self.__get_type_checked_text(text)
|
||
try:
|
||
if not text.endswith("\n"):
|
||
element.send_keys(text)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
element.send_keys(text[:-1])
|
||
if self.slow_mode or self.demo_mode:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
else:
|
||
time.sleep(0.0135)
|
||
try:
|
||
element.send_keys(Keys.RETURN)
|
||
except WebDriverException as e:
|
||
if (
|
||
"cannot determine loading status" in e.msg
|
||
or "unexpected command response" in e.msg
|
||
):
|
||
pass # Odd issue where the click did happen. Continue.
|
||
else:
|
||
raise
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.14)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
time.sleep(0.04)
|
||
if not text.endswith("\n"):
|
||
element.send_keys(text)
|
||
else:
|
||
element.send_keys(text[:-1])
|
||
if self.slow_mode or self.demo_mode:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
else:
|
||
time.sleep(0.0135)
|
||
try:
|
||
element.send_keys(Keys.RETURN)
|
||
except WebDriverException as e:
|
||
if (
|
||
"cannot determine loading status" in e.msg
|
||
or "unexpected command response" in e.msg
|
||
):
|
||
pass # Odd issue where the click did happen. Continue.
|
||
else:
|
||
raise
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.03)
|
||
if self.undetectable:
|
||
time.sleep(0.025)
|
||
if (
|
||
retry
|
||
and element.get_attribute("value") != text
|
||
and not text.endswith("\n")
|
||
):
|
||
logging.debug("update_text() is falling back to JavaScript!")
|
||
self.set_value(selector, text, by=by)
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def add_text(self, selector, text, by="css selector", timeout=None):
|
||
"""The more-reliable version of driver.send_keys()
|
||
Similar to update_text(), but won't clear the text field first."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.send_keys(selector, text)
|
||
return
|
||
if self.__is_shadow_selector(selector):
|
||
self.__shadow_type(selector, text, timeout, clear_first=False)
|
||
return
|
||
if selector == "html" and text in ["\n", Keys.ENTER, Keys.RETURN]:
|
||
# This is a shortcut for calling self.click_active_element().
|
||
# Use after "\t" or Keys.TAB to cycle through elements first.
|
||
self.click_active_element()
|
||
return
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if (
|
||
selector == "html" and text.count("\t") >= 1
|
||
and text.count("\n") == 1 and text.endswith("\n")
|
||
and text.replace("\t", "").replace("\n", "").replace(" ", "") == ""
|
||
):
|
||
# Shortcut to send multiple tabs followed by click_active_element()
|
||
self.wait_for_ready_state_complete()
|
||
element.send_keys(Keys.TAB * text.count("\t"))
|
||
self.click_active_element()
|
||
return
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
pre_action_url = None
|
||
if self.demo_mode:
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
text = self.__get_type_checked_text(text)
|
||
try:
|
||
if not text.endswith("\n"):
|
||
element.send_keys(text)
|
||
else:
|
||
element.send_keys(text[:-1])
|
||
if self.slow_mode or self.demo_mode:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
else:
|
||
time.sleep(0.0135)
|
||
element.send_keys(Keys.RETURN)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if not text.endswith("\n"):
|
||
element.send_keys(text)
|
||
else:
|
||
element.send_keys(text[:-1])
|
||
if self.slow_mode or self.demo_mode:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
else:
|
||
time.sleep(0.0135)
|
||
element.send_keys(Keys.RETURN)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def type(
|
||
self, selector, text, by="css selector", timeout=None, retry=False
|
||
):
|
||
"""Same as self.update_text()
|
||
This method updates an element's text field with new text.
|
||
Has multiple parts:
|
||
* Waits for the element to be visible.
|
||
* Waits for the element to be interactive.
|
||
* Clears the text field.
|
||
* Types in the new text.
|
||
* Hits Enter/Submit (if the text ends in "\n").
|
||
@Params
|
||
selector - The selector of the text field.
|
||
text - The new text to type into the text field.
|
||
by - The type of selector to search by. (Default: "css selector")
|
||
timeout - How long to wait for the selector to be visible.
|
||
retry - If True, use JS if the Selenium text update fails.
|
||
DO NOT confuse self.type() with Python type()! They are different!
|
||
"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.update_text(selector, text, by=by, timeout=timeout, retry=retry)
|
||
|
||
def send_keys(self, selector, text, by="css selector", timeout=None):
|
||
"""Same as self.add_text()
|
||
Similar to update_text(), but won't clear the text field first."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.add_text(selector, text, by=by, timeout=timeout)
|
||
|
||
def press_keys(self, selector, text, by="css selector", timeout=None):
|
||
"""Use send_keys() to press one key at a time."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.press_keys(selector, text, timeout=timeout)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if self.demo_mode:
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
self.__demo_mode_highlight_if_active(css_selector, By.CSS_SELECTOR)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sel_tex = [css_selector, text]
|
||
action = ["pkeys", sel_tex, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
press_enter = False
|
||
if text.endswith("\n"):
|
||
text = text[:-1]
|
||
press_enter = True
|
||
for key in text:
|
||
element.send_keys(key)
|
||
if press_enter:
|
||
if self.slow_mode or self.demo_mode:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
else:
|
||
time.sleep(0.0135)
|
||
element.send_keys(Keys.RETURN)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
if not self.undetectable:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
time.sleep(0.15)
|
||
if self.demo_mode:
|
||
if press_enter:
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
elif self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
if self.undetectable:
|
||
time.sleep(0.02)
|
||
|
||
def submit(self, selector, by="css selector"):
|
||
"""Alternative to self.driver.find_element_by_*(SELECTOR).submit()"""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.submit(selector)
|
||
return
|
||
element = self.wait_for_element_clickable(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
element.submit()
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def clear(self, selector, by="css selector", timeout=None):
|
||
"""This method clears an element's text field.
|
||
A clear() is already included with most methods that type text,
|
||
such as self.type(), self.update_text(), etc.
|
||
Does not use Demo Mode highlights, mainly because we expect
|
||
that some users will be calling an unnecessary clear() before
|
||
calling a method that already includes clear() as part of it.
|
||
In case websites trigger an autofill after clearing a field,
|
||
add backspaces to make sure autofill doesn't undo the clear.
|
||
@Params
|
||
selector - The selector of the text field.
|
||
by - The type of selector to search by. (Default: "css selector")
|
||
timeout - How long to wait for the selector to be visible."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
self.__shadow_clear(selector, timeout)
|
||
return
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
self.scroll_to(selector, by=by, timeout=timeout)
|
||
try:
|
||
element.clear()
|
||
backspaces = Keys.BACK_SPACE * 42 # Autofill Defense
|
||
element.send_keys(backspaces)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
element.clear()
|
||
with suppress(Exception):
|
||
backspaces = Keys.BACK_SPACE * 42 # Autofill Defense
|
||
element.send_keys(backspaces)
|
||
except Exception:
|
||
element.clear()
|
||
|
||
def focus(self, selector, by="css selector", timeout=None):
|
||
"""Make the current page focus on an interactable element.
|
||
If the element is not interactable, only scrolls to it.
|
||
The "tab" key is another way of setting the page focus."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.focus(selector)
|
||
return
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if not element.is_displayed():
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = """document.querySelector('%s').focus();""" % css_selector
|
||
self.execute_script(script)
|
||
self.__demo_mode_pause_if_active()
|
||
return
|
||
self.scroll_to(selector, by=by, timeout=timeout)
|
||
try:
|
||
element.send_keys(Keys.NULL)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
try:
|
||
element.send_keys(Keys.NULL)
|
||
except ENI_Exception:
|
||
# Non-interactable element. Skip focus and continue.
|
||
pass
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def refresh_page(self):
|
||
self.__check_scope()
|
||
self.__last_page_load_url = None
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.reload()
|
||
return
|
||
js_utils.clear_out_console_logs(self.driver)
|
||
self.driver.refresh()
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def refresh(self):
|
||
"""The shorter version of self.refresh_page()"""
|
||
self.refresh_page()
|
||
|
||
def get_current_url(self):
|
||
self.__check_scope()
|
||
current_url = None
|
||
if self.__is_cdp_swap_needed():
|
||
current_url = self.cdp.get_current_url()
|
||
else:
|
||
current_url = self.driver.current_url
|
||
if "%" in current_url:
|
||
try:
|
||
from urllib.parse import unquote
|
||
|
||
current_url = unquote(current_url, errors="strict")
|
||
except Exception:
|
||
pass
|
||
return current_url
|
||
|
||
def get_origin(self):
|
||
self.__check_scope()
|
||
return self.execute_script("return window.location.origin;")
|
||
|
||
def get_page_source(self):
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_page_source()
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait:
|
||
time.sleep(0.025)
|
||
return self.driver.page_source
|
||
|
||
def get_page_title(self):
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_title()
|
||
self.wait_for_ready_state_complete()
|
||
with suppress(Exception):
|
||
self.wait_for_element_present(
|
||
"title", by="css selector", timeout=settings.MINI_TIMEOUT
|
||
)
|
||
time.sleep(0.025)
|
||
return self.driver.title
|
||
|
||
def get_title(self):
|
||
"""The shorter version of self.get_page_title()"""
|
||
return self.get_page_title()
|
||
|
||
def get_user_agent(self):
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_user_agent()
|
||
return self.execute_script("return navigator.userAgent;")
|
||
|
||
def get_locale_code(self):
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_locale_code()
|
||
return self.execute_script(
|
||
"return navigator.language || navigator.languages[0];"
|
||
)
|
||
|
||
def go_back(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.go_back()
|
||
return
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
self.save_recorded_actions()
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
self.__last_page_load_url = None
|
||
self.driver.back()
|
||
with suppress(Exception):
|
||
if pre_action_url == self.driver.current_url:
|
||
self.driver.back() # Again because the page was redirected
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["go_bk", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if self.browser == "safari":
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.02)
|
||
self.driver.refresh()
|
||
time.sleep(0.02)
|
||
self.wait_for_ready_state_complete()
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def go_forward(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.go_forward()
|
||
return
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
self.save_recorded_actions()
|
||
self.__last_page_load_url = None
|
||
self.driver.forward()
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["go_fw", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
self.wait_for_ready_state_complete()
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def open_start_page(self):
|
||
"""Navigates the current browser window to the start_page.
|
||
You can set the start_page on the command-line in three ways:
|
||
'--start_page=URL', '--start-page=URL', or '--url=URL'.
|
||
If the start_page is not set, then "about:blank" will be used."""
|
||
self.__check_scope()
|
||
start_page = self.start_page
|
||
if isinstance(start_page, str):
|
||
start_page = start_page.strip() # Remove extra whitespace
|
||
if start_page and len(start_page) >= 4:
|
||
if page_utils.is_valid_url(start_page):
|
||
self.open(start_page)
|
||
else:
|
||
new_start_page = "https://" + start_page
|
||
if page_utils.is_valid_url(new_start_page):
|
||
self.__dont_record_open = True
|
||
self.open(new_start_page)
|
||
self.__dont_record_open = False
|
||
else:
|
||
logging.info('Invalid URL: "%s"!' % start_page)
|
||
if self.get_current_url() != "about:blank":
|
||
self.open("about:blank")
|
||
else:
|
||
if self.get_current_url() != "about:blank":
|
||
self.open("about:blank")
|
||
|
||
def open_if_not_url(self, url):
|
||
"""Opens the url in the browser if it's not the current url (*).
|
||
Parameters tagged on by search engines are ignored for this method.
|
||
Eg. If the current url is:
|
||
* https://www.bing.com/search?q=SeleniumBase&source=hp
|
||
And the url privided by this method call is:
|
||
* https://www.bing.com/search?q=SeleniumBase
|
||
Then the urls will be considered the same,
|
||
and no open() action will be performed.
|
||
This method is primarily used by Recorder Mode script generation,
|
||
where both clicks and opens are recorded. So if a click() action
|
||
leads to an open() action, then the script generator will attempt
|
||
to convert the open() action into open_if_not_url() so that the
|
||
same page isn't opened again if the user is already on the page."""
|
||
self.__check_scope()
|
||
current_url = self.get_current_url()
|
||
if current_url != url:
|
||
if (
|
||
"?q=" not in current_url
|
||
or "&" not in current_url
|
||
or current_url.find("?q=") >= current_url.find("&")
|
||
or current_url.split("&")[0] != url
|
||
):
|
||
self.open(url)
|
||
|
||
def is_element_present(self, selector, by="css selector"):
|
||
"""Returns whether the element exists in the HTML."""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.is_element_present(selector)
|
||
self.wait_for_ready_state_complete()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_element_present(selector)
|
||
return page_actions.is_element_present(self.driver, selector, by)
|
||
|
||
def is_element_visible(self, selector, by="css selector"):
|
||
"""Returns whether the element is visible on the page."""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.is_element_visible(selector)
|
||
self.wait_for_ready_state_complete()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_element_visible(selector)
|
||
return page_actions.is_element_visible(self.driver, selector, by)
|
||
|
||
def is_element_clickable(self, selector, by="css selector"):
|
||
self.wait_for_ready_state_complete()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_element_clickable(selector)
|
||
return page_actions.is_element_clickable(self.driver, selector, by)
|
||
|
||
def is_element_enabled(self, selector, by="css selector"):
|
||
self.wait_for_ready_state_complete()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_element_enabled(selector)
|
||
return page_actions.is_element_enabled(self.driver, selector, by)
|
||
|
||
def is_text_visible(self, text, selector="body", by="css selector"):
|
||
"""Returns whether the text substring is visible in the element."""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.is_text_visible(text, selector)
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_text_visible(text, selector)
|
||
return page_actions.is_text_visible(self.driver, text, selector, by)
|
||
|
||
def is_exact_text_visible(self, text, selector="body", by="css selector"):
|
||
"""Returns whether the text is exactly equal to the element text.
|
||
(Leading and trailing whitespace is ignored in the verification.)"""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.is_exact_text_visible(text, selector)
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_exact_text_visible(text, selector)
|
||
return page_actions.is_exact_text_visible(
|
||
self.driver, text, selector, by
|
||
)
|
||
|
||
def is_non_empty_text_visible(self, selector="body", by="css selector"):
|
||
"""Returns whether the element has any non-empty text visible.
|
||
Whitespace-only text is considered empty text."""
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_non_empty_text_visible(selector)
|
||
return page_actions.is_non_empty_text_visible(
|
||
self.driver, selector, by=by
|
||
)
|
||
|
||
def is_attribute_present(
|
||
self, selector, attribute, value=None, by="css selector"
|
||
):
|
||
"""Returns True if the element attribute/value is found.
|
||
If the value is not specified, the attribute only needs to exist."""
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__is_shadow_attribute_present(
|
||
selector, attribute, value
|
||
)
|
||
return page_actions.is_attribute_present(
|
||
self.driver, selector, attribute, value, by
|
||
)
|
||
|
||
def is_link_text_visible(self, link_text):
|
||
"""Returns whether there's an exact match for the link text."""
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
return page_actions.is_element_visible(
|
||
self.driver, link_text, by="link text"
|
||
)
|
||
|
||
def is_partial_link_text_visible(self, partial_link_text):
|
||
"""Returns whether there's a substring match for the link text."""
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
return page_actions.is_element_visible(
|
||
self.driver, partial_link_text, by="partial link text"
|
||
)
|
||
|
||
def is_link_text_present(self, link_text):
|
||
"""Returns True if the link text appears in the HTML of the page.
|
||
The element doesn't need to be visible,
|
||
such as elements hidden inside a dropdown selection."""
|
||
self.wait_for_ready_state_complete()
|
||
soup = self.get_beautiful_soup()
|
||
html_links = soup.find_all("a")
|
||
for html_link in html_links:
|
||
if html_link.text.strip() == link_text.strip():
|
||
return True
|
||
return False
|
||
|
||
def is_partial_link_text_present(self, link_text):
|
||
"""Returns True if the partial link appears in the HTML of the page.
|
||
The element doesn't need to be visible,
|
||
such as elements hidden inside a dropdown selection."""
|
||
self.wait_for_ready_state_complete()
|
||
soup = self.get_beautiful_soup()
|
||
html_links = soup.find_all("a")
|
||
for html_link in html_links:
|
||
if link_text.strip() in html_link.text.strip():
|
||
return True
|
||
return False
|
||
|
||
def get_link_attribute(self, link_text, attribute, hard_fail=True):
|
||
"""Finds a link by link text and then returns the attribute's value.
|
||
If the link text or attribute cannot be found, an exception will
|
||
get raised if hard_fail is True (otherwise None is returned)."""
|
||
self.wait_for_ready_state_complete()
|
||
soup = self.get_beautiful_soup()
|
||
html_links = soup.find_all("a")
|
||
for html_link in html_links:
|
||
if html_link.text.strip() == link_text.strip():
|
||
if html_link.has_attr(attribute):
|
||
attribute_value = html_link.get(attribute)
|
||
return attribute_value
|
||
if hard_fail:
|
||
raise Exception(
|
||
"Unable to find attribute {%s} from link text {%s}!"
|
||
% (attribute, link_text)
|
||
)
|
||
else:
|
||
return None
|
||
if hard_fail:
|
||
raise Exception("Link text {%s} was not found!" % link_text)
|
||
else:
|
||
return None
|
||
|
||
def get_link_text_attribute(self, link_text, attribute, hard_fail=True):
|
||
"""Same as self.get_link_attribute()
|
||
Finds a link by link text and then returns the attribute's value.
|
||
If the link text or attribute cannot be found, an exception will
|
||
get raised if hard_fail is True (otherwise None is returned)."""
|
||
return self.get_link_attribute(link_text, attribute, hard_fail)
|
||
|
||
def get_partial_link_text_attribute(
|
||
self, link_text, attribute, hard_fail=True
|
||
):
|
||
"""Finds a link by partial link text and then returns the attribute's
|
||
value. If the partial link text or attribute cannot be found, an
|
||
exception will get raised if hard_fail is True (otherwise None
|
||
is returned)."""
|
||
self.wait_for_ready_state_complete()
|
||
soup = self.get_beautiful_soup()
|
||
html_links = soup.find_all("a")
|
||
for html_link in html_links:
|
||
if link_text.strip() in html_link.text.strip():
|
||
if html_link.has_attr(attribute):
|
||
attribute_value = html_link.get(attribute)
|
||
return attribute_value
|
||
if hard_fail:
|
||
raise Exception(
|
||
"Unable to find attribute {%s} from "
|
||
"partial link text {%s}!" % (attribute, link_text)
|
||
)
|
||
else:
|
||
return None
|
||
if hard_fail:
|
||
msg = "Partial Link text {%s} was not found!" % link_text
|
||
page_actions.timeout_exception("LinkTextNotFoundException", msg)
|
||
else:
|
||
return None
|
||
|
||
def click_link_text(self, link_text, timeout=None):
|
||
"""This method clicks link text on a page."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.find_element(link_text, timeout=timeout).click()
|
||
return
|
||
self.__skip_if_esc()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
link_text = self.__get_type_checked_text(link_text)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click_link(link_text)
|
||
return
|
||
if self.browser == "safari":
|
||
if self.demo_mode:
|
||
self.wait_for_link_text_present(link_text, timeout=timeout)
|
||
try:
|
||
self.__jquery_slow_scroll_to(link_text, by="link text")
|
||
except Exception:
|
||
element = self.wait_for_link_text_visible(
|
||
link_text, timeout=timeout
|
||
)
|
||
self.__slow_scroll_to_element(element)
|
||
o_bs = "" # original_box_shadow
|
||
loops = settings.HIGHLIGHTS
|
||
selector = self.convert_to_css_selector(
|
||
link_text, by="link text"
|
||
)
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
try:
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
self.__highlight_with_jquery(selector, loops, o_bs)
|
||
except Exception:
|
||
pass # JQuery probably couldn't load. Skip highlighting.
|
||
self.__jquery_click(link_text, by="link text")
|
||
return
|
||
if not self.is_link_text_present(link_text):
|
||
self.wait_for_link_text_present(link_text, timeout=timeout)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
try:
|
||
element = self.wait_for_link_text_visible(link_text, timeout=0.2)
|
||
self.__demo_mode_highlight_if_active(link_text, by="link text")
|
||
try:
|
||
self.__element_click(element)
|
||
except (Stale_Exception, ENI_Exception, ECI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_link_text_visible(
|
||
link_text, timeout=timeout
|
||
)
|
||
self.__element_click(element)
|
||
except Exception:
|
||
found_css = False
|
||
text_id = self.get_link_attribute(link_text, "id", False)
|
||
if text_id:
|
||
link_css = '[id="%s"]' % link_text
|
||
found_css = True
|
||
if not found_css:
|
||
href = self.__get_href_from_link_text(link_text, False)
|
||
if href:
|
||
if href.startswith("/") or page_utils.is_valid_url(href):
|
||
link_css = '[href="%s"]' % href
|
||
found_css = True
|
||
if not found_css:
|
||
ngclick = self.get_link_attribute(link_text, "ng-click", False)
|
||
if ngclick:
|
||
link_css = '[ng-click="%s"]' % ngclick
|
||
found_css = True
|
||
if not found_css:
|
||
onclick = self.get_link_attribute(link_text, "onclick", False)
|
||
if onclick:
|
||
link_css = '[onclick="%s"]' % onclick
|
||
found_css = True
|
||
success = False
|
||
if found_css:
|
||
if self.is_element_visible(link_css):
|
||
self.click(link_css)
|
||
success = True
|
||
else:
|
||
# The link text might be hidden under a dropdown menu
|
||
success = self.__click_dropdown_link_text(
|
||
link_text, link_css
|
||
)
|
||
if not success:
|
||
element = self.wait_for_link_text_visible(
|
||
link_text, timeout=settings.MINI_TIMEOUT
|
||
)
|
||
self.__element_click(element)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
elif (
|
||
latest_window_count == pre_window_count - 1
|
||
and latest_window_count > 0
|
||
):
|
||
# If a click closes the active window,
|
||
# switch to the last one if it exists.
|
||
self.switch_to_window(-1)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
with suppress(Exception):
|
||
self.wait_for_ready_state_complete()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
elif self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
|
||
def click_partial_link_text(self, partial_link_text, timeout=None):
|
||
"""This method clicks the partial link text on a page."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
partial_link_text = self.__get_type_checked_text(partial_link_text)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.find_element(partial_link_text, timeout=timeout).click()
|
||
return
|
||
if not self.is_partial_link_text_present(partial_link_text):
|
||
self.wait_for_partial_link_text_present(
|
||
partial_link_text, timeout=timeout
|
||
)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
try:
|
||
element = self.wait_for_partial_link_text(
|
||
partial_link_text, timeout=0.2
|
||
)
|
||
self.__demo_mode_highlight_if_active(
|
||
partial_link_text, by="link text"
|
||
)
|
||
try:
|
||
self.__element_click(element)
|
||
except (Stale_Exception, ENI_Exception, ECI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_partial_link_text(
|
||
partial_link_text, timeout=timeout
|
||
)
|
||
self.__element_click(element)
|
||
except Exception:
|
||
found_css = False
|
||
text_id = self.get_partial_link_text_attribute(
|
||
partial_link_text, "id", False
|
||
)
|
||
if text_id:
|
||
link_css = '[id="%s"]' % partial_link_text
|
||
found_css = True
|
||
if not found_css:
|
||
href = self.__get_href_from_partial_link_text(
|
||
partial_link_text, False
|
||
)
|
||
if href:
|
||
if href.startswith("/") or page_utils.is_valid_url(href):
|
||
link_css = '[href="%s"]' % href
|
||
found_css = True
|
||
if not found_css:
|
||
ngclick = self.get_partial_link_text_attribute(
|
||
partial_link_text, "ng-click", False
|
||
)
|
||
if ngclick:
|
||
link_css = '[ng-click="%s"]' % ngclick
|
||
found_css = True
|
||
if not found_css:
|
||
onclick = self.get_partial_link_text_attribute(
|
||
partial_link_text, "onclick", False
|
||
)
|
||
if onclick:
|
||
link_css = '[onclick="%s"]' % onclick
|
||
found_css = True
|
||
success = False
|
||
if found_css:
|
||
if self.is_element_visible(link_css):
|
||
self.click(link_css)
|
||
success = True
|
||
else:
|
||
# The link text might be hidden under a dropdown menu
|
||
success = self.__click_dropdown_partial_link_text(
|
||
partial_link_text, link_css
|
||
)
|
||
if not success:
|
||
element = self.wait_for_partial_link_text(
|
||
partial_link_text, timeout=settings.MINI_TIMEOUT
|
||
)
|
||
self.__element_click(element)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
elif (
|
||
latest_window_count == pre_window_count - 1
|
||
and latest_window_count > 0
|
||
):
|
||
# If a click closes the active window,
|
||
# switch to the last one if it exists.
|
||
self.switch_to_window(-1)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
with suppress(Exception):
|
||
self.wait_for_ready_state_complete()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def get_text(self, selector="body", by="css selector", timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_text(selector)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__get_shadow_text(selector, timeout)
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
try:
|
||
element_text = element.text
|
||
if self.browser == "safari":
|
||
if element.tag_name.lower() in ["input", "textarea"]:
|
||
element_text = element.get_attribute("value")
|
||
else:
|
||
element_text = element.get_attribute("innerText")
|
||
elif element.tag_name.lower() in ["input", "textarea"]:
|
||
element_text = element.get_property("value")
|
||
except (Stale_Exception, ENI_Exception, TimeoutException):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.14)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
element_text = element.text
|
||
if self.browser == "safari":
|
||
if element.tag_name.lower() in ["input", "textarea"]:
|
||
element_text = element.get_attribute("value")
|
||
else:
|
||
element_text = element.get_attribute("innerText")
|
||
elif element.tag_name.lower() in ["input", "textarea"]:
|
||
element_text = element.get_property("value")
|
||
return element_text
|
||
|
||
def get_attribute(
|
||
self,
|
||
selector,
|
||
attribute,
|
||
by="css selector",
|
||
timeout=None,
|
||
hard_fail=True,
|
||
):
|
||
"""This method uses JavaScript to get the value of an attribute.
|
||
If the attribute doesn't exist or isn't found, an exception will
|
||
get raised if hard_fail is True (otherwise None is returned)."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_element_attribute(selector, attribute)
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__get_shadow_attribute(
|
||
selector, attribute, timeout=timeout
|
||
)
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
try:
|
||
attribute_value = element.get_attribute(attribute)
|
||
except (Stale_Exception, ENI_Exception, TimeoutException):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.14)
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
attribute_value = element.get_attribute(attribute)
|
||
if attribute_value is not None:
|
||
return attribute_value
|
||
else:
|
||
if hard_fail:
|
||
raise Exception(
|
||
"Element {%s} has no attribute {%s}!"
|
||
% (selector, attribute)
|
||
)
|
||
else:
|
||
return None
|
||
|
||
def set_attribute(
|
||
self,
|
||
selector,
|
||
attribute,
|
||
value,
|
||
by="css selector",
|
||
timeout=None,
|
||
scroll=False,
|
||
):
|
||
"""This method uses JavaScript to set/update an attribute.
|
||
Only the first matching selector from querySelector() is used."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
original_attribute = attribute
|
||
original_value = value
|
||
if scroll and self.is_element_visible(selector, by=by):
|
||
with suppress(Exception):
|
||
self.scroll_to(selector, by=by, timeout=timeout)
|
||
attribute = re.escape(attribute)
|
||
attribute = self.__escape_quotes_if_needed(attribute)
|
||
value = re.escape(value)
|
||
value = self.__escape_quotes_if_needed(value)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""document.querySelector('%s').setAttribute('%s','%s');"""
|
||
% (css_selector, attribute, value)
|
||
)
|
||
self.execute_script(script)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sele_attr_val = [selector, original_attribute, original_value]
|
||
action = ["s_at_", sele_attr_val, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def set_attributes(self, selector, attribute, value, by="css selector"):
|
||
"""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")"""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.set_attributes(selector, attribute, value)
|
||
return
|
||
original_attribute = attribute
|
||
original_value = value
|
||
attribute = re.escape(attribute)
|
||
attribute = self.__escape_quotes_if_needed(attribute)
|
||
value = re.escape(value)
|
||
value = self.__escape_quotes_if_needed(value)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = """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.execute_script(script)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sele_attr_val = [selector, original_attribute, original_value]
|
||
action = ["s_ats", sele_attr_val, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def set_attribute_all(self, selector, attribute, value, by="css selector"):
|
||
"""Same as set_attributes(), but using querySelectorAll naming scheme.
|
||
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_attribute_all("a", "href", "https://google.com")"""
|
||
self.set_attributes(selector, attribute, value, by=by)
|
||
|
||
def remove_attribute(
|
||
self, selector, attribute, by="css selector", timeout=None
|
||
):
|
||
"""This method uses JavaScript to remove an attribute.
|
||
Only the first matching selector from querySelector() is used."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.is_element_visible(selector, by=by):
|
||
with suppress(Exception):
|
||
self.scroll_to(selector, by=by, timeout=timeout)
|
||
attribute = re.escape(attribute)
|
||
attribute = self.__escape_quotes_if_needed(attribute)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = """document.querySelector('%s').removeAttribute('%s');""" % (
|
||
css_selector,
|
||
attribute,
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def remove_attributes(self, selector, attribute, by="css selector"):
|
||
"""This method uses JavaScript to remove a common attribute.
|
||
All matching selectors from querySelectorAll() are used."""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
attribute = re.escape(attribute)
|
||
attribute = self.__escape_quotes_if_needed(attribute)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = """var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
$elements[index].removeAttribute('%s');}""" % (
|
||
css_selector,
|
||
attribute,
|
||
)
|
||
with suppress(Exception):
|
||
self.execute_script(script)
|
||
|
||
def internalize_links(self):
|
||
"""All `target="_blank"` links become `target="_self"`.
|
||
This prevents those links from opening in a new tab."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.internalize_links()
|
||
return
|
||
self.set_attributes('[target="_blank"]', "target", "_self")
|
||
|
||
def get_parent(self, element, by="css selector", timeout=None):
|
||
"""Returns the parent element.
|
||
If element is a string, then finds element first via selector."""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_parent(element)
|
||
if isinstance(element, str):
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
element = self.wait_for_element_present(
|
||
element, by=by, timeout=timeout
|
||
)
|
||
return element.find_element(by="xpath", value="..")
|
||
|
||
def get_property(
|
||
self, selector, property, by="css selector", timeout=None
|
||
):
|
||
"""Returns the property value of an element.
|
||
This is not the same as self.get_property_value(), which returns
|
||
the value of an element's computed style using a different algorithm.
|
||
If no result is found, an empty string (instead of None) is returned.
|
||
Example:
|
||
html_text = self.get_property(SELECTOR, "textContent") """
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.01)
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
try:
|
||
property_value = element.get_property(property)
|
||
except (Stale_Exception, ENI_Exception, TimeoutException):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.14)
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
property_value = element.get_property(property)
|
||
if not property_value:
|
||
return ""
|
||
return property_value
|
||
|
||
def get_text_content(
|
||
self, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Returns the text that appears in the HTML for an element.
|
||
This is different from "self.get_text(selector, by="css selector")"
|
||
because that only returns the visible text on a page for an element,
|
||
rather than the HTML text that's being returned from this method."""
|
||
self.__check_scope()
|
||
return self.get_property(
|
||
selector, property="textContent", by=by, timeout=timeout
|
||
)
|
||
|
||
def get_property_value(
|
||
self, selector, property, by="css selector", timeout=None
|
||
):
|
||
"""Returns the property value of a page element's computed style.
|
||
Example:
|
||
opacity = self.get_property_value("html body a", "opacity")
|
||
self.assertTrue(float(opacity) > 0, "Element not visible!") """
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
try:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
except Exception:
|
||
# If can't convert to CSS_Selector for JS, use element directly
|
||
script = (
|
||
"""var $elm = arguments[0];
|
||
$val = window.getComputedStyle($elm).getPropertyValue('%s');
|
||
return $val;""" % property
|
||
)
|
||
value = self.execute_script(script, element)
|
||
if value is not None:
|
||
return value
|
||
else:
|
||
return ""
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
script = """var $elm = document.querySelector('%s');
|
||
$val = window.getComputedStyle($elm).getPropertyValue('%s');
|
||
return $val;""" % (selector, property)
|
||
value = self.execute_script(script)
|
||
if value is not None:
|
||
return value
|
||
else:
|
||
return "" # Return an empty string if the property doesn't exist
|
||
|
||
def get_image_url(self, selector, by="css selector", timeout=None):
|
||
"""Extracts the URL from an image element on the page."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.get_attribute(
|
||
selector, attribute="src", by=by, timeout=timeout
|
||
)
|
||
|
||
def find_elements(self, selector, by="css selector", limit=0):
|
||
"""Returns a list of matching WebElements.
|
||
Elements could be either hidden or visible on the page.
|
||
If "limit" is set and > 0, will only return that many elements."""
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
elements = self.cdp.select_all(selector)
|
||
if limit and limit > 0 and len(elements) > limit:
|
||
elements = elements[:limit]
|
||
return elements
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.05)
|
||
elements = self.driver.find_elements(by=by, value=selector)
|
||
if limit and limit > 0 and len(elements) > limit:
|
||
elements = elements[:limit]
|
||
return elements
|
||
|
||
def find_visible_elements(self, selector, by="css selector", limit=0):
|
||
"""Returns a list of matching WebElements that are visible.
|
||
If "limit" is set and > 0, will only return that many elements."""
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
elements = self.cdp.find_visible_elements(selector)
|
||
if limit and limit > 0 and len(elements) > limit:
|
||
elements = elements[:limit]
|
||
return elements
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.05)
|
||
return page_actions.find_visible_elements(
|
||
self.driver, selector, by, limit
|
||
)
|
||
|
||
def click_visible_elements(
|
||
self, selector, by="css selector", limit=0, timeout=None
|
||
):
|
||
"""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"]')"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click_visible_elements(selector, limit)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.12)
|
||
if self.undetectable:
|
||
time.sleep(0.06)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
with suppress(Exception):
|
||
# If the first element isn't visible, wait a little.
|
||
if not element.is_displayed():
|
||
time.sleep(0.16)
|
||
if self.undetectable:
|
||
time.sleep(0.06)
|
||
elements = self.find_elements(selector, by=by)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
click_count = 0
|
||
for element in elements:
|
||
if limit and limit > 0 and click_count >= limit:
|
||
return
|
||
try:
|
||
if element.is_displayed():
|
||
self.__scroll_to_element(element)
|
||
if self.browser == "safari":
|
||
self.execute_script("arguments[0].click();", element)
|
||
else:
|
||
element.click()
|
||
click_count += 1
|
||
self.wait_for_ready_state_complete()
|
||
except ECI_Exception:
|
||
continue # (Overlay likely)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
try:
|
||
if element.is_displayed():
|
||
self.__scroll_to_element(element)
|
||
if self.browser == "safari":
|
||
self.execute_script(
|
||
"arguments[0].click();", element
|
||
)
|
||
else:
|
||
element.click()
|
||
click_count += 1
|
||
self.wait_for_ready_state_complete()
|
||
except (Stale_Exception, ENI_Exception):
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
return # Probably on new page / Elements are all stale
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
|
||
def click_nth_visible_element(
|
||
self, selector, number, by="css selector", timeout=None
|
||
):
|
||
"""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.)"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click_nth_visible_element(selector, number)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
elements = self.find_visible_elements(selector, by=by)
|
||
if len(elements) < number:
|
||
raise Exception(
|
||
"Not enough matching {%s} elements of type {%s} to "
|
||
"click number %s!" % (selector, by, number)
|
||
)
|
||
number = number - 1
|
||
if number < 0:
|
||
number = 0
|
||
element = elements[number]
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
try:
|
||
self.__scroll_to_element(element)
|
||
self.__element_click(element)
|
||
except (Stale_Exception, ENI_Exception, ECI_Exception):
|
||
time.sleep(0.12)
|
||
self.wait_for_ready_state_complete()
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
elements = self.find_visible_elements(selector, by=by)
|
||
if len(elements) < number:
|
||
raise Exception(
|
||
"Not enough matching {%s} elements of type {%s} to "
|
||
"click number %s!" % (selector, by, number)
|
||
)
|
||
number = number - 1
|
||
if number < 0:
|
||
number = 0
|
||
element = elements[number]
|
||
self.__element_click(element)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
|
||
def click_if_visible(self, selector, by="css selector", timeout=0):
|
||
"""If the page selector exists and is visible, clicks on the element.
|
||
This method only clicks on the first matching element found.
|
||
Use click_visible_elements() to click all matching elements.
|
||
If a "timeout" is provided, waits that long for the element
|
||
to appear before giving up and returning without a click()."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click_if_visible(selector)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
elif timeout > 0:
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
|
||
def click_active_element(self):
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click_active_element()
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
if self.recorder_mode:
|
||
selector = js_utils.get_active_element_css(self.driver)
|
||
self.click(selector)
|
||
return
|
||
else:
|
||
self.execute_script("document.activeElement.click();")
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
if settings.WAIT_FOR_RSC_ON_CLICKS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
# A smaller subset of self.wait_for_ready_state_complete()
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.driver.current_url != pre_action_url:
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def click_with_offset(
|
||
self,
|
||
selector,
|
||
x,
|
||
y,
|
||
by="css selector",
|
||
mark=None,
|
||
timeout=None,
|
||
center=None,
|
||
):
|
||
"""Click an element at an {X,Y}-offset location.
|
||
{0,0} is the top-left corner of the element.
|
||
If center==True, {0,0} becomes the center of the element.
|
||
If mark==True, will draw a dot at location. (Useful for debugging)
|
||
In Demo Mode, mark becomes True unless set to False. (Default: None)"""
|
||
self.__check_scope()
|
||
self.__click_with_offset(
|
||
selector,
|
||
x,
|
||
y,
|
||
by=by,
|
||
double=False,
|
||
mark=mark,
|
||
timeout=timeout,
|
||
center=center,
|
||
)
|
||
|
||
def double_click_with_offset(
|
||
self,
|
||
selector,
|
||
x,
|
||
y,
|
||
by="css selector",
|
||
mark=None,
|
||
timeout=None,
|
||
center=None,
|
||
):
|
||
"""Double click an element at an {X,Y}-offset location.
|
||
{0,0} is the top-left corner of the element.
|
||
If center==True, {0,0} becomes the center of the element.
|
||
If mark==True, will draw a dot at location. (Useful for debugging)
|
||
In Demo Mode, mark becomes True unless set to False. (Default: None)"""
|
||
self.__check_scope()
|
||
self.__click_with_offset(
|
||
selector,
|
||
x,
|
||
y,
|
||
by=by,
|
||
double=True,
|
||
mark=mark,
|
||
timeout=timeout,
|
||
center=center,
|
||
)
|
||
|
||
def is_checked(self, selector, by="css selector", timeout=None):
|
||
"""Determines if a checkbox or a radio button element is checked.
|
||
Returns True if the element is checked.
|
||
Returns False if the element is not checked.
|
||
If the element is not present on the page, raises an exception.
|
||
If the element is not a checkbox or radio, raises an exception."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.is_checked(selector)
|
||
kind = self.get_attribute(selector, "type", by=by, timeout=timeout)
|
||
if kind != "checkbox" and kind != "radio":
|
||
raise Exception("Expecting a checkbox or a radio button element!")
|
||
return bool(
|
||
self.get_attribute(
|
||
selector, "checked", by=by, timeout=timeout, hard_fail=False
|
||
)
|
||
)
|
||
|
||
def is_selected(self, selector, by="css selector", timeout=None):
|
||
"""Same as is_checked()"""
|
||
return self.is_checked(selector, by=by, timeout=timeout)
|
||
|
||
def check_if_unchecked(self, selector, by="css selector"):
|
||
"""If a checkbox or radio button is not checked, will check it."""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.check_if_unchecked(selector)
|
||
return
|
||
if not self.is_checked(selector, by=by):
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
else:
|
||
element = self.wait_for_element_present(selector, by=by)
|
||
opacity = self.execute_script(
|
||
'return arguments[0].style.opacity;', element
|
||
)
|
||
# Handle switches that sit on checkboxes with zero opacity:
|
||
# Change the opacity a bit to allow the click to succeed.
|
||
with suppress(Exception):
|
||
self.execute_script(
|
||
'arguments[0].style.opacity="0.001";', element
|
||
)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
else:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
self.__dont_record_js_click = True
|
||
self.js_click(selector, by="css selector")
|
||
self.__dont_record_js_click = False
|
||
with suppress(Exception):
|
||
self.execute_script(
|
||
'arguments[0].style.opacity="arguments[1]";',
|
||
element,
|
||
opacity,
|
||
)
|
||
|
||
def select_if_unselected(self, selector, by="css selector"):
|
||
"""Same as check_if_unchecked()"""
|
||
self.check_if_unchecked(selector, by=by)
|
||
|
||
def uncheck_if_checked(self, selector, by="css selector"):
|
||
"""If a checkbox is checked, will uncheck it."""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.uncheck_if_checked(selector)
|
||
return
|
||
if self.is_checked(selector, by=by):
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
else:
|
||
element = self.wait_for_element_present(selector, by=by)
|
||
opacity = self.execute_script(
|
||
'return arguments[0].style.opacity;', element
|
||
)
|
||
# Handle switches that sit on checkboxes with zero opacity:
|
||
# Change the opacity a bit to allow the click to succeed.
|
||
with suppress(Exception):
|
||
self.execute_script(
|
||
'arguments[0].style.opacity="0.001";', element
|
||
)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.click(selector, by=by)
|
||
else:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
self.__dont_record_js_click = True
|
||
self.js_click(selector, by="css selector")
|
||
self.__dont_record_js_click = False
|
||
with suppress(Exception):
|
||
self.execute_script(
|
||
'arguments[0].style.opacity="arguments[1]";',
|
||
element,
|
||
opacity,
|
||
)
|
||
|
||
def unselect_if_selected(self, selector, by="css selector"):
|
||
"""Same as uncheck_if_checked()"""
|
||
self.uncheck_if_checked(selector, by=by)
|
||
|
||
def is_element_in_an_iframe(self, selector, by="css selector"):
|
||
"""Returns True if the selector's element is located in an iframe.
|
||
Otherwise returns False."""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.is_element_present(selector, by=by):
|
||
return False
|
||
soup = self.get_beautiful_soup()
|
||
iframe_list = soup.select("iframe")
|
||
for iframe in iframe_list:
|
||
iframe_identifier = None
|
||
if iframe.has_attr("name") and len(iframe["name"]) > 0:
|
||
iframe_identifier = iframe["name"]
|
||
elif iframe.has_attr("id") and len(iframe["id"]) > 0:
|
||
iframe_identifier = iframe["id"]
|
||
elif iframe.has_attr("class") and len(iframe["class"]) > 0:
|
||
iframe_class = " ".join(iframe["class"])
|
||
iframe_identifier = '[class="%s"]' % iframe_class
|
||
else:
|
||
continue
|
||
self.switch_to_frame(iframe_identifier)
|
||
if self.is_element_present(selector, by=by):
|
||
self.switch_to_default_content()
|
||
return True
|
||
self.switch_to_default_content()
|
||
return False
|
||
|
||
def switch_to_frame_of_element(self, selector, by="css selector"):
|
||
"""Set driver control to the iframe containing element (assuming the
|
||
element is in a single-nested iframe) and returns the iframe name.
|
||
If element is not in an iframe, returns None, and nothing happens.
|
||
May not work if multiple iframes are nested within each other."""
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
if self.undetectable:
|
||
time.sleep(0.04)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.is_element_present(selector, by=by):
|
||
return None
|
||
soup = self.get_beautiful_soup()
|
||
iframe_list = soup.select("iframe")
|
||
for iframe in iframe_list:
|
||
iframe_identifier = None
|
||
if iframe.has_attr("name") and len(iframe["name"]) > 0:
|
||
iframe_identifier = iframe["name"]
|
||
elif iframe.has_attr("id") and len(iframe["id"]) > 0:
|
||
iframe_identifier = iframe["id"]
|
||
elif iframe.has_attr("class") and len(iframe["class"]) > 0:
|
||
iframe_class = " ".join(iframe["class"])
|
||
iframe_identifier = '[class="%s"]' % iframe_class
|
||
else:
|
||
continue
|
||
with suppress(Exception):
|
||
self.switch_to_frame(iframe_identifier, timeout=1)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
if self.is_element_present(selector, by=by):
|
||
return iframe_identifier
|
||
self.switch_to_default_content()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
try:
|
||
self.switch_to_frame(selector, timeout=1)
|
||
if self.undetectable:
|
||
self.__uc_frame_layer += 1
|
||
return selector
|
||
except Exception:
|
||
if self.is_element_present(selector, by=by):
|
||
return ""
|
||
raise Exception(
|
||
"Could not switch to iframe containing "
|
||
"element {%s}!" % selector
|
||
)
|
||
|
||
def hover(self, selector, by="css selector", timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
original_by = by
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.gui_hover_element(selector)
|
||
return
|
||
self.wait_for_element_visible(
|
||
original_selector, by=original_by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
self.scroll_to(selector, by=by)
|
||
time.sleep(0.05) # Settle down from scrolling before hovering
|
||
element = page_actions.hover_on_element(self.driver, selector, by)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["hover", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return element
|
||
|
||
def hover_and_click(
|
||
self,
|
||
hover_selector,
|
||
click_selector,
|
||
hover_by="css selector",
|
||
click_by="css selector",
|
||
timeout=None,
|
||
js_click=False,
|
||
):
|
||
"""When you want to hover over an element or dropdown menu,
|
||
and then click an element that appears after that."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = hover_selector
|
||
original_by = hover_by
|
||
hover_selector, hover_by = self.__recalculate_selector(
|
||
hover_selector, hover_by
|
||
)
|
||
original_click_selector = click_selector
|
||
click_selector, click_by = self.__recalculate_selector(
|
||
click_selector, click_by
|
||
)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.gui_hover_and_click(hover_selector, click_selector)
|
||
return
|
||
dropdown_element = self.wait_for_element_visible(
|
||
original_selector, by=original_by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
self.scroll_to(hover_selector, by=hover_by)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if hover_by == By.XPATH:
|
||
hover_selector = original_selector
|
||
if click_by == By.XPATH:
|
||
click_selector = original_click_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
the_selectors = [hover_selector, click_selector]
|
||
action = ["ho_cl", the_selectors, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
outdated_driver = False
|
||
element = None
|
||
try:
|
||
if self.mobile_emulator:
|
||
# On mobile, click to hover the element
|
||
dropdown_element.click()
|
||
else:
|
||
page_actions.hover_element(self.driver, dropdown_element)
|
||
except Exception:
|
||
outdated_driver = True
|
||
element = self.wait_for_element_present(
|
||
click_selector, click_by, timeout
|
||
)
|
||
if click_by == By.LINK_TEXT:
|
||
self.open(self.__get_href_from_link_text(click_selector))
|
||
elif click_by == By.PARTIAL_LINK_TEXT:
|
||
self.open(
|
||
self.__get_href_from_partial_link_text(click_selector)
|
||
)
|
||
else:
|
||
self.__dont_record_js_click = True
|
||
self.js_click(click_selector, by=click_by)
|
||
self.__dont_record_js_click = False
|
||
if outdated_driver:
|
||
pass # Already did the click workaround
|
||
elif self.mobile_emulator:
|
||
self.click(click_selector, by=click_by)
|
||
elif not outdated_driver:
|
||
element = page_actions.hover_and_click(
|
||
self.driver,
|
||
hover_selector,
|
||
click_selector,
|
||
hover_by,
|
||
click_by,
|
||
timeout,
|
||
js_click,
|
||
)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
elif self.browser == "safari":
|
||
# Release the hover by hovering elsewhere
|
||
with suppress(Exception):
|
||
page_actions.hover_on_element(self.driver, "body")
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
return element
|
||
|
||
def hover_and_js_click(
|
||
self,
|
||
hover_selector,
|
||
click_selector,
|
||
hover_by="css selector",
|
||
click_by="css selector",
|
||
timeout=None,
|
||
):
|
||
self.hover_and_click(
|
||
hover_selector=hover_selector,
|
||
click_selector=click_selector,
|
||
hover_by=hover_by,
|
||
click_by=click_by,
|
||
timeout=timeout,
|
||
js_click=True,
|
||
)
|
||
|
||
def hover_and_double_click(
|
||
self,
|
||
hover_selector,
|
||
click_selector,
|
||
hover_by="css selector",
|
||
click_by="css selector",
|
||
timeout=None,
|
||
):
|
||
"""When you want to hover over an element or dropdown menu,
|
||
and then double-click an element that appears after that."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = hover_selector
|
||
original_by = hover_by
|
||
hover_selector, hover_by = self.__recalculate_selector(
|
||
hover_selector, hover_by
|
||
)
|
||
hover_selector = self.convert_to_css_selector(hover_selector, hover_by)
|
||
hover_by = By.CSS_SELECTOR
|
||
click_selector, click_by = self.__recalculate_selector(
|
||
click_selector, click_by
|
||
)
|
||
dropdown_element = self.wait_for_element_visible(
|
||
original_selector, by=original_by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(original_selector, original_by)
|
||
self.scroll_to(hover_selector, by=hover_by)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
outdated_driver = False
|
||
element = None
|
||
try:
|
||
page_actions.hover_element(self.driver, dropdown_element)
|
||
except Exception:
|
||
outdated_driver = True
|
||
element = self.wait_for_element_present(
|
||
click_selector, click_by, timeout
|
||
)
|
||
if click_by == By.LINK_TEXT:
|
||
self.open(self.__get_href_from_link_text(click_selector))
|
||
elif click_by == By.PARTIAL_LINK_TEXT:
|
||
self.open(
|
||
self.__get_href_from_partial_link_text(click_selector)
|
||
)
|
||
else:
|
||
self.__dont_record_js_click = True
|
||
self.js_click(click_selector, click_by)
|
||
self.__dont_record_js_click = False
|
||
if not outdated_driver:
|
||
element = page_actions.hover_element_and_double_click(
|
||
self.driver,
|
||
dropdown_element,
|
||
click_selector,
|
||
click_by="css selector",
|
||
timeout=timeout,
|
||
)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
return element
|
||
|
||
def drag_and_drop(
|
||
self,
|
||
drag_selector,
|
||
drop_selector,
|
||
drag_by="css selector",
|
||
drop_by="css selector",
|
||
timeout=None,
|
||
jquery=False,
|
||
):
|
||
"""Drag-and-drop an element from one selector to another."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
drag_selector, drag_by = self.__recalculate_selector(
|
||
drag_selector, drag_by
|
||
)
|
||
drop_selector, drop_by = self.__recalculate_selector(
|
||
drop_selector, drop_by
|
||
)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.gui_drag_and_drop(drag_selector, drop_selector)
|
||
return
|
||
drag_element = self.wait_for_element_clickable(
|
||
drag_selector, by=drag_by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(drag_selector, drag_by)
|
||
drop_element = self.wait_for_element_visible(
|
||
drop_selector, by=drop_by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(drop_selector, drop_by)
|
||
self.scroll_to(drop_selector, by=drop_by)
|
||
drag_selector = self.convert_to_css_selector(drag_selector, drag_by)
|
||
drop_selector = self.convert_to_css_selector(drop_selector, drop_by)
|
||
if not jquery:
|
||
drag_and_drop_script = js_utils.get_js_drag_and_drop_script()
|
||
self.execute_script(
|
||
drag_and_drop_script, drag_element, drop_element, 0, 0, 1, None
|
||
)
|
||
else:
|
||
drag_and_drop_script = js_utils.get_drag_and_drop_script()
|
||
self.safe_execute_script(
|
||
drag_and_drop_script
|
||
+ (
|
||
"$('%s').simulateDragDrop("
|
||
"{dropTarget: "
|
||
"'%s'});" % (drag_selector, drop_selector)
|
||
)
|
||
)
|
||
if self.demo_mode:
|
||
self.__demo_mode_pause_if_active()
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
return drag_element
|
||
|
||
def drag_and_drop_with_offset(
|
||
self, selector, x, y, by="css selector", timeout=None
|
||
):
|
||
"""Drag-and-drop an element to an {X,Y}-offset location."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
element = self.wait_for_element_visible(css_selector, timeout=timeout)
|
||
self.__demo_mode_highlight_if_active(css_selector, By.CSS_SELECTOR)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = js_utils.get_drag_and_drop_with_offset_script(
|
||
css_selector, x, y
|
||
)
|
||
self.execute_script(script)
|
||
if self.demo_mode:
|
||
self.__demo_mode_pause_if_active()
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
return element
|
||
|
||
def __element_click(self, element):
|
||
self.__check_scope()
|
||
if (
|
||
not self.undetectable
|
||
or self.uc_cdp_events
|
||
or self.__uc_frame_layer > 0
|
||
or not hasattr(element, "uc_click")
|
||
or element.tag_name.lower() != "a"
|
||
):
|
||
element.click()
|
||
else:
|
||
try:
|
||
href = element.get_attribute("href")
|
||
target = element.get_attribute("target")
|
||
if len(href) > 0 and target != "_blank":
|
||
element.uc_click()
|
||
else:
|
||
element.click()
|
||
time.sleep(0.012)
|
||
except Exception:
|
||
element.click()
|
||
|
||
def __select_option(
|
||
self,
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by="css selector",
|
||
option_by="text",
|
||
timeout=None,
|
||
):
|
||
"""Selects an HTML <select> option by specification.
|
||
Option specifications are by "text", "index", or "value".
|
||
Defaults to "text" if option_by is unspecified or unknown."""
|
||
from selenium.webdriver.support.ui import Select
|
||
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
dropdown_selector, dropdown_by = self.__recalculate_selector(
|
||
dropdown_selector, dropdown_by
|
||
)
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_present(
|
||
dropdown_selector, by=dropdown_by, timeout=timeout
|
||
)
|
||
try:
|
||
element = self.wait_for_element_clickable(
|
||
dropdown_selector, by=dropdown_by, timeout=1.8
|
||
)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
if self.is_element_visible(dropdown_selector, by=dropdown_by):
|
||
self.__demo_mode_highlight_if_active(
|
||
dropdown_selector, dropdown_by
|
||
)
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
try:
|
||
if option_by == "index":
|
||
Select(element).select_by_index(option)
|
||
elif option_by == "value":
|
||
Select(element).select_by_value(option)
|
||
else:
|
||
Select(element).select_by_visible_text(option)
|
||
time.sleep(0.05)
|
||
self.wait_for_ready_state_complete()
|
||
except Exception:
|
||
time.sleep(0.25)
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_present(
|
||
dropdown_selector, by=dropdown_by, timeout=timeout
|
||
)
|
||
try:
|
||
element = self.wait_for_element_clickable(
|
||
dropdown_selector, by=dropdown_by, timeout=1.8
|
||
)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
if option_by == "index":
|
||
try:
|
||
Select(element).select_by_index(option)
|
||
except Exception:
|
||
msg = (
|
||
"Element {%s} has no selectable index option {%s}!"
|
||
% (dropdown_selector, option)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"NoSuchOptionException", msg
|
||
)
|
||
elif option_by == "value":
|
||
try:
|
||
Select(element).select_by_value(option)
|
||
except Exception:
|
||
msg = (
|
||
"Element {%s} has no selectable value option {%s}!"
|
||
% (dropdown_selector, option)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"NoSuchOptionException", msg
|
||
)
|
||
else:
|
||
try:
|
||
Select(element).select_by_visible_text(option)
|
||
except Exception:
|
||
msg = (
|
||
"Element {%s} has no selectable text option {%s}!"
|
||
% (dropdown_selector, option)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"NoSuchOptionException", msg
|
||
)
|
||
time.sleep(0.05)
|
||
self.wait_for_ready_state_complete()
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
if settings.WAIT_FOR_RSC_ON_CLICKS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
# A smaller subset of self.wait_for_ready_state_complete()
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.driver.current_url != pre_action_url:
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def select_option_by_text(
|
||
self,
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by="css selector",
|
||
timeout=None,
|
||
):
|
||
"""Selects an HTML <select> option by option text.
|
||
@Params
|
||
dropdown_selector - the <select> selector.
|
||
option - the text of the option."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.select_option_by_text(dropdown_selector, option)
|
||
return
|
||
self.__select_option(
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by=dropdown_by,
|
||
option_by="text",
|
||
timeout=timeout,
|
||
)
|
||
|
||
def select_option_by_index(
|
||
self,
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by="css selector",
|
||
timeout=None,
|
||
):
|
||
"""Selects an HTML <select> option by option index.
|
||
@Params
|
||
dropdown_selector - the <select> selector.
|
||
option - the index number of the option."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__select_option(
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by=dropdown_by,
|
||
option_by="index",
|
||
timeout=timeout,
|
||
)
|
||
|
||
def select_option_by_value(
|
||
self,
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by="css selector",
|
||
timeout=None,
|
||
):
|
||
"""Selects an HTML <select> option by option value.
|
||
@Params
|
||
dropdown_selector - the <select> selector.
|
||
option - the value property of the option."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__select_option(
|
||
dropdown_selector,
|
||
option,
|
||
dropdown_by=dropdown_by,
|
||
option_by="value",
|
||
timeout=timeout,
|
||
)
|
||
|
||
def get_select_options(
|
||
self,
|
||
dropdown_selector,
|
||
attribute="text",
|
||
by="css selector",
|
||
timeout=None,
|
||
):
|
||
"""Returns a list of select options as attribute text (configurable).
|
||
@Params
|
||
dropdown_selector - The selector of the "select" element.
|
||
attribute - Choose from "text", "index", "value", or None (elements).
|
||
by - The "by" of the "select" selector to use. Default: "css selector".
|
||
timeout - Wait time for "select". If None: settings.SMALL_TIMEOUT."""
|
||
self.wait_for_ready_state_complete()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector = dropdown_selector
|
||
allowed_attributes = ["text", "index", "value", None]
|
||
if attribute not in allowed_attributes:
|
||
raise Exception("The attribute must be in %s" % allowed_attributes)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = self.wait_for_element(selector, by=by, timeout=timeout)
|
||
if element.tag_name.lower() != "select":
|
||
raise Exception(
|
||
'Element tag_name for get_select_options(selector) must be a '
|
||
'"select"! Actual tag_name found was: "%s"'
|
||
% element.tag_name.lower()
|
||
)
|
||
if by != "css selector":
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
option_selector = selector + " option"
|
||
option_elements = self.find_elements(option_selector)
|
||
if not attribute:
|
||
return option_elements
|
||
elif attribute == "text":
|
||
return [e.text for e in option_elements]
|
||
else:
|
||
return [e.get_attribute(attribute) for e in option_elements]
|
||
|
||
def load_html_string(self, html_string, new_page=True):
|
||
"""Loads an HTML string into the web browser.
|
||
If new_page==True, the page will switch to: "data:text/html,"
|
||
If new_page==False, will load HTML into the current page."""
|
||
self.wait_for_ready_state_complete()
|
||
new_lines = []
|
||
lines = html_string.split("\n")
|
||
for line in lines:
|
||
if not line.strip().startswith("//"):
|
||
new_lines.append(line)
|
||
html_string = "\n".join(new_lines)
|
||
soup = self.get_beautiful_soup(html_string)
|
||
found_base = False
|
||
links = soup.find_all("link")
|
||
href = None
|
||
for link in links:
|
||
if link.get("rel") == ["canonical"] and link.get("href"):
|
||
found_base = True
|
||
href = link.get("href")
|
||
href = self.get_domain_url(href)
|
||
if (
|
||
found_base
|
||
and html_string.count("<head>") == 1
|
||
and html_string.count("<base") == 0
|
||
):
|
||
html_string = html_string.replace(
|
||
"<head>", '<head><base href="%s">' % href
|
||
)
|
||
elif not found_base:
|
||
bases = soup.find_all("base")
|
||
for base in bases:
|
||
if base.get("href"):
|
||
href = base.get("href")
|
||
if href:
|
||
html_string = html_string.replace('base: "."', 'base: "%s"' % href)
|
||
|
||
soup = self.get_beautiful_soup(html_string)
|
||
scripts = soup.find_all("script")
|
||
for script in scripts:
|
||
if script.get("type") != "application/json":
|
||
html_string = html_string.replace(str(script), "")
|
||
soup = self.get_beautiful_soup(html_string)
|
||
|
||
found_head = False
|
||
found_body = False
|
||
html_head = None
|
||
html_body = None
|
||
if soup.head and len(str(soup.head)) > 12:
|
||
found_head = True
|
||
html_head = str(soup.head)
|
||
html_head = re.escape(html_head)
|
||
html_head = self.__escape_quotes_if_needed(html_head)
|
||
html_head = html_head.replace("\\ ", " ")
|
||
if soup.body and len(str(soup.body)) > 12:
|
||
found_body = True
|
||
html_body = str(soup.body)
|
||
html_body = html_body.replace("\xc2\xa0", " ")
|
||
html_body = html_body.replace("\xc2\xa1", "¡")
|
||
html_body = html_body.replace("\xc2\xa9", "©")
|
||
html_body = html_body.replace("\xc2\xb7", "·")
|
||
html_body = html_body.replace("\xc2\xbf", "¿")
|
||
html_body = html_body.replace("\xc3\x97", "×")
|
||
html_body = html_body.replace("\xc3\xb7", "÷")
|
||
html_body = re.escape(html_body)
|
||
html_body = self.__escape_quotes_if_needed(html_body)
|
||
html_body = html_body.replace("\\ ", " ")
|
||
html_string = re.escape(html_string)
|
||
html_string = self.__escape_quotes_if_needed(html_string)
|
||
html_string = html_string.replace("\\ ", " ")
|
||
|
||
if new_page:
|
||
self.open("data:text/html,<head></head><body><div></div></body>")
|
||
inner_head = """document.getElementsByTagName("head")[0].innerHTML"""
|
||
inner_body = """document.getElementsByTagName("body")[0].innerHTML"""
|
||
with suppress(Exception):
|
||
self.wait_for_element_present("body", timeout=1)
|
||
if not found_body:
|
||
self.execute_script('''%s = \"%s\"''' % (inner_body, html_string))
|
||
elif found_body and not found_head:
|
||
self.execute_script('''%s = \"%s\"''' % (inner_body, html_body))
|
||
elif found_body and found_head:
|
||
self.execute_script('''%s = \"%s\"''' % (inner_head, html_head))
|
||
time.sleep(0.02)
|
||
self.execute_script('''%s = \"%s\"''' % (inner_body, html_body))
|
||
else:
|
||
raise Exception("Logic Error!")
|
||
|
||
for script in scripts:
|
||
js_code = script.string
|
||
js_src = script.get("src")
|
||
if js_code and script.get("type") != "application/json":
|
||
js_code_lines = js_code.split("\n")
|
||
new_lines = []
|
||
for line in js_code_lines:
|
||
line = line.strip()
|
||
new_lines.append(line)
|
||
js_code = "\n".join(new_lines)
|
||
js_code = re.escape(js_code)
|
||
js_utils.add_js_code(self.driver, js_code)
|
||
elif js_src:
|
||
js_utils.add_js_link(self.driver, js_src)
|
||
else:
|
||
pass
|
||
|
||
def set_content(self, html_string, new_page=False):
|
||
"""Same as load_html_string(), but "new_page" defaults to False."""
|
||
self.load_html_string(html_string, new_page=new_page)
|
||
|
||
def load_html_file(self, html_file, new_page=True):
|
||
"""Loads a local html file into the browser from a relative file path.
|
||
If new_page==True, the page will switch to: "data:text/html,"
|
||
If new_page==False, will load HTML into the current page.
|
||
Local images and other local src content WILL BE IGNORED."""
|
||
self.__check_scope()
|
||
if self.__looks_like_a_page_url(html_file):
|
||
self.open(html_file)
|
||
return
|
||
if len(html_file) < 6 or not html_file.endswith(".html"):
|
||
raise Exception('Expecting a ".html" file!')
|
||
abs_path = os.path.abspath(".")
|
||
file_path = None
|
||
if abs_path in html_file:
|
||
file_path = html_file
|
||
else:
|
||
file_path = os.path.join(abs_path, html_file)
|
||
html_string = None
|
||
with open(file_path, "r") as f:
|
||
html_string = f.read().strip()
|
||
self.load_html_string(html_string, new_page)
|
||
|
||
def open_html_file(self, html_file):
|
||
"""Opens a local html file into the browser from a relative file path.
|
||
The URL displayed in the web browser will start with "file://"."""
|
||
self.__check_scope()
|
||
if self.__looks_like_a_page_url(html_file):
|
||
self.open(html_file)
|
||
return
|
||
if len(html_file) < 6 or not html_file.endswith(".html"):
|
||
raise Exception('Expecting a ".html" file!')
|
||
abs_path = os.path.abspath(".")
|
||
file_path = None
|
||
if abs_path in html_file:
|
||
file_path = html_file
|
||
else:
|
||
file_path = os.path.join(abs_path, html_file)
|
||
self.open("file://" + file_path)
|
||
|
||
def execute_script(self, script, *args, **kwargs):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.evaluate(script)
|
||
self._check_browser()
|
||
return self.driver.execute_script(script, *args, **kwargs)
|
||
|
||
def execute_cdp_cmd(self, script, *args, **kwargs):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
return self.driver.execute_cdp_cmd(script, *args, **kwargs)
|
||
|
||
def execute_async_script(self, script, timeout=None):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
if not timeout:
|
||
timeout = settings.EXTREME_TIMEOUT
|
||
return js_utils.execute_async_script(self.driver, script, timeout)
|
||
|
||
def safe_execute_script(self, script, *args, **kwargs):
|
||
"""When executing a script that contains a jQuery command,
|
||
it's important that the jQuery library has been loaded first.
|
||
This method will load jQuery if it wasn't already loaded."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
self.activate_jquery()
|
||
return self.driver.execute_script(script, *args, **kwargs)
|
||
|
||
def get_element_at_x_y(self, x, y):
|
||
"""Return element at current window's x,y coordinates."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
return self.execute_script(
|
||
"return document.elementFromPoint(%s, %s);" % (x, y)
|
||
)
|
||
|
||
def get_gui_element_rect(self, selector, by="css selector"):
|
||
"""Very similar to element.rect, but the x, y coordinates are
|
||
relative to the entire screen, rather than the browser window.
|
||
This is specifically for PyAutoGUI actions on the full screen.
|
||
(Note: There may be complications if iframes are involved.)"""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_gui_element_rect(selector)
|
||
element = self.wait_for_element_present(selector, by=by, timeout=1)
|
||
element_rect = element.rect
|
||
e_width = element_rect["width"]
|
||
e_height = element_rect["height"]
|
||
i_x = 0
|
||
i_y = 0
|
||
iframe_switch = False
|
||
if self.__is_in_frame():
|
||
self.switch_to_parent_frame()
|
||
if self.__is_in_frame():
|
||
raise Exception("Nested iframes breaks get_gui_element_rect!")
|
||
iframe_switch = True
|
||
iframe = self.wait_for_element_present("iframe", timeout=1)
|
||
i_x = iframe.rect["x"]
|
||
i_y = iframe.rect["y"]
|
||
window_rect = self.get_window_rect()
|
||
w_bottom_y = window_rect["y"] + window_rect["height"]
|
||
viewport_height = self.execute_script("return window.innerHeight;")
|
||
x = math.ceil(window_rect["x"] + i_x + element_rect["x"])
|
||
y = math.ceil(w_bottom_y - viewport_height + i_y + element_rect["y"])
|
||
y_scroll_offset = self.execute_script("return window.pageYOffset;")
|
||
y = int(y - y_scroll_offset)
|
||
if iframe_switch:
|
||
self.switch_to_frame()
|
||
if not self.is_element_present(selector, by=by):
|
||
self.switch_to_parent_frame()
|
||
return ({"height": e_height, "width": e_width, "x": x, "y": y})
|
||
|
||
def get_gui_element_center(self, selector, by="css selector"):
|
||
"""Returns the x, y coordinates of the element's center based
|
||
on the entire GUI / screen, rather than on the browser window.
|
||
This is specifically for PyAutoGUI actions on the full screen.
|
||
(Note: There may be complications if iframes are involved.)"""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_gui_element_center(selector)
|
||
element_rect = self.get_gui_element_rect(selector, by=by)
|
||
x = element_rect["x"] + (element_rect["width"] / 2.0) + 0.5
|
||
y = element_rect["y"] + (element_rect["height"] / 2.0) + 0.5
|
||
return (x, y)
|
||
|
||
def get_screen_rect(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_screen_rect()
|
||
self._check_browser()
|
||
return self.driver.get_screen_rect()
|
||
|
||
def get_window_rect(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_window_rect()
|
||
self._check_browser()
|
||
return self.driver.get_window_rect()
|
||
|
||
def get_window_size(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_window_size()
|
||
self._check_browser()
|
||
return self.driver.get_window_size()
|
||
|
||
def get_window_position(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_window_position()
|
||
self._check_browser()
|
||
return self.driver.get_window_position()
|
||
|
||
def set_window_rect(self, x, y, width, height):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.set_window_rect(x, y, width, height)
|
||
return
|
||
self._check_browser()
|
||
self.driver.set_window_rect(x, y, width, height)
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def set_window_size(self, width, height):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
self.driver.set_window_size(width, height)
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def set_window_position(self, x, y):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
self.driver.set_window_position(x, y)
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def maximize_window(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.maximize()
|
||
return
|
||
self._check_browser()
|
||
self.driver.maximize_window()
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def minimize_window(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.minimize()
|
||
return
|
||
self._check_browser()
|
||
self.driver.minimize_window()
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def reset_window_size(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.reset_window_size()
|
||
return
|
||
self._check_browser()
|
||
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.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def switch_to_frame(self, frame="iframe", timeout=None):
|
||
"""Wait for an iframe to appear, and switch to it. This should be
|
||
usable as a drop-in replacement for driver.switch_to.frame().
|
||
The iframe identifier can be a selector, an index, an id, a name,
|
||
or a web element, but scrolling to the iframe first will only occur
|
||
for visible iframes with a string selector.
|
||
@Params
|
||
frame - the frame element, name, id, index, or selector
|
||
timeout - the time to wait for the alert in seconds """
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
if self.undetectable:
|
||
time.sleep(0.05)
|
||
if isinstance(frame, str) and self.is_element_visible(frame):
|
||
try:
|
||
self.scroll_to(frame, timeout=1)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
if self.undetectable:
|
||
time.sleep(0.04)
|
||
except Exception:
|
||
time.sleep(0.02)
|
||
else:
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
if self.undetectable:
|
||
time.sleep(0.05)
|
||
if self.undetectable:
|
||
self.__uc_frame_layer += 1
|
||
if (
|
||
self.recorder_mode
|
||
and self._rec_overrides_switch
|
||
and self.__current_url_is_recordable()
|
||
):
|
||
r_a = self.get_session_storage_item("recorder_activated")
|
||
if r_a == "yes":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["sk_op", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
self.__set_c_from_switch = True
|
||
self.set_content_to_frame(frame, timeout=timeout)
|
||
self.__set_c_from_switch = False
|
||
origin = self.get_origin()
|
||
action = ["sw_fr", frame, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.035)
|
||
if self.undetectable:
|
||
time.sleep(0.035)
|
||
page_actions.switch_to_frame(self.driver, frame, timeout)
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.015)
|
||
if self.undetectable:
|
||
time.sleep(0.015)
|
||
|
||
def switch_to_default_content(self):
|
||
"""Brings driver control outside the current iframe.
|
||
If the driver is currently set inside an iframe or nested iframes,
|
||
then the driver control will exit from all entered iframes.
|
||
If the driver is not currently set in an iframe, nothing happens."""
|
||
self.__check_scope()
|
||
if (
|
||
self.recorder_mode
|
||
and self._rec_overrides_switch
|
||
and self.__current_url_is_recordable()
|
||
):
|
||
r_a = self.get_session_storage_item("recorder_activated")
|
||
if r_a == "yes":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
self.__set_c_from_switch = True
|
||
self.set_content_to_default()
|
||
self.__set_c_from_switch = False
|
||
origin = self.get_origin()
|
||
action = ["sw_dc", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return
|
||
self.driver.switch_to.default_content()
|
||
if self.undetectable:
|
||
self.__uc_frame_layer = 0
|
||
|
||
def switch_to_parent_frame(self):
|
||
"""Brings driver control outside the current iframe.
|
||
If the driver is currently set inside an iframe or nested iframes,
|
||
the driver control will be set to one level above the current frame.
|
||
If the driver is not currently set in an iframe, nothing happens."""
|
||
self.__check_scope()
|
||
if (
|
||
self.recorder_mode
|
||
and self._rec_overrides_switch
|
||
and self.__current_url_is_recordable()
|
||
):
|
||
r_a = self.get_session_storage_item("recorder_activated")
|
||
if r_a == "yes":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
self.__set_c_from_switch = True
|
||
self.set_content_to_default(nested=True)
|
||
self.__set_c_from_switch = False
|
||
origin = self.get_origin()
|
||
action = ["sw_pf", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return
|
||
self.driver.switch_to.parent_frame()
|
||
if self.undetectable:
|
||
self.__uc_frame_layer -= 1
|
||
if self.__uc_frame_layer < 0:
|
||
self.__uc_frame_layer = 0
|
||
|
||
@contextmanager
|
||
def frame_switch(self, frame, timeout=None):
|
||
""" Context Manager for switching into iframes.
|
||
Usage example:
|
||
with self.frame_switch("iframe"):
|
||
# Perform actions here that should be done within the iframe.
|
||
# The iframe is automatically exited after the "with" block ends.
|
||
"""
|
||
if self.recorder_mode:
|
||
self.__frame_switch_layer += 1
|
||
if self.__frame_switch_layer >= 2:
|
||
self.__frame_switch_multi = True
|
||
self.switch_to_frame(frame, timeout=timeout)
|
||
if self.undetectable:
|
||
self.__uc_frame_layer += 1
|
||
yield
|
||
if self.undetectable:
|
||
self.__uc_frame_layer -= 1
|
||
if self.__uc_frame_layer < 0:
|
||
self.__uc_frame_layer = 0
|
||
self.switch_to_parent_frame()
|
||
if self.recorder_mode:
|
||
self.__frame_switch_layer -= 1
|
||
if self.__frame_switch_layer < 0:
|
||
self.__frame_switch_layer = 0
|
||
self.__frame_switch_multi = False
|
||
if self.__frame_switch_layer == 0 and self.__frame_switch_multi:
|
||
self.refresh()
|
||
self.__frame_switch_multi = False
|
||
|
||
def set_content_to_frame(self, frame, timeout=None):
|
||
"""Replaces the page html with an iframe's html from that page.
|
||
If the iframe contains an "src" field that includes a valid URL,
|
||
then instead of replacing the current html, this method will then
|
||
open up the "src" URL of the iframe in a new browser tab.
|
||
To return to default content, use: self.set_content_to_default().
|
||
This method also sets the state of the browser window so that the
|
||
self.set_content_to_default() method can bring the user back to
|
||
the original content displayed, which is similar to how the methods
|
||
self.switch_to_frame(frame) and self.switch_to_default_content()
|
||
work together to get the user into frames and out of all of them."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
current_url = self.get_current_url()
|
||
c_tab = self.driver.current_window_handle
|
||
current_page_source = self.get_page_source()
|
||
self.execute_script("document.cframe_swap = 0;")
|
||
page_actions.switch_to_frame(self.driver, frame, timeout)
|
||
iframe_html = self.get_page_source()
|
||
self.driver.switch_to.default_content()
|
||
self.wait_for_ready_state_complete()
|
||
frame_found = False
|
||
o_frame = frame
|
||
if self.is_element_present(frame):
|
||
frame_found = True
|
||
elif " " not in frame:
|
||
frame = 'iframe[name="%s"]' % frame
|
||
if self.is_element_present(frame):
|
||
frame_found = True
|
||
time_stamp = 0
|
||
url = None
|
||
if frame_found:
|
||
url = self.execute_script(
|
||
"""return document.querySelector('%s').src;""" % frame
|
||
)
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
pass
|
||
else:
|
||
url = None
|
||
cframe_tab = False
|
||
if url:
|
||
cframe_tab = True
|
||
self.__page_sources.append([current_url, current_page_source, c_tab])
|
||
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["sk_op", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
time_stamp = str(int(time_stamp) + 1)
|
||
|
||
if cframe_tab:
|
||
self.execute_script("document.cframe_tab = 1;")
|
||
self.open_new_window(switch_to=True)
|
||
self.open(url)
|
||
self.execute_script("document.cframe_tab = 1;")
|
||
else:
|
||
self.set_content(iframe_html)
|
||
if not self.execute_script("return document.cframe_swap;"):
|
||
self.execute_script("document.cframe_swap = 1;")
|
||
else:
|
||
self.execute_script("document.cframe_swap += 1;")
|
||
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
origin = self.get_origin()
|
||
action = ["s_c_f", o_frame, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def set_content_to_default(self, nested=False):
|
||
"""After using self.set_content_to_frame(), this reverts the page back.
|
||
If self.set_content_to_frame() hasn't been called here, only refreshes.
|
||
If "nested" is set to True when the content is set to a nested iframe,
|
||
then the page control will only exit from the current iframe entered,
|
||
instead of exiting out of all iframes entered."""
|
||
self.__check_scope()
|
||
swap_cnt = self.execute_script("return document.cframe_swap;")
|
||
tab_sta = self.execute_script("return document.cframe_tab;")
|
||
time_stamp = 0
|
||
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["sk_op", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
time_stamp = str(int(time_stamp) + 1)
|
||
|
||
if not nested:
|
||
# Sets the page to the outer-most content.
|
||
# If page control was inside nested iframes, exits them all.
|
||
# If only in one iframe, has the same effect as nested=True.
|
||
if (
|
||
len(self.__page_sources) > 0
|
||
and (
|
||
(swap_cnt and int(swap_cnt) > 0)
|
||
or (tab_sta and int(tab_sta) > 0)
|
||
)
|
||
):
|
||
past_content = self.__page_sources[0]
|
||
past_url = past_content[0]
|
||
past_source = past_content[1]
|
||
past_tab = past_content[2]
|
||
current_tab = self.driver.current_window_handle
|
||
if not current_tab == past_tab:
|
||
if past_tab in self.driver.window_handles:
|
||
self.switch_to_window(past_tab)
|
||
url_of_past_tab = self.get_current_url()
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
if url_of_past_tab == past_url:
|
||
self.set_content(past_source)
|
||
else:
|
||
self.refresh_page()
|
||
else:
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
self.refresh_page()
|
||
self.execute_script("document.cframe_swap = 0;")
|
||
self.__page_sources = []
|
||
else:
|
||
# (If Nested is True)
|
||
# Sets the page to the content outside the current nested iframe.
|
||
# If only in one iframe, has the same effect as nested=True.
|
||
just_refresh = False
|
||
if swap_cnt and int(swap_cnt) > 0 and len(self.__page_sources) > 0:
|
||
self.execute_script("document.cframe_swap -= 1;")
|
||
current_url = self.get_current_url()
|
||
past_content = self.__page_sources.pop()
|
||
past_url = past_content[0]
|
||
past_source = past_content[1]
|
||
if current_url == past_url:
|
||
self.set_content(past_source)
|
||
else:
|
||
just_refresh = True
|
||
elif tab_sta and int(tab_sta) > 0 and len(self.__page_sources) > 0:
|
||
past_content = self.__page_sources.pop()
|
||
past_tab = past_content[2]
|
||
if past_tab in self.driver.window_handles:
|
||
self.switch_to_window(past_tab)
|
||
else:
|
||
just_refresh = True
|
||
else:
|
||
just_refresh = True
|
||
if just_refresh:
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
self.refresh_page()
|
||
self.execute_script("document.cframe_swap = 0;")
|
||
self.__page_sources = []
|
||
|
||
if self.recorder_mode and not self.__set_c_from_switch:
|
||
origin = self.get_origin()
|
||
action = ["s_c_d", nested, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def set_content_to_default_content(self, nested=False):
|
||
"""Same as self.set_content_to_default()."""
|
||
self.set_content_to_default(nested=nested)
|
||
|
||
def set_content_to_parent(self):
|
||
"""Same as self.set_content_to_parent_frame().
|
||
Same as self.set_content_to_default(nested=True).
|
||
Sets the page to the content outside the current nested iframe.
|
||
Reverts self.set_content_to_frame()."""
|
||
self.set_content_to_default(nested=True)
|
||
|
||
def set_content_to_parent_frame(self):
|
||
"""Same as self.set_content_to_parent().
|
||
Same as self.set_content_to_default(nested=True).
|
||
Sets the page to the content outside the current nested iframe.
|
||
Reverts self.set_content_to_frame()."""
|
||
self.set_content_to_default(nested=True)
|
||
|
||
def open_new_window(self, switch_to=True):
|
||
"""Opens a new browser tab/window and switches to it by default."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.open_new_tab(switch_to=switch_to)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if switch_to:
|
||
try:
|
||
self.driver.switch_to.new_window("tab")
|
||
except Exception:
|
||
self.driver.execute_script("window.open('');")
|
||
self.switch_to_newest_window()
|
||
else:
|
||
self.driver.execute_script("window.open('');")
|
||
time.sleep(0.01)
|
||
if self.browser == "safari":
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def switch_to_window(self, window, timeout=None):
|
||
"""Switches control of the browser to the specified window.
|
||
The window can be an integer: 0 -> 1st tab, 1 -> 2nd tab, etc...
|
||
Or it can be a list item from self.driver.window_handles"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed() and not isinstance(window, str):
|
||
self.cdp.switch_to_tab(window)
|
||
return
|
||
page_actions.switch_to_window(self.driver, window, timeout)
|
||
|
||
def switch_to_default_window(self):
|
||
self.switch_to_window(0)
|
||
|
||
def switch_to_newest_window(self):
|
||
self.switch_to_window(-1)
|
||
|
||
def get_new_driver(
|
||
self,
|
||
browser=None,
|
||
headless=None,
|
||
locale_code=None,
|
||
protocol=None,
|
||
servername=None,
|
||
port=None,
|
||
proxy=None,
|
||
proxy_bypass_list=None,
|
||
proxy_pac_url=None,
|
||
multi_proxy=None,
|
||
agent=None,
|
||
switch_to=True,
|
||
cap_file=None,
|
||
cap_string=None,
|
||
recorder_ext=None,
|
||
disable_cookies=None,
|
||
disable_js=None,
|
||
disable_csp=None,
|
||
enable_ws=None,
|
||
enable_sync=None,
|
||
use_auto_ext=None,
|
||
undetectable=None,
|
||
uc_cdp_events=None,
|
||
uc_subprocess=None,
|
||
log_cdp_events=None,
|
||
no_sandbox=None,
|
||
disable_gpu=None,
|
||
headless1=None,
|
||
headless2=None,
|
||
incognito=None,
|
||
guest_mode=None,
|
||
dark_mode=None,
|
||
devtools=None,
|
||
remote_debug=None,
|
||
enable_3d_apis=None,
|
||
swiftshader=None,
|
||
ad_block_on=None,
|
||
host_resolver_rules=None,
|
||
block_images=None,
|
||
do_not_track=None,
|
||
chromium_arg=None,
|
||
firefox_arg=None,
|
||
firefox_pref=None,
|
||
user_data_dir=None,
|
||
extension_zip=None,
|
||
extension_dir=None,
|
||
disable_features=None,
|
||
binary_location=None,
|
||
driver_version=None,
|
||
page_load_strategy=None,
|
||
use_wire=None,
|
||
external_pdf=None,
|
||
is_mobile=None,
|
||
d_width=None,
|
||
d_height=None,
|
||
d_p_r=None,
|
||
**kwargs,
|
||
):
|
||
"""This method spins up an extra browser for tests that require
|
||
more than one. The first browser is already provided by tests
|
||
that import base_case.BaseCase from seleniumbase. If parameters
|
||
aren't specified, the method uses the same as the default driver.
|
||
@Params
|
||
browser - the browser to use. (Ex: "chrome", "firefox")
|
||
headless - the option to run webdriver in headless mode
|
||
locale_code - the Language Locale Code for the web browser
|
||
protocol - if using a Selenium Grid, set the host protocol here
|
||
servername - if using a Selenium Grid, set the host address here
|
||
port - if using a Selenium Grid, set the host port here
|
||
proxy - if using a proxy server, specify the "host:port" combo here
|
||
proxy_bypass_list - ";"-separated hosts to bypass (Eg. "*.foo.com")
|
||
proxy_pac_url - designates the proxy PAC URL to use (Chromium-only)
|
||
multi_proxy - allow multiple proxies with auth while multi-threaded
|
||
switch_to - the option to switch to the new driver (default = True)
|
||
cap_file - the file containing desired capabilities for the browser
|
||
cap_string - the string with desired capabilities for the browser
|
||
recorder_ext - the option to enable the SBase Recorder extension
|
||
disable_cookies - the option to disable Cookies (May break things!)
|
||
disable_js - the option to disable JavaScript (May break websites!)
|
||
disable_csp - an option to disable Chrome's Content Security Policy
|
||
enable_ws - the option to enable the Web Security feature (Chrome)
|
||
enable_sync - the option to enable the Chrome Sync feature (Chrome)
|
||
use_auto_ext - the option to enable Chrome's Automation Extension
|
||
undetectable - the option to use an undetectable chromedriver
|
||
uc_cdp_events - capture CDP events in "undetectable" mode (Chrome)
|
||
uc_subprocess - use the undetectable chromedriver as a subprocess
|
||
log_cdp_events - capture {"performance": "ALL", "browser": "ALL"})
|
||
no_sandbox - the option to enable the "No-Sandbox" feature (Chrome)
|
||
disable_gpu - the option to enable Chrome's "Disable GPU" feature
|
||
headless1 - the option to use the older headless mode (Chromium)
|
||
headless2 - the option to use the newer headless mode (Chromium)
|
||
incognito - the option to enable Chrome's Incognito mode (Chrome)
|
||
guest_mode - the option to enable Chrome's Guest mode (Chrome)
|
||
dark_mode - the option to enable Chrome's Dark mode (Chrome)
|
||
devtools - the option to open Chrome's DevTools on start (Chrome)
|
||
remote_debug - the option to enable Chrome's Remote Debugger
|
||
enable_3d_apis - the option to enable WebGL and 3D APIs (Chrome)
|
||
swiftshader - the option to use Chrome's swiftshader (Chrome-only)
|
||
ad_block_on - the option to block ads from loading (Chromium-only)
|
||
host_resolver_rules - Configure host-resolver-rules (Chromium-only)
|
||
block_images - the option to block images from loading (Chrome)
|
||
do_not_track - indicate that websites should not track you (Chrome)
|
||
chromium_arg - the option to add a Chromium arg to Chrome/Edge
|
||
firefox_arg - the option to add a Firefox arg to Firefox runs
|
||
firefox_pref - the option to add a Firefox pref:value set (Firefox)
|
||
user_data_dir - Chrome's User Data Directory to use (Chrome-only)
|
||
extension_zip - A Chrome Extension ZIP file to use (Chrome-only)
|
||
extension_dir - A Chrome Extension folder to use (Chrome-only)
|
||
disable_features - the option to disable features on Chrome/Edge
|
||
binary_location - the path of the browser binary to use (Chromium)
|
||
driver_version - the chromedriver or uc_driver version to force
|
||
page_load_strategy - the option to change pageLoadStrategy (Chrome)
|
||
use_wire - Use selenium-wire webdriver instead of the selenium one
|
||
external_pdf - "plugins.always_open_pdf_externally": True. (Chrome)
|
||
is_mobile - the option to use the mobile emulator (Chrome-only)
|
||
d_width - the device width of the mobile emulator (Chrome-only)
|
||
d_height - the device height of the mobile emulator (Chrome-only)
|
||
d_p_r - the device pixel ratio of the mobile emulator (Chrome-only)
|
||
"""
|
||
self.__check_scope()
|
||
if self.browser == "remote" and self.servername == "localhost":
|
||
raise Exception(
|
||
'Cannot use "remote" browser driver on localhost!'
|
||
" Did you mean to connect to a remote Grid server"
|
||
" such as BrowserStack or Sauce Labs?"
|
||
' If so, you must specify the "server" and "port"'
|
||
" parameters on the command line! "
|
||
"Example: "
|
||
"--server=user:key@hub.browserstack.com --port=80"
|
||
)
|
||
browserstack_ref = "https://browserstack.com/automate/capabilities"
|
||
sauce_labs_ref = (
|
||
"https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/"
|
||
)
|
||
if self.browser == "remote" and not (self.cap_file or self.cap_string):
|
||
raise Exception(
|
||
"Need to specify a desired capabilities file when "
|
||
'using "--browser=remote". Add "--cap_file=FILE". '
|
||
"File should be in the Python format used by: "
|
||
"%s OR %s \n"
|
||
"(See SeleniumBase/examples/capabilities/sample_cap_file_BS.py"
|
||
" and SeleniumBase/examples/capabilities/sample_cap_file_SL.py"
|
||
" for examples!)"
|
||
% (browserstack_ref, sauce_labs_ref)
|
||
)
|
||
shortcuts = ["dark", "guest", "locale", "mobile", "pls", "uc", "wire"]
|
||
if kwargs:
|
||
for key in kwargs.keys():
|
||
if key not in shortcuts:
|
||
raise TypeError("Unexpected keyword argument '%s'" % key)
|
||
if browser is None:
|
||
browser = self.browser
|
||
browser_name = browser
|
||
if headless is None:
|
||
headless = self.headless
|
||
if locale_code is None:
|
||
locale_code = self.locale_code
|
||
if "locale" in kwargs and not locale_code:
|
||
locale_code = kwargs["locale"]
|
||
if protocol is None:
|
||
protocol = self.protocol
|
||
if servername is None:
|
||
servername = self.servername
|
||
if port is None:
|
||
port = self.port
|
||
use_grid = False
|
||
if servername != "localhost":
|
||
# Use Selenium Grid (Use "127.0.0.1" for localhost Grid)
|
||
use_grid = True
|
||
proxy_string = proxy
|
||
if proxy_string is None:
|
||
proxy_string = self.proxy_string
|
||
if proxy_bypass_list is None:
|
||
proxy_bypass_list = self.proxy_bypass_list
|
||
if proxy_pac_url is None:
|
||
proxy_pac_url = self.proxy_pac_url
|
||
if multi_proxy is None:
|
||
multi_proxy = self.multi_proxy
|
||
user_agent = agent
|
||
if user_agent is None:
|
||
user_agent = self.user_agent
|
||
if recorder_ext is None:
|
||
recorder_ext = self.recorder_ext
|
||
if disable_cookies is None:
|
||
disable_cookies = self.disable_cookies
|
||
if disable_js is None:
|
||
disable_js = self.disable_js
|
||
if disable_csp is None:
|
||
disable_csp = self.disable_csp
|
||
if enable_ws is None:
|
||
enable_ws = self.enable_ws
|
||
if enable_sync is None:
|
||
enable_sync = self.enable_sync
|
||
if use_auto_ext is None:
|
||
use_auto_ext = self.use_auto_ext
|
||
if undetectable is None:
|
||
undetectable = self.undetectable
|
||
if uc_cdp_events is None:
|
||
uc_cdp_events = self.uc_cdp_events
|
||
if uc_subprocess is None:
|
||
uc_subprocess = self.uc_subprocess
|
||
if "uc" in kwargs and not undetectable:
|
||
undetectable = kwargs["uc"]
|
||
if log_cdp_events is None:
|
||
log_cdp_events = self.log_cdp_events
|
||
if no_sandbox is None:
|
||
no_sandbox = self.no_sandbox
|
||
if disable_gpu is None:
|
||
disable_gpu = self.disable_gpu
|
||
if headless1 is None:
|
||
headless1 = self.headless1
|
||
if headless2 is None:
|
||
headless2 = self.headless2
|
||
if incognito is None:
|
||
incognito = self.incognito
|
||
if guest_mode is None:
|
||
guest_mode = self.guest_mode
|
||
if "guest" in kwargs and not guest_mode:
|
||
guest_mode = kwargs["guest"]
|
||
if dark_mode is None:
|
||
dark_mode = self.dark_mode
|
||
if "dark" in kwargs and not dark_mode:
|
||
dark_mode = kwargs["dark"]
|
||
if devtools is None:
|
||
devtools = self.devtools
|
||
if remote_debug is None:
|
||
remote_debug = self.remote_debug
|
||
if enable_3d_apis is None:
|
||
enable_3d_apis = self.enable_3d_apis
|
||
if swiftshader is None:
|
||
swiftshader = self._swiftshader
|
||
if ad_block_on is None:
|
||
ad_block_on = self.ad_block_on
|
||
if host_resolver_rules is None:
|
||
host_resolver_rules = self.host_resolver_rules
|
||
if block_images is None:
|
||
block_images = self.block_images
|
||
if do_not_track is None:
|
||
do_not_track = self.do_not_track
|
||
if chromium_arg is None:
|
||
chromium_arg = self.chromium_arg
|
||
if firefox_arg is None:
|
||
firefox_arg = self.firefox_arg
|
||
if firefox_pref is None:
|
||
firefox_pref = self.firefox_pref
|
||
if user_data_dir is None:
|
||
user_data_dir = self.user_data_dir
|
||
if extension_zip is None:
|
||
extension_zip = self.extension_zip
|
||
if extension_dir is None:
|
||
extension_dir = self.extension_dir
|
||
if disable_features is None:
|
||
disable_features = self.disable_features
|
||
if binary_location is None:
|
||
binary_location = self.binary_location
|
||
if driver_version is None:
|
||
driver_version = self.driver_version
|
||
if page_load_strategy is None:
|
||
page_load_strategy = self.page_load_strategy
|
||
if "pls" in kwargs and not page_load_strategy:
|
||
page_load_strategy = kwargs["pls"]
|
||
if use_wire is None:
|
||
use_wire = self.use_wire
|
||
if "wire" in kwargs and not use_wire:
|
||
use_wire = kwargs["wire"]
|
||
if external_pdf is None:
|
||
external_pdf = self.external_pdf
|
||
test_id = self.__get_test_id()
|
||
if cap_file is None:
|
||
cap_file = self.cap_file
|
||
if cap_string is None:
|
||
cap_string = self.cap_string
|
||
if is_mobile is None:
|
||
is_mobile = self.mobile_emulator
|
||
if "mobile" in kwargs and not is_mobile:
|
||
is_mobile = kwargs["mobile"]
|
||
if d_width is None:
|
||
d_width = self.__device_width
|
||
if d_height is None:
|
||
d_height = self.__device_height
|
||
if d_p_r is None:
|
||
d_p_r = self.__device_pixel_ratio
|
||
if is_mobile and not user_agent:
|
||
# Use a Pixel user agent by default if not specified
|
||
user_agent = constants.Mobile.AGENT
|
||
valid_browsers = constants.ValidBrowsers.valid_browsers
|
||
if browser_name not in valid_browsers:
|
||
raise Exception(
|
||
"Browser: {%s} is not a valid browser option. "
|
||
"Valid options = {%s}" % (browser, valid_browsers)
|
||
)
|
||
# Launch a web browser
|
||
new_driver = browser_launcher.get_driver(
|
||
browser_name=browser_name,
|
||
headless=headless,
|
||
locale_code=locale_code,
|
||
use_grid=use_grid,
|
||
protocol=protocol,
|
||
servername=servername,
|
||
port=port,
|
||
proxy_string=proxy_string,
|
||
proxy_bypass_list=proxy_bypass_list,
|
||
proxy_pac_url=proxy_pac_url,
|
||
multi_proxy=multi_proxy,
|
||
user_agent=user_agent,
|
||
cap_file=cap_file,
|
||
cap_string=cap_string,
|
||
recorder_ext=recorder_ext,
|
||
disable_cookies=disable_cookies,
|
||
disable_js=disable_js,
|
||
disable_csp=disable_csp,
|
||
enable_ws=enable_ws,
|
||
enable_sync=enable_sync,
|
||
use_auto_ext=use_auto_ext,
|
||
undetectable=undetectable,
|
||
uc_cdp_events=uc_cdp_events,
|
||
uc_subprocess=uc_subprocess,
|
||
log_cdp_events=log_cdp_events,
|
||
no_sandbox=no_sandbox,
|
||
disable_gpu=disable_gpu,
|
||
headless1=headless1,
|
||
headless2=headless2,
|
||
incognito=incognito,
|
||
guest_mode=guest_mode,
|
||
dark_mode=dark_mode,
|
||
devtools=devtools,
|
||
remote_debug=remote_debug,
|
||
enable_3d_apis=enable_3d_apis,
|
||
swiftshader=swiftshader,
|
||
ad_block_on=ad_block_on,
|
||
host_resolver_rules=host_resolver_rules,
|
||
block_images=block_images,
|
||
do_not_track=do_not_track,
|
||
chromium_arg=chromium_arg,
|
||
firefox_arg=firefox_arg,
|
||
firefox_pref=firefox_pref,
|
||
user_data_dir=user_data_dir,
|
||
extension_zip=extension_zip,
|
||
extension_dir=extension_dir,
|
||
disable_features=disable_features,
|
||
binary_location=binary_location,
|
||
driver_version=driver_version,
|
||
page_load_strategy=page_load_strategy,
|
||
use_wire=use_wire,
|
||
external_pdf=external_pdf,
|
||
test_id=test_id,
|
||
mobile_emulator=is_mobile,
|
||
device_width=d_width,
|
||
device_height=d_height,
|
||
device_pixel_ratio=d_p_r,
|
||
browser=browser_name,
|
||
)
|
||
self._drivers_list.append(new_driver)
|
||
self._drivers_browser_map[new_driver] = browser_name
|
||
if switch_to:
|
||
self.driver = new_driver
|
||
self.browser = browser_name
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
# Make sure the invisible browser window is big enough
|
||
width = settings.HEADLESS_START_WIDTH
|
||
height = settings.HEADLESS_START_HEIGHT
|
||
if self.browser != "chrome" and self.browser != "edge":
|
||
try:
|
||
self.driver.set_window_size(width, height)
|
||
# self.wait_for_ready_state_complete()
|
||
except Exception:
|
||
# This shouldn't fail, but in case it does,
|
||
# get safely through setUp() so that
|
||
# WebDrivers can get closed during tearDown().
|
||
pass
|
||
else:
|
||
width = settings.CHROME_START_WIDTH
|
||
height = settings.CHROME_START_HEIGHT
|
||
if self.is_chromium():
|
||
try:
|
||
if self.maximize_option:
|
||
self.driver.maximize_window()
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
pass # Now handled in browser_launcher.py
|
||
# self.driver.set_window_size(width, height)
|
||
except Exception:
|
||
pass # Keep existing browser resolution
|
||
elif self.browser == "firefox":
|
||
try:
|
||
if self.maximize_option:
|
||
self.driver.maximize_window()
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
with suppress(Exception):
|
||
self.driver.set_window_size(width, height)
|
||
except Exception:
|
||
pass # Keep existing browser resolution
|
||
elif self.browser == "safari":
|
||
if self.maximize_option:
|
||
try:
|
||
self.driver.maximize_window()
|
||
self.wait_for_ready_state_complete()
|
||
except Exception:
|
||
pass # Keep existing browser resolution
|
||
else:
|
||
with suppress(Exception):
|
||
self.driver.set_window_rect(10, 46, width, height)
|
||
if self.start_page and len(self.start_page) >= 4:
|
||
if page_utils.is_valid_url(self.start_page):
|
||
self.open(self.start_page)
|
||
else:
|
||
new_start_page = "https://" + self.start_page
|
||
if page_utils.is_valid_url(new_start_page):
|
||
self.__dont_record_open = True
|
||
self.open(new_start_page)
|
||
self.__dont_record_open = False
|
||
if undetectable:
|
||
if hasattr(new_driver, "cdp"):
|
||
self.cdp = new_driver.cdp
|
||
if hasattr(new_driver, "uc_open"):
|
||
self.uc_open = new_driver.uc_open
|
||
if hasattr(new_driver, "uc_open_with_tab"):
|
||
self.uc_open_with_tab = new_driver.uc_open_with_tab
|
||
if hasattr(new_driver, "uc_open_with_reconnect"):
|
||
self.uc_open_with_reconnect = new_driver.uc_open_with_reconnect
|
||
if hasattr(new_driver, "uc_open_with_cdp_mode"):
|
||
self.uc_open_with_cdp_mode = new_driver.uc_open_with_cdp_mode
|
||
if hasattr(new_driver, "uc_open_with_disconnect"):
|
||
self.uc_open_with_disconnect = (
|
||
new_driver.uc_open_with_disconnect
|
||
)
|
||
if hasattr(new_driver, "reconnect"):
|
||
self.reconnect = new_driver.reconnect
|
||
if hasattr(new_driver, "disconnect"):
|
||
self.disconnect = new_driver.disconnect
|
||
if hasattr(new_driver, "connect"):
|
||
self.connect = new_driver.connect
|
||
if hasattr(new_driver, "uc_click"):
|
||
self.uc_click = new_driver.uc_click
|
||
if hasattr(new_driver, "uc_gui_press_key"):
|
||
self.uc_gui_press_key = new_driver.uc_gui_press_key
|
||
if hasattr(new_driver, "uc_gui_press_keys"):
|
||
self.uc_gui_press_keys = new_driver.uc_gui_press_keys
|
||
if hasattr(new_driver, "uc_gui_write"):
|
||
self.uc_gui_write = new_driver.uc_gui_write
|
||
if hasattr(new_driver, "uc_gui_click_x_y"):
|
||
self.uc_gui_click_x_y = new_driver.uc_gui_click_x_y
|
||
if hasattr(new_driver, "uc_gui_click_captcha"):
|
||
self.uc_gui_click_captcha = new_driver.uc_gui_click_captcha
|
||
if hasattr(new_driver, "uc_gui_click_cf"):
|
||
self.uc_gui_click_cf = new_driver.uc_gui_click_cf
|
||
if hasattr(new_driver, "uc_gui_click_rc"):
|
||
self.uc_gui_click_rc = new_driver.uc_gui_click_rc
|
||
if hasattr(new_driver, "uc_gui_handle_captcha"):
|
||
self.uc_gui_handle_captcha = new_driver.uc_gui_handle_captcha
|
||
if hasattr(new_driver, "uc_gui_handle_cf"):
|
||
self.uc_gui_handle_cf = new_driver.uc_gui_handle_cf
|
||
if hasattr(new_driver, "uc_gui_handle_rc"):
|
||
self.uc_gui_handle_rc = new_driver.uc_gui_handle_rc
|
||
if hasattr(new_driver, "uc_switch_to_frame"):
|
||
self.uc_switch_to_frame = new_driver.uc_switch_to_frame
|
||
return new_driver
|
||
|
||
def switch_to_driver(self, driver):
|
||
"""Switches control of the browser to the specified driver.
|
||
Also sets the self.driver variable to the specified driver.
|
||
You may need this if using self.get_new_driver() in your code."""
|
||
self.__check_scope()
|
||
self.driver = driver
|
||
if self.driver in self._drivers_browser_map:
|
||
self.browser = self._drivers_browser_map[self.driver]
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp._swap_driver(self.driver)
|
||
self.bring_active_window_to_front()
|
||
|
||
def switch_to_default_driver(self):
|
||
"""Sets self.driver to the default/initial driver."""
|
||
self.__check_scope()
|
||
self.driver = self._default_driver
|
||
if self.driver in self._drivers_browser_map:
|
||
self.browser = self._drivers_browser_map[self.driver]
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp._swap_driver(self.driver)
|
||
self.bring_active_window_to_front()
|
||
|
||
def save_screenshot(
|
||
self, name, folder=None, selector=None, by="css selector"
|
||
):
|
||
"""Saves a screenshot of the current page.
|
||
If no folder is specified, uses the folder where pytest was called.
|
||
The screenshot will include the entire page unless a selector is given.
|
||
If a provided selector is not found, then takes a full-page screenshot.
|
||
If the folder provided doesn't exist, it will get created.
|
||
The screenshot will be in PNG format: (*.png)"""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.save_screenshot(name, folder=folder, selector=selector)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if selector and by:
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if page_actions.is_element_present(self.driver, selector, by):
|
||
return page_actions.save_screenshot(
|
||
self.driver, name, folder, selector, by
|
||
)
|
||
if self.recorder_mode:
|
||
url = self.get_current_url()
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
if not folder:
|
||
action = ["s_scr", name, origin, time_stamp]
|
||
else:
|
||
name_folder = [name, folder]
|
||
action = ["ss_tf", name_folder, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return page_actions.save_screenshot(self.driver, name, folder)
|
||
|
||
def save_screenshot_to_logs(
|
||
self, name=None, selector=None, by="css selector"
|
||
):
|
||
"""Saves a screenshot of the current page to the "latest_logs/" folder.
|
||
Naming is automatic:
|
||
If NO NAME provided: "_1_screenshot.png", "_2_screenshot.png", etc.
|
||
If NAME IS provided, it becomes: "_1_name.png", "_2_name.png", etc.
|
||
The screenshot will include the entire page unless a selector is given.
|
||
If a provided selector is not found, then takes a full-page screenshot.
|
||
(The last_page / failure screenshot is always "screenshot.png")
|
||
The screenshot will be in PNG format."""
|
||
self.wait_for_ready_state_complete()
|
||
test_logpath = os.path.join(self.log_path, self.__get_test_id())
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if name:
|
||
name = str(name)
|
||
self.__screenshot_count += 1
|
||
if not name or len(name) == 0:
|
||
name = "_%s_screenshot.png" % self.__screenshot_count
|
||
else:
|
||
pre_name = "_%s_" % self.__screenshot_count
|
||
if len(name) >= 4 and name[-4:].lower() == ".png":
|
||
name = name[:-4]
|
||
if len(name) == 0:
|
||
name = "screenshot"
|
||
name = "%s%s.png" % (pre_name, name)
|
||
if selector and by:
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if page_actions.is_element_present(self.driver, selector, by):
|
||
return page_actions.save_screenshot(
|
||
self.driver, name, test_logpath, selector, by
|
||
)
|
||
if self.recorder_mode:
|
||
url = self.get_current_url()
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["ss_tl", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
sb_config._has_logs = True
|
||
return page_actions.save_screenshot(self.driver, name, test_logpath)
|
||
|
||
def save_data_to_logs(self, data, file_name=None):
|
||
"""Saves data to the "latest_logs/" data folder of the current test.
|
||
If no file_name, file_name becomes: "data_1.txt", "data_2.txt", etc.
|
||
Useful variables for getting the "latest_logs/" or test data folders:
|
||
self.log_path OR self.log_abspath (For the top-level folder)
|
||
self.data_path OR self.data_abspath (Individual test folders)
|
||
If a file_name is given with no extension, adds ".txt" to the end."""
|
||
test_logpath = os.path.join(self.log_path, self.__get_test_id())
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if file_name:
|
||
file_name = str(file_name)
|
||
if not file_name or len(file_name) == 0:
|
||
self.__logs_data_count += 1
|
||
file_name = "data_%s.txt" % self.__logs_data_count
|
||
elif "." not in file_name:
|
||
file_name = "%s.txt" % file_name
|
||
self.__last_data_file = file_name
|
||
sb_config._has_logs = True
|
||
destination_folder = test_logpath
|
||
page_utils._save_data_as(data, destination_folder, file_name)
|
||
|
||
def append_data_to_logs(self, data, file_name=None):
|
||
"""Saves data to the "latest_logs/" folder of the current test.
|
||
If no file_name, file_name becomes the last data file used in
|
||
save_data_to_logs() or append_data_to_logs().
|
||
If neither method was called before, creates "data_1.txt"."""
|
||
test_logpath = os.path.join(self.log_path, self.__get_test_id())
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if file_name:
|
||
file_name = str(file_name)
|
||
if (not file_name or len(file_name) == 0) and self.__last_data_file:
|
||
file_name = self.__last_data_file
|
||
elif not file_name or len(file_name) == 0:
|
||
if self.__logs_data_count == 0:
|
||
self.__logs_data_count += 1
|
||
file_name = "data_%s.txt" % self.__logs_data_count
|
||
elif "." not in file_name:
|
||
file_name = "%s.txt" % file_name
|
||
self.__last_data_file = file_name
|
||
sb_config._has_logs = True
|
||
destination_folder = test_logpath
|
||
page_utils._append_data_to_file(data, destination_folder, file_name)
|
||
|
||
def save_page_source(self, name, folder=None):
|
||
"""Saves the page HTML to the current directory (or given subfolder).
|
||
If the folder specified doesn't exist, it will get created.
|
||
@Params
|
||
name - The file name to save the current page's HTML to.
|
||
folder - The folder to save the file to. (Default = current folder)"""
|
||
if not self.__is_cdp_swap_needed():
|
||
self.wait_for_ready_state_complete()
|
||
return page_actions.save_page_source(self.driver, name, folder)
|
||
|
||
def save_cookies(self, name="cookies.txt"):
|
||
"""Saves the page cookies to the "saved_cookies" folder."""
|
||
self.wait_for_ready_state_complete()
|
||
cookies = self.driver.get_cookies()
|
||
json_cookies = json.dumps(cookies)
|
||
if name.endswith("/"):
|
||
raise Exception("Invalid filename for Cookies!")
|
||
if "/" in name:
|
||
name = name.split("/")[-1]
|
||
if "\\" in name:
|
||
name = name.split("\\")[-1]
|
||
if len(name) < 1:
|
||
raise Exception("Filename for Cookies is too short!")
|
||
if not name.endswith(".txt"):
|
||
name = name + ".txt"
|
||
folder = constants.SavedCookies.STORAGE_FOLDER
|
||
abs_path = os.path.abspath(".")
|
||
file_path = os.path.join(abs_path, folder)
|
||
if not os.path.exists(file_path):
|
||
os.makedirs(file_path)
|
||
cookies_file_path = os.path.join(file_path, name)
|
||
cookies_file = codecs.open(cookies_file_path, "w+", encoding="utf-8")
|
||
cookies_file.writelines(json_cookies)
|
||
cookies_file.close()
|
||
|
||
def load_cookies(self, name="cookies.txt", expiry=False):
|
||
"""
|
||
Loads the page cookies from the "saved_cookies" folder.
|
||
Usage for setting expiry:
|
||
If expiry == 0 or False: Delete "expiry".
|
||
If expiry == -1 (or < 0): Do not modify "expiry".
|
||
If expiry > 0: Set "expiry" to expiry minutes in the future.
|
||
If expiry == True: Set "expiry" to 24 hours in the future.
|
||
"""
|
||
cookies = self.get_saved_cookies(name)
|
||
self.wait_for_ready_state_complete()
|
||
origin = self.get_origin()
|
||
trim_origin = origin.split("://")[-1]
|
||
for cookie in cookies:
|
||
if "domain" in cookie:
|
||
if cookie["domain"] not in origin:
|
||
cookie["domain"] = trim_origin
|
||
if "expiry" in cookie and (not expiry or expiry == 0):
|
||
del cookie["expiry"]
|
||
elif isinstance(expiry, (int, float)) and expiry < 0:
|
||
pass
|
||
elif isinstance(expiry, (int, float)) and expiry > 0:
|
||
cookie["expiry"] = int(time.time()) + int(expiry * 60.0)
|
||
elif expiry:
|
||
cookie["expiry"] = int(time.time()) + 86400
|
||
self.driver.add_cookie(cookie)
|
||
|
||
def delete_all_cookies(self):
|
||
"""Deletes all cookies in the web browser.
|
||
Does NOT delete the saved cookies file."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.clear_cookies()
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
self.driver.delete_all_cookies()
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["d_a_c", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def delete_saved_cookies(self, name="cookies.txt"):
|
||
"""Deletes the cookies file from the "saved_cookies" folder.
|
||
Does NOT delete the cookies from the web browser."""
|
||
if name.endswith("/"):
|
||
raise Exception("Invalid filename for Cookies!")
|
||
if "/" in name:
|
||
name = name.split("/")[-1]
|
||
if len(name) < 1:
|
||
raise Exception("Filename for Cookies is too short!")
|
||
if not name.endswith(".txt"):
|
||
name = name + ".txt"
|
||
folder = constants.SavedCookies.STORAGE_FOLDER
|
||
abs_path = os.path.abspath(".")
|
||
file_path = os.path.join(abs_path, folder)
|
||
cookies_file_path = os.path.join(file_path, name)
|
||
if os.path.exists(cookies_file_path):
|
||
if cookies_file_path.endswith(".txt"):
|
||
os.remove(cookies_file_path)
|
||
|
||
def get_saved_cookies(self, name="cookies.txt"):
|
||
"""Gets the page cookies from the "saved_cookies" folder."""
|
||
if name.endswith("/"):
|
||
raise Exception("Invalid filename for Cookies!")
|
||
if "/" in name:
|
||
name = name.split("/")[-1]
|
||
if "\\" in name:
|
||
name = name.split("\\")[-1]
|
||
if len(name) < 1:
|
||
raise Exception("Filename for Cookies is too short!")
|
||
if not name.endswith(".txt"):
|
||
name = name + ".txt"
|
||
folder = constants.SavedCookies.STORAGE_FOLDER
|
||
abs_path = os.path.abspath(".")
|
||
file_path = os.path.join(abs_path, folder)
|
||
cookies_file_path = os.path.join(file_path, name)
|
||
json_cookies = None
|
||
with open(cookies_file_path, "r") as f:
|
||
json_cookies = f.read().strip()
|
||
return json.loads(json_cookies)
|
||
|
||
def get_cookie(self, name):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
return self.driver.get_cookie(name)
|
||
|
||
def get_cookies(self):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
return self.driver.get_cookies()
|
||
|
||
def get_cookie_string(self):
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_cookie_string()
|
||
self._check_browser()
|
||
return self.execute_script("return document.cookie;")
|
||
|
||
def add_cookie(self, cookie_dict, expiry=False):
|
||
"""Usage examples:
|
||
self.add_cookie({'name': 'foo', 'value': 'bar'})
|
||
self.add_cookie({'name': 'foo', 'value': 'bar', 'path': '/'})
|
||
self.add_cookie({'name': 'foo', 'value': 'bar', 'secure': True})
|
||
self.add_cookie({'name': 'foo', 'value': 'bar', 'sameSite': 'Strict'})
|
||
Usage for setting expiry:
|
||
If expiry == 0 or False: Delete "expiry".
|
||
If expiry == -1 (or < 0): Do not modify "expiry".
|
||
If expiry > 0: Set "expiry" to expiry minutes in the future.
|
||
If expiry == True: Set "expiry" to 24 hours in the future.
|
||
"""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
cookie = cookie_dict
|
||
if "domain" in cookie:
|
||
origin = self.get_origin()
|
||
trim_origin = origin.split("://")[-1]
|
||
if cookie["domain"] not in origin:
|
||
cookie["domain"] = trim_origin
|
||
if "expiry" in cookie and (not expiry or expiry == 0):
|
||
del cookie["expiry"]
|
||
elif isinstance(expiry, (int, float)) and expiry < 0:
|
||
pass
|
||
elif isinstance(expiry, (int, float)) and expiry > 0:
|
||
cookie["expiry"] = int(time.time()) + int(expiry * 60.0)
|
||
elif expiry:
|
||
cookie["expiry"] = int(time.time()) + 86400
|
||
self.driver.add_cookie(cookie_dict)
|
||
|
||
def add_cookies(self, cookies, expiry=False):
|
||
"""
|
||
Usage for setting expiry:
|
||
If expiry == 0 or False: Delete "expiry".
|
||
If expiry == -1 (or < 0): Do not modify "expiry".
|
||
If expiry > 0: Set "expiry" to expiry minutes in the future.
|
||
If expiry == True: Set "expiry" to 24 hours in the future.
|
||
"""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
origin = self.get_origin()
|
||
trim_origin = origin.split("://")[-1]
|
||
for cookie in cookies:
|
||
if "domain" in cookie:
|
||
if cookie["domain"] not in origin:
|
||
cookie["domain"] = trim_origin
|
||
if "expiry" in cookie and (not expiry or expiry == 0):
|
||
del cookie["expiry"]
|
||
elif isinstance(expiry, (int, float)) and expiry < 0:
|
||
pass
|
||
elif isinstance(expiry, (int, float)) and expiry > 0:
|
||
cookie["expiry"] = int(time.time()) + int(expiry * 60.0)
|
||
elif expiry:
|
||
cookie["expiry"] = int(time.time()) + 86400
|
||
self.driver.add_cookie(cookie)
|
||
|
||
def __set_esc_skip(self):
|
||
if hasattr(self, "esc_end") and self.esc_end:
|
||
script = (
|
||
"""document.onkeydown = function(evt) {
|
||
evt = evt || window.event;
|
||
var isEscape = false;
|
||
if ("key" in evt) {
|
||
isEscape = (evt.key === "Escape" || evt.key === "Esc");
|
||
} else {
|
||
isEscape = (evt.keyCode === 27);
|
||
}
|
||
if (isEscape) {
|
||
document.sb_esc_end = 'yes';
|
||
}
|
||
};"""
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def __skip_if_esc(self):
|
||
if hasattr(self, "esc_end") and self.esc_end:
|
||
if self.execute_script("return document.sb_esc_end;") == "yes":
|
||
self.skip()
|
||
|
||
def wait_for_ready_state_complete(self, timeout=None):
|
||
"""Waits for the "readyState" of the page to be "complete".
|
||
Returns True when the method completes."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
self.__skip_if_esc()
|
||
if not timeout:
|
||
timeout = settings.EXTREME_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.EXTREME_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
js_utils.wait_for_ready_state_complete(self.driver, timeout)
|
||
self.wait_for_angularjs(timeout=settings.MINI_TIMEOUT)
|
||
if self.js_checking_on:
|
||
self.assert_no_js_errors()
|
||
self.__ad_block_as_needed()
|
||
self.__disable_beforeunload_as_needed()
|
||
if (
|
||
self.page_load_strategy == "none"
|
||
and hasattr(settings, "SKIP_JS_WAITS")
|
||
and settings.SKIP_JS_WAITS
|
||
):
|
||
time.sleep(0.01)
|
||
if self.undetectable:
|
||
time.sleep(0.035)
|
||
self.__set_esc_skip()
|
||
return True
|
||
|
||
def wait_for_angularjs(self, timeout=None, **kwargs):
|
||
"""Waits for Angular components of the page to finish loading.
|
||
Returns True when the method completes."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
js_utils.wait_for_angularjs(self.driver, timeout, **kwargs)
|
||
return True
|
||
|
||
def sleep(self, seconds):
|
||
self.__check_scope()
|
||
if (
|
||
not hasattr(sb_config, "time_limit")
|
||
or (hasattr(sb_config, "time_limit") and not sb_config.time_limit)
|
||
):
|
||
time.sleep(seconds)
|
||
elif seconds < 0.4:
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
time.sleep(seconds)
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
else:
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (seconds * 1000.0)
|
||
for x in range(int(seconds * 5)):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.2)
|
||
if (
|
||
self.recorder_mode
|
||
and hasattr(sb_config, "record_sleep")
|
||
and sb_config.record_sleep
|
||
):
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["sleep", seconds, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def install_addon(self, xpi_file):
|
||
"""Installs a Firefox add-on instantly at run-time.
|
||
@Params
|
||
xpi_file - A file archive in .xpi format."""
|
||
self.wait_for_ready_state_complete()
|
||
if self.browser != "firefox":
|
||
raise Exception(
|
||
"install_addon(xpi_file) is for Firefox ONLY!\n"
|
||
"To load a Chrome extension, use the comamnd-line:\n"
|
||
"--extension_zip=CRX_FILE OR --extension_dir=DIR"
|
||
)
|
||
xpi_path = os.path.abspath(xpi_file)
|
||
self.driver.install_addon(xpi_path, temporary=True)
|
||
|
||
def activate_jquery(self):
|
||
"""If "jQuery is not defined", use this method to activate it for use.
|
||
This happens because jQuery is not always defined on web sites."""
|
||
self.wait_for_ready_state_complete()
|
||
js_utils.activate_jquery(self.driver)
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def activate_demo_mode(self):
|
||
self.demo_mode = True
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["a_d_m", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def deactivate_demo_mode(self):
|
||
self.demo_mode = False
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["d_d_m", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def activate_design_mode(self):
|
||
# Activate Chrome's Design Mode, which lets you edit a site directly.
|
||
# See: https://twitter.com/sulco/status/1177559150563344384
|
||
self.wait_for_ready_state_complete()
|
||
script = """document.designMode = 'on';"""
|
||
self.execute_script(script)
|
||
|
||
def deactivate_design_mode(self, url=None):
|
||
# Deactivate Chrome's Design Mode.
|
||
self.wait_for_ready_state_complete()
|
||
script = """document.designMode = 'off';"""
|
||
self.execute_script(script)
|
||
|
||
def activate_cdp_mode(self, url=None):
|
||
if hasattr(self.driver, "_is_using_uc") and self.driver._is_using_uc:
|
||
if self.__is_cdp_swap_needed():
|
||
return # CDP Mode is already active
|
||
if not self.is_connected():
|
||
self.driver.connect()
|
||
current_url = self.get_current_url()
|
||
if not current_url.startswith(("about", "data", "chrome")):
|
||
self.get_new_driver(undetectable=True)
|
||
self.driver.uc_open_with_cdp_mode(url)
|
||
else:
|
||
self.get_new_driver(undetectable=True)
|
||
self.driver.uc_open_with_cdp_mode(url)
|
||
self.cdp = self.driver.cdp
|
||
|
||
def activate_recorder(self):
|
||
from seleniumbase.js_code.recorder_js import recorder_js
|
||
|
||
if not self.is_chromium():
|
||
if not is_linux:
|
||
c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX
|
||
c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX
|
||
cr = colorama.Style.RESET_ALL
|
||
sc = c1 + "Selenium" + c2 + "Base" + cr
|
||
raise Exception(
|
||
"The %s Recorder is for Chromium only!\n"
|
||
" (Supported browsers: Chrome and Edge)" % sc
|
||
)
|
||
url = self.driver.current_url
|
||
if url.startswith(("data:", "about:", "chrome:", "edge:")):
|
||
message = (
|
||
"The URL in Recorder-Mode cannot start with: "
|
||
'"data:", "about:", "chrome:", or "edge:"!'
|
||
)
|
||
print("\n" + message)
|
||
return
|
||
if self.recorder_ext:
|
||
return # The Recorder extension is already active
|
||
with suppress(Exception):
|
||
recorder_on = self.get_session_storage_item("recorder_activated")
|
||
if not recorder_on == "yes":
|
||
self.execute_script(recorder_js)
|
||
self.recorder_mode = True
|
||
message = "Recorder Mode ACTIVE. [ESC]: Pause. [~`]: Resume."
|
||
print("\n" + message)
|
||
p_msg = "Recorder Mode ACTIVE.<br>[ESC]: Pause. [~`]: Resume."
|
||
self.post_message(p_msg, pause=False, style="error")
|
||
|
||
def __current_url_is_recordable(self):
|
||
url = self.get_current_url()
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
return True
|
||
return False
|
||
|
||
def save_recorded_actions(self):
|
||
"""(When using Recorder Mode, use this method if you plan on
|
||
navigating to a different domain/origin in the same tab.)
|
||
This method saves recorded actions from the active tab so that
|
||
a complete recording can be exported as a SeleniumBase file at the
|
||
end of the test. This is only needed in special cases because most
|
||
actions that result in a new origin, (such as clicking on a link),
|
||
should automatically open a new tab while Recorder Mode is enabled."""
|
||
if self.driver is None:
|
||
return
|
||
url = self.get_current_url()
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
origin = self.get_origin()
|
||
self.__origins_to_save.append(origin)
|
||
tab_actions = self.__get_recorded_actions_on_active_tab()
|
||
for n in range(len(tab_actions)):
|
||
if (
|
||
n > 2
|
||
and tab_actions[n - 2][0] == "sw_fr"
|
||
and tab_actions[n - 1][0] == "sk_fo"
|
||
and tab_actions[n][0] != "_url_"
|
||
):
|
||
origin = tab_actions[n - 2][2]
|
||
time_stamp = str(int(tab_actions[n][3]) - 1)
|
||
new_action = ["sw_pf", "", origin, time_stamp]
|
||
tab_actions.append(new_action)
|
||
self.__actions_to_save.append(tab_actions)
|
||
|
||
def __get_recorded_actions_on_active_tab(self):
|
||
url = self.driver.current_url
|
||
if url.startswith(("data:", "about:", "chrome:", "edge:")):
|
||
return []
|
||
self.__origins_to_save.append(self.get_origin())
|
||
actions = self.get_session_storage_item("recorded_actions")
|
||
if actions:
|
||
actions = json.loads(actions)
|
||
return actions
|
||
else:
|
||
return []
|
||
|
||
def __process_recorded_actions(self):
|
||
"""Generates code after the SeleniumBase Recorder runs."""
|
||
if self.driver is None:
|
||
return
|
||
from seleniumbase.core import recorder_helper
|
||
|
||
raw_actions = [] # All raw actions from sessionStorage
|
||
srt_actions = []
|
||
sb_actions = []
|
||
action_dict = {}
|
||
for window in self.driver.window_handles:
|
||
self.switch_to_window(window)
|
||
tab_actions = self.__get_recorded_actions_on_active_tab()
|
||
for n in range(len(tab_actions)):
|
||
if (
|
||
n > 2
|
||
and tab_actions[n - 2][0] == "sw_fr"
|
||
and tab_actions[n - 1][0] == "sk_fo"
|
||
and tab_actions[n][0] != "_url_"
|
||
):
|
||
origin = tab_actions[n - 2][2]
|
||
time_stamp = str(int(tab_actions[n][3]) - 1)
|
||
new_action = ["sw_pf", "", origin, time_stamp]
|
||
tab_actions.append(new_action)
|
||
for action in tab_actions:
|
||
if action not in raw_actions:
|
||
raw_actions.append(action)
|
||
for tab_actions in self.__actions_to_save:
|
||
for action in tab_actions:
|
||
if action not in raw_actions:
|
||
raw_actions.append(action)
|
||
for action in self.__extra_actions:
|
||
if action not in raw_actions:
|
||
raw_actions.append(action)
|
||
for action in raw_actions:
|
||
if int(action[3]) < int(self.__js_start_time):
|
||
continue
|
||
# Use key for sorting and preventing duplicates
|
||
key = str(action[3]) + "-" + str(action[0])
|
||
action_dict[key] = action
|
||
for key in sorted(action_dict):
|
||
# print(action_dict[key]) # For debugging purposes
|
||
srt_actions.append(action_dict[key])
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] == "sk_fo":
|
||
srt_actions[n][0] = "sk_op"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "sk_op"
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "c_box"
|
||
and n > 0
|
||
and (
|
||
(
|
||
srt_actions[n - 1][0] == "click"
|
||
and int(srt_actions[n][3]) - int(srt_actions[n - 1][3])
|
||
< 16
|
||
)
|
||
or (
|
||
srt_actions[n - 1][0] == "js_cl"
|
||
and int(srt_actions[n][3]) - int(srt_actions[n - 1][3])
|
||
< 42
|
||
)
|
||
or (
|
||
srt_actions[n - 1][0] == "jq_cl"
|
||
and int(srt_actions[n][3]) - int(srt_actions[n - 1][3])
|
||
< 440
|
||
)
|
||
)
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
if srt_actions[n - 1][0] == "click":
|
||
srt_actions[n - 1][0] = "ch_cl"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
n > 0
|
||
and srt_actions[n - 1][0] == "c_box"
|
||
and (
|
||
srt_actions[n][0] == "click"
|
||
or srt_actions[n][0] == "js_cl"
|
||
or srt_actions[n][0] == "jq_cl"
|
||
)
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 16
|
||
)
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 1
|
||
and srt_actions[n - 1][0] == "_skip"
|
||
and srt_actions[n - 2][0] == "sk_op"
|
||
and srt_actions[n][2] == srt_actions[n - 1][2]
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "click"
|
||
or srt_actions[n - 1][0] == "js_cl"
|
||
or srt_actions[n - 1][0] == "js_ca"
|
||
or srt_actions[n - 1][0] == "jq_cl"
|
||
or srt_actions[n - 1][0] == "jq_ca"
|
||
)
|
||
):
|
||
sel1 = srt_actions[n - 1][1]
|
||
url1 = srt_actions[n - 1][2]
|
||
if (
|
||
srt_actions[n - 1][0] == "js_cl"
|
||
or srt_actions[n - 1][0] == "js_ca"
|
||
or srt_actions[n - 1][0] == "jq_cl"
|
||
or srt_actions[n - 1][0] == "jq_ca"
|
||
):
|
||
url1 = srt_actions[n - 1][2][0]
|
||
if url1.endswith("/#/"):
|
||
url1 = url1[:-3]
|
||
elif url1.endswith("/"):
|
||
url1 = url1[:-1]
|
||
url2 = srt_actions[n][2]
|
||
if url2.endswith("/#/"):
|
||
url2 = url1[:-3]
|
||
elif url2.endswith("/"):
|
||
url2 = url2[:-1]
|
||
if (
|
||
url1 == url2
|
||
or url1 == url2.replace("www.", "")
|
||
or url1 == url2.replace("https://", "http://")
|
||
or sel1.split(" ")[-1].startswith("a[href=")
|
||
or (len(url1) > 0
|
||
and (url2.startswith(url1) or "?search" in url1)
|
||
and (int(srt_actions[n][3]) - int(
|
||
srt_actions[n - 1][3]) < 6500))
|
||
):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "begin"
|
||
or srt_actions[n - 1][0] == "_url_"
|
||
)
|
||
):
|
||
url1 = srt_actions[n - 1][2]
|
||
if url1.endswith("/#/"):
|
||
url1 = url1[:-3]
|
||
elif url1.endswith("/"):
|
||
url1 = url1[:-1]
|
||
url2 = srt_actions[n][2]
|
||
if url2.endswith("/#/"):
|
||
url2 = url1[:-3]
|
||
elif url2.endswith("/"):
|
||
url2 = url2[:-1]
|
||
if url1.replace("www.", "") == url2.replace("www.", ""):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
elif url1.replace("http://", "https://") == url2:
|
||
srt_actions[n - 1][0] = "_skip"
|
||
elif url2.startswith(url1):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "input"
|
||
and srt_actions[n - 1][2] == ""
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
elif (
|
||
srt_actions[n][0] == "input"
|
||
and n > 1
|
||
and srt_actions[n - 2][0] == "input"
|
||
and srt_actions[n - 1][0] == "submi"
|
||
and srt_actions[n - 2][1].startswith("textarea")
|
||
and srt_actions[n - 2][1] == srt_actions[n][1]
|
||
):
|
||
srt_actions[n - 2][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "click"
|
||
or srt_actions[n - 1][0] == "js_cl"
|
||
or srt_actions[n - 1][0] == "js_ca"
|
||
or srt_actions[n - 1][0] == "jq_cl"
|
||
or srt_actions[n - 1][0] == "jq_ca"
|
||
or srt_actions[n - 1][0] == "input"
|
||
)
|
||
and int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 6500
|
||
):
|
||
if (
|
||
srt_actions[n - 1][0] == "click"
|
||
or srt_actions[n - 1][0] == "js_cl"
|
||
or srt_actions[n - 1][0] == "js_ca"
|
||
or srt_actions[n - 1][0] == "jq_cl"
|
||
or srt_actions[n - 1][0] == "jq_ca"
|
||
):
|
||
if srt_actions[n - 1][1].startswith(("input", "button")):
|
||
srt_actions[n][0] = "f_url"
|
||
elif srt_actions[n - 1][0] == "input":
|
||
if srt_actions[n - 1][2].endswith("\n"):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "dbclk"
|
||
and n > 1
|
||
and srt_actions[n - 2][0] == "click"
|
||
and srt_actions[n - 1][0] == "click"
|
||
and srt_actions[n - 2][1] == srt_actions[n - 1][1]
|
||
):
|
||
srt_actions[n - 2][0] = "_skip"
|
||
srt_actions[n - 1][0] = "_skip"
|
||
srt_actions[n][1] = srt_actions[n - 1][1]
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "dbclk"
|
||
and srt_actions[n][1] == ""
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "cho_f"
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "chfil"
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
srt_actions[n][2] = srt_actions[n - 1][1][1]
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "e_mfa"
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "submi"
|
||
or srt_actions[n - 1][0] == "e_mfa"
|
||
)
|
||
):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 1
|
||
and srt_actions[n - 1][0] == "_url_"
|
||
and srt_actions[n - 2][0] == "o_url"
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 4800
|
||
)
|
||
):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "click"
|
||
or srt_actions[n - 1][0] == "js_cl"
|
||
or srt_actions[n - 1][0] == "js_ca"
|
||
or srt_actions[n - 1][0] == "jq_cl"
|
||
or srt_actions[n - 1][0] == "jq_ca"
|
||
)
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 5200
|
||
)
|
||
):
|
||
srt_actions[n][0] = "f_url"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
n > 1
|
||
and srt_actions[n][0] == "f_url"
|
||
and srt_actions[n - 1][0] == "_url_"
|
||
):
|
||
url0 = srt_actions[n][2]
|
||
url1 = srt_actions[n - 1][2]
|
||
if url0.endswith("/"):
|
||
url0 = url0[0:-1]
|
||
if url1.endswith("/"):
|
||
url1 = url1[0:-1]
|
||
url0 = url0.replace("http://", "https://")
|
||
url0 = url0.replace("://www.", "://")
|
||
url1 = url1.replace("http://", "https://")
|
||
url1 = url1.replace("://www.", "://")
|
||
if url0 == url1:
|
||
srt_actions[n][0] = "_url_"
|
||
srt_actions[n - 1][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "f_url"
|
||
and "?q=" in srt_actions[n][2]
|
||
and "&" in srt_actions[n][2]
|
||
and srt_actions[n][2].find("?q=") < srt_actions[n][2].find("&")
|
||
):
|
||
srt_actions[n][2] = srt_actions[n][2].split("&")[0]
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 2
|
||
and srt_actions[n - 1][0] == "_skip"
|
||
and srt_actions[n - 2][0] == "o_url"
|
||
and (
|
||
srt_actions[n - 3][0] == "sw_fr"
|
||
or srt_actions[n - 3][0] == "sw_dc"
|
||
or srt_actions[n - 3][0] == "sw_pf"
|
||
or srt_actions[n - 3][0] == "s_c_d"
|
||
or srt_actions[n - 3][0] == "s_c_f"
|
||
)
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 3][3]) < 4200
|
||
)
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
(srt_actions[n][0] == "begin" or srt_actions[n][0] == "_url_")
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "sw_fr"
|
||
or srt_actions[n - 1][0] == "sw_dc"
|
||
or srt_actions[n - 1][0] == "sw_pf"
|
||
or srt_actions[n - 1][0] == "s_c_d"
|
||
or srt_actions[n - 1][0] == "s_c_f"
|
||
)
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 4200
|
||
)
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 2
|
||
and srt_actions[n - 1][0] == "js_cl"
|
||
and srt_actions[n - 2][0] == "input"
|
||
and srt_actions[n][2].endswith("\n")
|
||
and srt_actions[n][1] == srt_actions[n - 2][1]
|
||
and srt_actions[n][2][0:-1] == srt_actions[n - 2][2]
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 440
|
||
)
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
srt_actions[n - 2][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 1
|
||
and srt_actions[n - 1][0] == "jq_cl"
|
||
and srt_actions[n][2].endswith("\n")
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 1][3]) < 900
|
||
)
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 2
|
||
and srt_actions[n - 3][0] == "js_ty"
|
||
and srt_actions[n - 1][0] == "_skip"
|
||
and srt_actions[n - 2][0] == "_skip"
|
||
and srt_actions[n][2].endswith("\n")
|
||
and srt_actions[n][2][0:-1] == srt_actions[n - 3][1][1]
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 3][3]) < 440
|
||
)
|
||
):
|
||
srt_actions[n - 3][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 1
|
||
and srt_actions[n - 2][0] == "jq_ty"
|
||
and srt_actions[n - 1][0] == "_skip"
|
||
and srt_actions[n][2].endswith("\n")
|
||
and srt_actions[n][2][0:-1] == srt_actions[n - 2][1][1]
|
||
and (
|
||
int(srt_actions[n][3]) - int(srt_actions[n - 2][3]) < 900
|
||
)
|
||
):
|
||
srt_actions[n - 2][0] = "_skip"
|
||
origins = []
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "begin"
|
||
or srt_actions[n][0] == "_url_"
|
||
or srt_actions[n][0] == "f_url"
|
||
):
|
||
origin = srt_actions[n][1]
|
||
if origin.endswith("/"):
|
||
origin = origin[0:-1]
|
||
if origin not in origins:
|
||
origins.append(origin)
|
||
for origin in self.__origins_to_save:
|
||
origins.append(origin)
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "click"
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "ho_cl"
|
||
and srt_actions[n - 1][2] in origins
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
srt_actions[n][0] = "h_clk"
|
||
srt_actions[n][1] = srt_actions[n - 1][1][0]
|
||
srt_actions[n][2] = srt_actions[n - 1][1][1]
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] == "chfil" and srt_actions[n][2] in origins:
|
||
srt_actions[n][0] = "cho_f"
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "sh_fc"
|
||
and n > 0
|
||
and srt_actions[n - 1][0] == "sh_fc"
|
||
):
|
||
srt_actions[n - 1][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] == "canva":
|
||
srt_actions[n][1][1] = math.ceil(float(srt_actions[n][1][1]))
|
||
srt_actions[n][1][2] = math.ceil(float(srt_actions[n][1][2]))
|
||
ext_actions = []
|
||
ext_actions.append("_url_")
|
||
ext_actions.append("js_cl")
|
||
ext_actions.append("js_ca")
|
||
ext_actions.append("js_ty")
|
||
ext_actions.append("s_val")
|
||
ext_actions.append("jq_cl")
|
||
ext_actions.append("jq_ca")
|
||
ext_actions.append("jq_ty")
|
||
ext_actions.append("pkeys")
|
||
ext_actions.append("r_clk")
|
||
ext_actions.append("as_el")
|
||
ext_actions.append("as_ep")
|
||
ext_actions.append("asenv")
|
||
ext_actions.append("acc_a")
|
||
ext_actions.append("dis_a")
|
||
ext_actions.append("hi_li")
|
||
ext_actions.append("as_lt")
|
||
ext_actions.append("as_ti")
|
||
ext_actions.append("as_tc")
|
||
ext_actions.append("a_url")
|
||
ext_actions.append("a_u_c")
|
||
ext_actions.append("as_df")
|
||
ext_actions.append("do_fi")
|
||
ext_actions.append("as_at")
|
||
ext_actions.append("as_te")
|
||
ext_actions.append("astnv")
|
||
ext_actions.append("aetnv")
|
||
ext_actions.append("as_et")
|
||
ext_actions.append("asnet")
|
||
ext_actions.append("wf_el")
|
||
ext_actions.append("sw_fr")
|
||
ext_actions.append("sw_dc")
|
||
ext_actions.append("sw_pf")
|
||
ext_actions.append("s_c_f")
|
||
ext_actions.append("s_c_d")
|
||
ext_actions.append("hover")
|
||
ext_actions.append("sleep")
|
||
ext_actions.append("sh_fc")
|
||
ext_actions.append("s_at_")
|
||
ext_actions.append("s_ats")
|
||
ext_actions.append("a_d_m")
|
||
ext_actions.append("d_d_m")
|
||
ext_actions.append("c_l_s")
|
||
ext_actions.append("c_s_s")
|
||
ext_actions.append("d_a_c")
|
||
ext_actions.append("e_mfa")
|
||
ext_actions.append("go_bk")
|
||
ext_actions.append("go_fw")
|
||
ext_actions.append("s_scr")
|
||
ext_actions.append("ss_tf")
|
||
ext_actions.append("ss_tl")
|
||
ext_actions.append("da_el")
|
||
ext_actions.append("da_ep")
|
||
ext_actions.append("da_te")
|
||
ext_actions.append("da_et")
|
||
ext_actions.append("danet")
|
||
ext_actions.append("pr_da")
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] in ext_actions:
|
||
origin = srt_actions[n][2]
|
||
if (
|
||
srt_actions[n][0] == "js_cl"
|
||
or srt_actions[n][0] == "js_ca"
|
||
or srt_actions[n][0] == "jq_cl"
|
||
or srt_actions[n][0] == "jq_ca"
|
||
):
|
||
origin = srt_actions[n][2][1]
|
||
if origin.endswith("/"):
|
||
origin = origin[0:-1]
|
||
if srt_actions[n][0] == "js_ty":
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
if srt_actions[n][0] == "jq_ty":
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
if srt_actions[n][0] == "pkeys":
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
if srt_actions[n][0] == "e_mfa":
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
if srt_actions[n][0] == "_url_" and origin not in origins:
|
||
origins.append(origin)
|
||
if origin not in origins:
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if (
|
||
srt_actions[n][0] == "input"
|
||
and n > 0
|
||
and (
|
||
srt_actions[n - 1][0] == "js_ty"
|
||
or srt_actions[n - 1][0] == "jq_ty"
|
||
or srt_actions[n - 1][0] == "pkeys"
|
||
)
|
||
and srt_actions[n][2] == srt_actions[n - 1][2]
|
||
):
|
||
srt_actions[n][0] = "_skip"
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] == "ch_cl":
|
||
srt_actions[n][0] = "js_cl"
|
||
for n in range(len(srt_actions)):
|
||
if srt_actions[n][0] == "s_val":
|
||
srt_actions[n][0] = "set_v"
|
||
srt_actions[n][2] = srt_actions[n][1][1]
|
||
srt_actions[n][1] = srt_actions[n][1][0]
|
||
|
||
# Generate the script from processed actions
|
||
sb_actions = recorder_helper.generate_sbase_code(srt_actions)
|
||
filename = self.__get_filename()
|
||
classname = self.__class__.__name__
|
||
methodname = self._testMethodName
|
||
context_filename = None
|
||
if (
|
||
hasattr(sb_config, "is_context_manager")
|
||
and sb_config.is_context_manager
|
||
and (filename == "base_case.py" or methodname == "runTest")
|
||
):
|
||
import traceback
|
||
stack_base = traceback.format_stack()[0].split(os.sep)[-1]
|
||
test_base = stack_base.split(", in ")[0]
|
||
if hasattr(self, "cm_filename") and self.cm_filename:
|
||
filename = self.cm_filename
|
||
else:
|
||
filename = test_base.split('"')[0]
|
||
classname = "SB_Test"
|
||
methodname = "test_line_" + test_base.split(", line ")[-1]
|
||
context_filename = filename.split(".")[0] + "_rec.py"
|
||
if hasattr(self, "_using_sb_fixture"):
|
||
test_id = sb_config._test_id
|
||
filename = test_id.split("::")[0]
|
||
methodname = test_id.split("::")[-1]
|
||
if test_id.count("::") >= 2:
|
||
classname = test_id.split("::")[-2]
|
||
else:
|
||
classname = "MyTestClass"
|
||
methodname = methodname.replace("[", "__").replace("]", "")
|
||
methodname = re.sub(r"[\W]", "_", methodname)
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
classname = sb_config.behave_feature.name
|
||
classname = classname.replace("/", " ").replace(" & ", " ")
|
||
classname = re.sub(r"[^\w" + r"_ " + r"]", "", classname)
|
||
classname_parts = classname.split(" ")
|
||
new_classname = ""
|
||
for part in classname_parts:
|
||
new_classname += (part[0].upper() + part[1:])
|
||
classname = new_classname
|
||
methodname = sb_config.behave_scenario.name
|
||
methodname = methodname.replace("/", "_").replace(" & ", "_")
|
||
methodname = methodname.replace(" + ", " plus ")
|
||
methodname = methodname.replace(" - ", " minus ")
|
||
methodname = methodname.replace(" × ", " times ")
|
||
methodname = methodname.replace(" ÷ ", " divided by ")
|
||
methodname = methodname.replace(" = ", " equals ")
|
||
methodname = methodname.replace(" %% ", " percent ")
|
||
methodname = re.sub(r"[^\w" + r"_ " + r"]", "", methodname)
|
||
methodname = methodname.replace(" _ ", "_").lower()
|
||
methodname = methodname.replace(" ", "_").lower()
|
||
if not methodname.startswith("test_"):
|
||
methodname = "test_" + methodname
|
||
new_file = False
|
||
data = []
|
||
if filename not in sb_config._recorded_actions:
|
||
new_file = True
|
||
sb_config._recorded_actions[filename] = []
|
||
data.append("from seleniumbase import BaseCase")
|
||
if "--uc" in sys.argv:
|
||
data.append('BaseCase.main(__name__, __file__, "--uc")')
|
||
else:
|
||
data.append("BaseCase.main(__name__, __file__)")
|
||
data.append("")
|
||
data.append("")
|
||
data.append("class %s(BaseCase):" % classname)
|
||
else:
|
||
data = sb_config._recorded_actions[filename]
|
||
if not new_file and classname not in " ".join(data):
|
||
data.append("class %s(BaseCase):" % classname)
|
||
data.append(" def %s(self):" % methodname)
|
||
if len(sb_actions) > 0:
|
||
if "--uc" in sys.argv:
|
||
data.append(" self.activate_cdp_mode()")
|
||
for action in sb_actions:
|
||
if "--uc" in sys.argv:
|
||
action = action.replace(
|
||
"self.type(", "self.press_keys("
|
||
)
|
||
data.append(" " + action)
|
||
else:
|
||
data.append(" pass")
|
||
data.append("")
|
||
sb_config._recorded_actions[filename] = data
|
||
saved_data = data
|
||
|
||
recordings_folder = constants.Recordings.SAVED_FOLDER
|
||
if recordings_folder.endswith("/"):
|
||
recordings_folder = recordings_folder[:-1]
|
||
if not os.path.exists(recordings_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(recordings_folder)
|
||
sys.stdout.write("\nCreated recordings%s" % os.sep)
|
||
|
||
data = []
|
||
data.append("")
|
||
extra_file_name = "__init__.py"
|
||
extra_file_path = os.path.join(recordings_folder, extra_file_name)
|
||
if not os.path.exists(extra_file_path):
|
||
out_file = codecs.open(extra_file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
sys.stdout.write("\nCreated recordings%s__init__.py" % os.sep)
|
||
|
||
data = []
|
||
data.append("[pytest]")
|
||
data.append("addopts = --capture=no -p no:cacheprovider")
|
||
data.append("norecursedirs = .* build dist recordings temp assets")
|
||
data.append("filterwarnings =")
|
||
data.append(" ignore::pytest.PytestWarning")
|
||
data.append(" ignore:.*U.*mode is deprecated:DeprecationWarning")
|
||
data.append("junit_family = legacy")
|
||
data.append("python_files =")
|
||
data.append(" test_*.py ")
|
||
data.append(" *_test.py ")
|
||
data.append(" *_tests.py")
|
||
data.append(" *_suite.py")
|
||
data.append(" *_feature.py")
|
||
data.append(" *_test_rec.py")
|
||
data.append(" *_feature_rec.py")
|
||
data.append("python_classes = Test* *Test* *Test *Tests *Suite")
|
||
data.append("python_functions = test_*")
|
||
data.append("markers =")
|
||
data.append(" marker1: custom marker")
|
||
data.append(" marker2: custom marker")
|
||
data.append(" marker3: custom marker")
|
||
data.append(" marker_test_suite: custom marker")
|
||
data.append(" expected_failure: custom marker")
|
||
data.append(" local: custom marker")
|
||
data.append(" remote: custom marker")
|
||
data.append(" offline: custom marker")
|
||
data.append(" develop: custom marker")
|
||
data.append(" qa: custom marker")
|
||
data.append(" ci: custom marker")
|
||
data.append(" e2e: custom marker")
|
||
data.append(" ready: custom marker")
|
||
data.append(" smoke: custom marker")
|
||
data.append(" deploy: custom marker")
|
||
data.append(" active: custom marker")
|
||
data.append(" master: custom marker")
|
||
data.append(" release: custom marker")
|
||
data.append(" staging: custom marker")
|
||
data.append(" production: custom marker")
|
||
data.append("")
|
||
extra_file_name = "pytest.ini"
|
||
extra_file_path = os.path.join(recordings_folder, extra_file_name)
|
||
if not os.path.exists(extra_file_path):
|
||
out_file = codecs.open(extra_file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
sys.stdout.write("\nCreated recordings%spytest.ini" % os.sep)
|
||
|
||
data = []
|
||
data.append("[flake8]")
|
||
data.append("exclude=recordings,temp")
|
||
data.append("ignore=W503")
|
||
data.append("")
|
||
data.append("[nosetests]")
|
||
data.append("nocapture=1")
|
||
data.append("logging-level=INFO")
|
||
data.append("")
|
||
data.append("[behave]")
|
||
data.append("show_skipped=false")
|
||
data.append("show_timings=false")
|
||
data.append("")
|
||
extra_file_name = "setup.cfg"
|
||
extra_file_path = os.path.join(recordings_folder, extra_file_name)
|
||
if not os.path.exists(extra_file_path):
|
||
out_file = codecs.open(extra_file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
sys.stdout.write("\nCreated recordings%ssetup.cfg" % os.sep)
|
||
|
||
data = saved_data
|
||
file_name = self.__class__.__module__.split(".")[-1] + "_rec.py"
|
||
if hasattr(self, "_using_sb_fixture"):
|
||
test_id = sb_config._test_id
|
||
file_name = test_id.split("::")[0].split("/")[-1].split("\\")[-1]
|
||
file_name = file_name.split(".py")[0] + "_rec.py"
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
file_name = sb_config.behave_scenario.filename.replace(".", "_")
|
||
file_name = file_name.split("/")[-1].split("\\")[-1] + "_rec.py"
|
||
file_name = file_name
|
||
elif context_filename:
|
||
file_name = context_filename
|
||
file_path = os.path.join(recordings_folder, file_name)
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
rec_message = ">>> RECORDING SAVED as: "
|
||
if not new_file:
|
||
rec_message = ">>> RECORDING ADDED to: "
|
||
star_len = len(rec_message) + len(file_path)
|
||
with suppress(Exception):
|
||
terminal_size = os.get_terminal_size().columns
|
||
if terminal_size > 30 and star_len > terminal_size:
|
||
star_len = terminal_size
|
||
spc = "\n\n"
|
||
if hasattr(self, "rec_print") and self.rec_print:
|
||
spc = ""
|
||
sys.stdout.write("\nCreated recordings%s%s" % (os.sep, file_name))
|
||
print()
|
||
if " " not in file_path:
|
||
os.system("sbase print %s -n" % file_path)
|
||
elif '"' not in file_path:
|
||
os.system('sbase print "%s" -n' % file_path)
|
||
else:
|
||
os.system("sbase print '%s' -n" % file_path)
|
||
stars = "*" * star_len
|
||
c1 = ""
|
||
c2 = ""
|
||
cr = ""
|
||
if not is_linux:
|
||
c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX
|
||
c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX
|
||
cr = colorama.Style.RESET_ALL
|
||
rec_message = rec_message.replace(">>>", c2 + ">>>" + cr)
|
||
print("%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars))
|
||
|
||
if hasattr(self, "rec_behave") and self.rec_behave:
|
||
# Also generate necessary behave-gherkin files.
|
||
self.__process_recorded_behave_actions(srt_actions, colorama)
|
||
|
||
def __process_recorded_behave_actions(self, srt_actions, colorama):
|
||
from seleniumbase.behave import behave_helper
|
||
|
||
behave_actions = behave_helper.generate_gherkin(srt_actions)
|
||
filename = self.__get_filename()
|
||
feature_class = None
|
||
scenario_test = None
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
feature_class = sb_config.behave_feature.name
|
||
scenario_test = sb_config.behave_scenario.name
|
||
else:
|
||
feature_class = self.__class__.__name__
|
||
scenario_test = self._testMethodName
|
||
if hasattr(self, "_using_sb_fixture"):
|
||
test_id = sb_config._test_id
|
||
filename = test_id.split("::")[0]
|
||
scenario_test = test_id.split("::")[-1]
|
||
if test_id.count("::") >= 2:
|
||
feature_class = test_id.split("::")[-2]
|
||
else:
|
||
feature_class = "MyTestClass"
|
||
new_file = False
|
||
data = []
|
||
if filename not in sb_config._behave_recorded_actions:
|
||
new_file = True
|
||
sb_config._behave_recorded_actions[filename] = []
|
||
data.append("Feature: %s" % feature_class)
|
||
data.append("")
|
||
else:
|
||
data = sb_config._behave_recorded_actions[filename]
|
||
data.append(" Scenario: %s" % scenario_test)
|
||
if len(behave_actions) > 0:
|
||
count = 0
|
||
if "--uc" in sys.argv:
|
||
data.append(" Given Activate CDP Mode")
|
||
count += 1
|
||
for action in behave_actions:
|
||
if count == 0:
|
||
data.append(" Given " + action)
|
||
else:
|
||
data.append(" And " + action)
|
||
count += 1
|
||
data.append("")
|
||
sb_config._behave_recorded_actions[filename] = data
|
||
|
||
recordings_folder = constants.Recordings.SAVED_FOLDER
|
||
if recordings_folder.endswith("/"):
|
||
recordings_folder = recordings_folder[:-1]
|
||
if not os.path.exists(recordings_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(recordings_folder)
|
||
features_folder = os.path.join(recordings_folder, "features")
|
||
if not os.path.exists(features_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(features_folder)
|
||
steps_folder = os.path.join(features_folder, "steps")
|
||
if not os.path.exists(steps_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(steps_folder)
|
||
|
||
file_name = filename.split(".")[0]
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
file_name = sb_config.behave_scenario.filename.replace(".", "_")
|
||
file_name = file_name.split("/")[-1].split("\\")[-1] + "_rec.feature"
|
||
file_path = os.path.join(features_folder, file_name)
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
|
||
rec_message = ">>> RECORDING SAVED as: "
|
||
if not new_file:
|
||
rec_message = ">>> RECORDING ADDED to: "
|
||
star_len = len(rec_message) + len(file_path)
|
||
with suppress(Exception):
|
||
terminal_size = os.get_terminal_size().columns
|
||
if terminal_size > 30 and star_len > terminal_size:
|
||
star_len = terminal_size
|
||
spc = "\n"
|
||
if hasattr(self, "rec_print") and self.rec_print:
|
||
spc = ""
|
||
print()
|
||
if " " not in file_path:
|
||
os.system("sbase print %s -n" % file_path)
|
||
elif '"' not in file_path:
|
||
os.system('sbase print "%s" -n' % file_path)
|
||
else:
|
||
os.system("sbase print '%s' -n" % file_path)
|
||
stars = "*" * star_len
|
||
c1 = ""
|
||
c2 = ""
|
||
cr = ""
|
||
if not is_linux:
|
||
c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX
|
||
c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX
|
||
cr = colorama.Style.RESET_ALL
|
||
rec_message = rec_message.replace(">>>", c2 + ">>>" + cr)
|
||
print("%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars))
|
||
|
||
data = []
|
||
data.append("")
|
||
file_name = "__init__.py"
|
||
file_path = os.path.join(features_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
print("Created recordings/features/__init__.py")
|
||
|
||
data = []
|
||
data.append("[behave]")
|
||
data.append("show_skipped=false")
|
||
data.append("show_timings=false")
|
||
data.append("")
|
||
file_name = "behave.ini"
|
||
file_path = os.path.join(features_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
print("Created recordings/features/behave.ini")
|
||
|
||
data = []
|
||
data.append("from seleniumbase import BaseCase")
|
||
data.append("from seleniumbase.behave import behave_sb")
|
||
data.append(
|
||
"behave_sb.set_base_class(BaseCase) # Accepts a BaseCase subclass"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import before_all # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import before_feature # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import before_scenario # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import before_step # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import after_step # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import after_scenario # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import after_feature # noqa"
|
||
)
|
||
data.append(
|
||
"from seleniumbase.behave.behave_sb import after_all # noqa"
|
||
)
|
||
data.append("")
|
||
file_name = "environment.py"
|
||
file_path = os.path.join(features_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
print("Created recordings/features/environment.py")
|
||
|
||
data = []
|
||
data.append("")
|
||
file_name = "__init__.py"
|
||
file_path = os.path.join(steps_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
print("Created recordings/features/steps/__init__.py")
|
||
|
||
data = []
|
||
data.append("from seleniumbase.behave import steps # noqa")
|
||
data.append("")
|
||
file_name = "imported.py"
|
||
file_path = os.path.join(steps_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
out_file = codecs.open(file_path, "w+", "utf-8")
|
||
out_file.writelines("\r\n".join(data))
|
||
out_file.close()
|
||
print("Created recordings/features/steps/imported.py")
|
||
|
||
def bring_active_window_to_front(self):
|
||
"""Brings the active browser window to the front (on top).
|
||
Useful when multiple drivers are being used at the same time."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.bring_active_window_to_front()
|
||
return
|
||
with suppress(Exception):
|
||
if not self.__is_in_frame():
|
||
# Only bring the window to the front if not in a frame
|
||
# because the driver resets itself to default content.
|
||
self.switch_to_window(self.driver.current_window_handle)
|
||
|
||
def bring_to_front(self, selector, by="css selector"):
|
||
"""Updates the Z-index of a page element to bring it into view.
|
||
Useful when getting a WebDriverException, such as the one below:
|
||
{ Element is not clickable at point (#, #).
|
||
Other element would receive the click: ... }"""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
try:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
except Exception:
|
||
# If can't convert to CSS_Selector for JS, use element directly
|
||
script = ("""arguments[0].style.zIndex = '999999';""")
|
||
self.execute_script(script, element)
|
||
return
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
script = (
|
||
"""document.querySelector('%s').style.zIndex = '999999';"""
|
||
% selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def highlight_click(
|
||
self, selector, by="css selector", loops=3, scroll=True, timeout=None,
|
||
):
|
||
"""Highlights the element, and then clicks it."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if not self.demo_mode:
|
||
self.__highlight(selector, by=by, loops=loops, scroll=scroll)
|
||
self.click(selector, by=by)
|
||
|
||
def highlight_update_text(
|
||
self,
|
||
selector,
|
||
text,
|
||
by="css selector",
|
||
loops=3,
|
||
scroll=True,
|
||
timeout=None,
|
||
):
|
||
"""Highlights the element and then types text into the field."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if not self.demo_mode:
|
||
self.__highlight(selector, by=by, loops=loops, scroll=scroll)
|
||
self.update_text(selector, text, by=by)
|
||
|
||
def highlight_type(
|
||
self,
|
||
selector,
|
||
text,
|
||
by="css selector",
|
||
loops=3,
|
||
scroll=True,
|
||
timeout=None,
|
||
):
|
||
"""Same as self.highlight_update_text()
|
||
As above, highlights the element and then types text into the field."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if not self.demo_mode:
|
||
self.__highlight(selector, by=by, loops=loops, scroll=scroll)
|
||
self.update_text(selector, text, by=by)
|
||
|
||
def highlight_if_visible(
|
||
self, selector, by="css selector", loops=4, scroll=True,
|
||
):
|
||
"""Highlights the element if the element is visible."""
|
||
self.__check_scope()
|
||
if self.is_element_visible(selector, by=by):
|
||
self.__highlight(selector, by=by, loops=loops, scroll=scroll)
|
||
|
||
def __highlight_element(self, element, loops=None, scroll=True):
|
||
self.__check_scope()
|
||
if not loops:
|
||
loops = settings.HIGHLIGHTS
|
||
if scroll and self.browser != "safari":
|
||
with suppress(Exception):
|
||
self.__slow_scroll_to_element(element)
|
||
if self.highlights:
|
||
loops = self.highlights
|
||
if self.browser == "ie":
|
||
loops = 1 # Override previous setting because IE is slow
|
||
loops = int(loops)
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
# Headless modes have less need for highlighting elements.
|
||
# However, highlight() may be used as a sleep alternative.
|
||
loops = int(math.ceil(loops * 0.5))
|
||
o_bs = "" # original_box_shadow
|
||
try:
|
||
style = element.get_attribute("style")
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
style = element.get_attribute("style")
|
||
if style:
|
||
if "box-shadow: " in style:
|
||
box_start = style.find("box-shadow: ")
|
||
box_end = style.find(";", box_start) + 1
|
||
original_box_shadow = style[box_start:box_end]
|
||
o_bs = original_box_shadow
|
||
self.__highlight_element_with_js(element, loops, o_bs)
|
||
time.sleep(0.065)
|
||
|
||
def __highlight(
|
||
self, selector, by="css selector", loops=None, scroll=True
|
||
):
|
||
"""This method uses fancy JavaScript to highlight an element.
|
||
(Automatically using in S_e_l_e_n_i_u_m_B_a_s_e Demo Mode)"""
|
||
self.__check_scope()
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
if not loops:
|
||
loops = settings.HIGHLIGHTS
|
||
if scroll:
|
||
try:
|
||
if self.browser != "safari":
|
||
scroll_distance = js_utils.get_scroll_distance_to_element(
|
||
self.driver, element
|
||
)
|
||
if abs(scroll_distance) > constants.Values.SSMD:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
else:
|
||
self.__slow_scroll_to_element(element)
|
||
else:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
self.__slow_scroll_to_element(element)
|
||
use_element_directly = False
|
||
try:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
except Exception:
|
||
# If can't convert to CSS_Selector for JS, use element directly
|
||
use_element_directly = True
|
||
if self.highlights:
|
||
loops = self.highlights
|
||
if self.browser == "ie":
|
||
loops = 1 # Override previous setting because IE is slow
|
||
loops = int(loops)
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
# Headless modes have less need for highlighting elements.
|
||
# However, highlight() may be used as a sleep alternative.
|
||
loops = int(math.ceil(loops * 0.5))
|
||
o_bs = "" # original_box_shadow
|
||
try:
|
||
style = element.get_attribute("style")
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by="css selector", timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
style = element.get_attribute("style")
|
||
if style:
|
||
if "box-shadow: " in style:
|
||
box_start = style.find("box-shadow: ")
|
||
box_end = style.find(";", box_start) + 1
|
||
original_box_shadow = style[box_start:box_end]
|
||
o_bs = original_box_shadow
|
||
if use_element_directly:
|
||
self.__highlight_element_with_js(element, loops, o_bs)
|
||
elif ":contains" not in selector and ":first" not in selector:
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
self.__highlight_with_js(selector, loops, o_bs)
|
||
else:
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
try:
|
||
self.__highlight_with_jquery(selector, loops, o_bs)
|
||
except Exception:
|
||
pass # JQuery probably couldn't load. Skip highlighting.
|
||
time.sleep(0.065)
|
||
|
||
def highlight(
|
||
self,
|
||
selector,
|
||
by="css selector",
|
||
loops=None,
|
||
scroll=True,
|
||
timeout=None,
|
||
):
|
||
"""This method uses fancy JavaScript to highlight an element.
|
||
@Params
|
||
selector - the selector of the element to find (Accepts WebElement)
|
||
by - the type of selector to search by (Default: CSS)
|
||
loops - # of times to repeat the highlight animation
|
||
(Default: 4. Each loop lasts for about 0.2s)
|
||
scroll - the option to scroll to the element first (Default: True)
|
||
timeout - the time to wait for the element to appear """
|
||
self.__check_scope()
|
||
if not self.__is_cdp_swap_needed():
|
||
self._check_browser()
|
||
self.__skip_if_esc()
|
||
if isinstance(selector, WebElement):
|
||
self.__highlight_element(selector, loops=loops, scroll=scroll)
|
||
return
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
self.__highlight(selector=selector, by=by, loops=loops, scroll=scroll)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["hi_li", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def highlight_elements(
|
||
self,
|
||
selector,
|
||
by="css selector",
|
||
loops=None,
|
||
scroll=True,
|
||
limit=0,
|
||
):
|
||
if not limit:
|
||
limit = 0 # 0 means no limit
|
||
limit = int(limit)
|
||
count = 0
|
||
elements = self.find_elements(selector, by=by)
|
||
for element in elements:
|
||
with suppress(Exception):
|
||
if element.is_displayed():
|
||
self.__highlight_element(
|
||
element, loops=loops, scroll=scroll
|
||
)
|
||
count += 1
|
||
if limit > 0 and count >= limit:
|
||
break
|
||
|
||
def press_up_arrow(self, selector="body", times=1, by="css selector"):
|
||
"""Simulates pressing the UP Arrow on the keyboard.
|
||
By default, "html" will be used as the CSS Selector target.
|
||
You can specify how many times in-a-row the action happens."""
|
||
self.__check_scope()
|
||
if times < 1:
|
||
return
|
||
element = self.wait_for_element_present(selector)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
for i in range(int(times)):
|
||
try:
|
||
element.send_keys(Keys.ARROW_UP)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_visible(selector)
|
||
element.send_keys(Keys.ARROW_UP)
|
||
time.sleep(0.01)
|
||
if self.slow_mode:
|
||
time.sleep(0.1)
|
||
|
||
def press_down_arrow(self, selector="body", times=1, by="css selector"):
|
||
"""Simulates pressing the DOWN Arrow on the keyboard.
|
||
By default, "html" will be used as the CSS Selector target.
|
||
You can specify how many times in-a-row the action happens."""
|
||
self.__check_scope()
|
||
if times < 1:
|
||
return
|
||
element = self.wait_for_element_present(selector)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
for i in range(int(times)):
|
||
try:
|
||
element.send_keys(Keys.ARROW_DOWN)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_visible(selector)
|
||
element.send_keys(Keys.ARROW_DOWN)
|
||
time.sleep(0.01)
|
||
if self.slow_mode:
|
||
time.sleep(0.1)
|
||
|
||
def press_left_arrow(self, selector="body", times=1, by="css selector"):
|
||
"""Simulates pressing the LEFT Arrow on the keyboard.
|
||
By default, "html" will be used as the CSS Selector target.
|
||
You can specify how many times in-a-row the action happens."""
|
||
self.__check_scope()
|
||
if times < 1:
|
||
return
|
||
element = self.wait_for_element_present(selector)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
for i in range(int(times)):
|
||
try:
|
||
element.send_keys(Keys.ARROW_LEFT)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_visible(selector)
|
||
element.send_keys(Keys.ARROW_LEFT)
|
||
time.sleep(0.01)
|
||
if self.slow_mode:
|
||
time.sleep(0.1)
|
||
|
||
def press_right_arrow(self, selector="body", times=1, by="css selector"):
|
||
"""Simulates pressing the RIGHT Arrow on the keyboard.
|
||
By default, "html" will be used as the CSS Selector target.
|
||
You can specify how many times in-a-row the action happens."""
|
||
self.__check_scope()
|
||
if times < 1:
|
||
return
|
||
element = self.wait_for_element_present(selector)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
for i in range(int(times)):
|
||
try:
|
||
element.send_keys(Keys.ARROW_RIGHT)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
element = self.wait_for_element_visible(selector)
|
||
element.send_keys(Keys.ARROW_RIGHT)
|
||
time.sleep(0.01)
|
||
if self.slow_mode:
|
||
time.sleep(0.1)
|
||
|
||
def scroll_to(self, selector, by="css selector", timeout=None):
|
||
"""Fast scroll to destination"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.scroll_into_view(selector)
|
||
return
|
||
if self.demo_mode or self.slow_mode:
|
||
self.slow_scroll_to(selector, by=by, timeout=timeout)
|
||
return
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
try:
|
||
self.__scroll_to_element(element, selector, by)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
self.__scroll_to_element(element, selector, by)
|
||
|
||
def scroll_to_element(self, selector, by="css selector", timeout=None):
|
||
"""Same as self.scroll_to()"""
|
||
self.scroll_to(selector, by=by, timeout=timeout)
|
||
|
||
def slow_scroll_to(self, selector, by="css selector", timeout=None):
|
||
"""Slow motion scroll to destination"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
original_by = by
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed() and ":contains(" not in selector:
|
||
self.cdp.scroll_into_view(selector)
|
||
return
|
||
element = self.wait_for_element_visible(
|
||
original_selector, by=original_by, timeout=timeout
|
||
)
|
||
try:
|
||
if self.browser != "safari":
|
||
scroll_distance = js_utils.get_scroll_distance_to_element(
|
||
self.driver, element
|
||
)
|
||
if abs(scroll_distance) > constants.Values.SSMD:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
else:
|
||
self.__slow_scroll_to_element(element)
|
||
else:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
original_selector, by=original_by, timeout=timeout
|
||
)
|
||
self.__slow_scroll_to_element(element)
|
||
|
||
def slow_scroll_to_element(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.slow_scroll_to()"""
|
||
self.slow_scroll_to(selector, by=by, timeout=timeout)
|
||
|
||
def scroll_into_view(self, selector, by="css selector", timeout=None):
|
||
"""Uses the JS scrollIntoView() method to scroll to an element.
|
||
Unlike other scroll methods, (which put elements upper-center),
|
||
this method places elements at the very top of the screen."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.scroll_into_view(selector)
|
||
return
|
||
element = self.wait_for_element_visible(selector, by, timeout=timeout)
|
||
self.execute_script("arguments[0].scrollIntoView();", element)
|
||
|
||
def scroll_to_top(self):
|
||
"""Scroll to the top of the page."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.scroll_to_top()
|
||
return
|
||
scroll_script = "window.scrollTo(0, 0);"
|
||
with suppress(Exception):
|
||
self.execute_script(scroll_script)
|
||
time.sleep(0.012)
|
||
|
||
def scroll_to_bottom(self):
|
||
"""Scroll to the bottom of the page."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.scroll_to_bottom()
|
||
return
|
||
scroll_script = "window.scrollTo(0, 10000);"
|
||
with suppress(Exception):
|
||
self.execute_script(scroll_script)
|
||
time.sleep(0.012)
|
||
|
||
def scroll_to_y(self, y):
|
||
"""Scroll to y position on the page."""
|
||
self.__check_scope()
|
||
y = int(y)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.scroll_to_y(y)
|
||
return
|
||
scroll_script = "window.scrollTo(0, %s);" % y
|
||
with suppress(Exception):
|
||
self.execute_script(scroll_script)
|
||
time.sleep(0.012)
|
||
|
||
def click_xpath(self, xpath):
|
||
"""Technically, self.click() automatically detects xpath selectors,
|
||
so self.click_xpath() is just a longer name for the same action."""
|
||
self.click(xpath, by="xpath")
|
||
|
||
def js_click(
|
||
self,
|
||
selector,
|
||
by="css selector",
|
||
all_matches=False,
|
||
timeout=None,
|
||
scroll=True,
|
||
):
|
||
"""Clicks an element using JavaScript.
|
||
Can be used to click hidden / invisible elements.
|
||
If "all_matches" is False, only the first match is clicked.
|
||
If "scroll" is False, won't scroll unless running in Demo Mode."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.click(selector, timeout=timeout)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
if not timeout or timeout is True:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
if by == By.LINK_TEXT:
|
||
message = (
|
||
"Pure JavaScript doesn't support clicking by Link Text. "
|
||
"You may want to use self.jquery_click() instead, which "
|
||
"allows this with :contains(), assuming jQuery isn't blocked. "
|
||
"For now, self.js_click() will use a regular WebDriver click."
|
||
)
|
||
logging.debug(message)
|
||
self.click(selector, by=by)
|
||
return
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if not page_actions.is_element_clickable(self.driver, selector, by):
|
||
self.wait_for_ready_state_complete()
|
||
scroll_done = False
|
||
if self.is_element_visible(selector, by=by):
|
||
scroll_done = True
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if scroll and not self.demo_mode and not self.slow_mode:
|
||
success = js_utils.scroll_to_element(self.driver, element)
|
||
if not success:
|
||
self.wait_for_ready_state_complete()
|
||
timeout = settings.SMALL_TIMEOUT
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout=timeout
|
||
)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
time_stamp = 0
|
||
action = ["", "", "", time_stamp]
|
||
pre_action_url = None
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
pre_window_count = len(self.driver.window_handles)
|
||
if self.recorder_mode and not self.__dont_record_js_click:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
tag_name = None
|
||
href = ""
|
||
if ":contains\\(" not in css_selector:
|
||
tag_name = self.execute_script(
|
||
"return document.querySelector('%s').tagName.toLowerCase()"
|
||
";" % css_selector
|
||
)
|
||
if tag_name == "a":
|
||
href = self.execute_script(
|
||
"return document.querySelector('%s').href;" % css_selector
|
||
)
|
||
origin = self.get_origin()
|
||
href_origin = [href, origin]
|
||
action = ["js_cl", selector, href_origin, time_stamp]
|
||
if all_matches:
|
||
action[0] = "js_ca"
|
||
if not self.is_element_visible(selector, by=by):
|
||
self.wait_for_ready_state_complete()
|
||
if self.is_element_visible(selector, by=by):
|
||
if scroll and not scroll_done:
|
||
success = js_utils.scroll_to_element(self.driver, element)
|
||
if not success:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout=timeout
|
||
)
|
||
if self.recorder_mode and not self.__dont_record_js_click:
|
||
action[3] = self.execute_script("return Date.now();")
|
||
self.__extra_actions.append(action)
|
||
if not all_matches:
|
||
if ":contains\\(" not in css_selector:
|
||
try:
|
||
self.__js_click(selector, by=by)
|
||
except Exception:
|
||
current_url = self.driver.current_url
|
||
if current_url == pre_action_url:
|
||
self.__js_click_element(element)
|
||
else:
|
||
try:
|
||
self.__js_click_element(element)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.05)
|
||
current_url = self.driver.current_url
|
||
if current_url == pre_action_url:
|
||
element = self.wait_for_element_present(
|
||
selector, by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
if (
|
||
current_url == pre_action_url
|
||
and self.is_element_visible(selector)
|
||
and self.is_element_clickable(selector)
|
||
):
|
||
try:
|
||
self.__element_click(element)
|
||
except Exception:
|
||
try:
|
||
self.__js_click_element(element)
|
||
except Exception:
|
||
element = self.wait_for_element_present(
|
||
selector, by, timeout=settings.MINI_TIMEOUT
|
||
)
|
||
self.__js_click_element(element)
|
||
elif current_url == pre_action_url:
|
||
try:
|
||
self.__js_click_element(element)
|
||
except Exception:
|
||
current_url = self.driver.current_url
|
||
if current_url == pre_action_url:
|
||
element = self.wait_for_element_present(
|
||
selector, by, timeout=settings.MINI_TIMEOUT
|
||
)
|
||
self.__js_click_element(element)
|
||
else:
|
||
if ":contains\\(" not in css_selector:
|
||
self.__js_click_all(selector, by=by)
|
||
else:
|
||
click_script = """jQuery('%s').click();""" % css_selector
|
||
self.safe_execute_script(click_script)
|
||
latest_window_count = len(self.driver.window_handles)
|
||
if (
|
||
latest_window_count > pre_window_count
|
||
and (
|
||
self.recorder_mode
|
||
or (
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK
|
||
and self.driver.current_url == pre_action_url
|
||
)
|
||
)
|
||
):
|
||
self.__switch_to_newest_window_if_not_blank()
|
||
elif (
|
||
latest_window_count == pre_window_count - 1
|
||
and latest_window_count > 0
|
||
):
|
||
# If a click closes the active window,
|
||
# switch to the last one if it exists.
|
||
self.switch_to_window(-1)
|
||
with suppress(Exception):
|
||
self.wait_for_ready_state_complete()
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def js_click_if_present(self, selector, by="css selector", timeout=0):
|
||
"""If the page selector exists, js_click() the element.
|
||
This method only clicks on the first matching element found.
|
||
If a "timeout" is provided, waits that long for the element to
|
||
be present before giving up and returning without a js_click()."""
|
||
self.wait_for_ready_state_complete()
|
||
if self.is_element_present(selector, by=by):
|
||
self.js_click(selector, by=by)
|
||
elif timeout > 0:
|
||
with suppress(Exception):
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
if self.is_element_present(selector, by=by):
|
||
self.js_click(selector, by=by)
|
||
|
||
def js_click_if_visible(self, selector, by="css selector", timeout=0):
|
||
"""If the page selector exists and is visible, js_click() the element.
|
||
This method only clicks on the first matching element found.
|
||
If a "timeout" is provided, waits that long for the element
|
||
to appear before giving up and returning without a js_click()."""
|
||
self.wait_for_ready_state_complete()
|
||
if self.is_element_visible(selector, by=by):
|
||
self.js_click(selector, by=by)
|
||
elif timeout > 0:
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.js_click(selector, by=by)
|
||
|
||
def js_click_all(self, selector, by="css selector", timeout=None):
|
||
"""Clicks all matching elements using pure JS. (No jQuery)"""
|
||
self.js_click(
|
||
selector, by="css selector", all_matches=True, timeout=timeout
|
||
)
|
||
|
||
def jquery_click(self, selector, by="css selector", timeout=None):
|
||
"""Clicks an element using jQuery. (Different from using pure JS.)
|
||
Can be used to click hidden / invisible elements."""
|
||
self.__check_scope()
|
||
if not timeout or timeout is True:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = selector
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
click_script = """jQuery('%s')[0].click();""" % selector
|
||
if (
|
||
self.recorder_mode
|
||
and self.__current_url_is_recordable()
|
||
and self.get_session_storage_item("pause_recorder") == "no"
|
||
):
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
tag_name = None
|
||
href = ""
|
||
if ":contains\\(" not in css_selector:
|
||
tag_name = self.execute_script(
|
||
"return document.querySelector('%s').tagName.toLowerCase()"
|
||
";" % css_selector
|
||
)
|
||
if tag_name == "a":
|
||
href = self.execute_script(
|
||
"return document.querySelector('%s').href;" % css_selector
|
||
)
|
||
origin = self.get_origin()
|
||
href_origin = [href, origin]
|
||
action = ["jq_cl", original_selector, href_origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
self.safe_execute_script(click_script)
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def jquery_click_all(self, selector, by="css selector", timeout=None):
|
||
"""Clicks all matching elements using jQuery."""
|
||
self.__check_scope()
|
||
if not timeout or timeout is True:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
click_script = """jQuery('%s').click();""" % css_selector
|
||
if (
|
||
self.recorder_mode
|
||
and self.__current_url_is_recordable()
|
||
and self.get_session_storage_item("pause_recorder") == "no"
|
||
):
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
tag_name = None
|
||
href = ""
|
||
if ":contains\\(" not in css_selector:
|
||
tag_name = self.execute_script(
|
||
"return document.querySelector('%s').tagName.toLowerCase()"
|
||
";" % css_selector
|
||
)
|
||
if tag_name == "a":
|
||
href = self.execute_script(
|
||
"return document.querySelector('%s').href;" % css_selector
|
||
)
|
||
origin = self.get_origin()
|
||
href_origin = [href, origin]
|
||
action = ["jq_ca", original_selector, href_origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
self.safe_execute_script(click_script)
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def hide_element(self, selector, by="css selector"):
|
||
"""Hide the first element on the page that matches the selector."""
|
||
self.__check_scope()
|
||
element = None
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=0.5
|
||
)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector and element:
|
||
script = (
|
||
'const e = arguments[0];'
|
||
'e.style.display="none";e.style.visibility="hidden";'
|
||
)
|
||
self.execute_script(script, element)
|
||
elif ":contains(" in css_selector and not element:
|
||
selector = self.__make_css_match_first_element_only(css_selector)
|
||
script = """jQuery('%s').hide();""" % selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
'const e = document.querySelector("%s");'
|
||
'e.style.display="none";e.style.visibility="hidden";'
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def hide_elements(self, selector, by="css selector"):
|
||
"""Hide all elements on the page that match the selector."""
|
||
self.__check_scope()
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector:
|
||
script = """jQuery('%s').hide();""" % css_selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
$elements[index].style.display="none";
|
||
$elements[index].style.visibility="hidden";}"""
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def show_element(self, selector, by="css selector"):
|
||
"""Show the first element on the page that matches the selector."""
|
||
self.__check_scope()
|
||
element = None
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
element = self.wait_for_element_present(selector, by=by, timeout=1)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector and element:
|
||
script = (
|
||
'const e = arguments[0];'
|
||
'e.style.display="";e.style.visibility="visible";'
|
||
)
|
||
self.execute_script(script, element)
|
||
elif ":contains(" in css_selector and not element:
|
||
selector = self.__make_css_match_first_element_only(css_selector)
|
||
script = """jQuery('%s').show(0);""" % selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
'const e = document.querySelector("%s");'
|
||
'e.style.display="";e.style.visibility="visible";'
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def show_elements(self, selector, by="css selector"):
|
||
"""Show all elements on the page that match the selector."""
|
||
self.__check_scope()
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector:
|
||
script = """jQuery('%s').show(0);""" % css_selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
$elements[index].style.display="";
|
||
$elements[index].style.visibility="visible";}"""
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def remove_element(self, selector, by="css selector"):
|
||
"""Remove the first element on the page that matches the selector."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.remove_element(selector)
|
||
return
|
||
element = None
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=0.5
|
||
)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector and element:
|
||
script = (
|
||
'const e = arguments[0];'
|
||
'e.parentElement.removeChild(e);'
|
||
)
|
||
self.execute_script(script, element)
|
||
elif ":contains(" in css_selector and not element:
|
||
selector = self.__make_css_match_first_element_only(css_selector)
|
||
script = """jQuery('%s').remove();""" % selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
'const e = document.querySelector("%s");'
|
||
'e.parentElement.removeChild(e);'
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def remove_elements(self, selector, by="css selector"):
|
||
"""Remove all elements on the page that match the selector."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.remove_elements(selector)
|
||
return
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if ":contains(" in css_selector:
|
||
script = """jQuery('%s').remove();""" % css_selector
|
||
self.safe_execute_script(script)
|
||
else:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
$elements[index].remove();}"""
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def ad_block(self):
|
||
"""Block ads that appear on the current web page."""
|
||
from seleniumbase.config import ad_block_list
|
||
|
||
self.__check_scope() # Using wait_for_RSC would cause an infinite loop
|
||
for css_selector in ad_block_list.AD_BLOCK_LIST:
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
$elements[index].remove();}"""
|
||
% css_selector
|
||
)
|
||
try:
|
||
self.execute_script(script)
|
||
except Exception:
|
||
pass # Don't fail test if ad_blocking fails
|
||
|
||
def show_file_choosers(self):
|
||
"""Display hidden file-chooser input fields on sites if present."""
|
||
self.wait_for_ready_state_complete()
|
||
css_selector = 'input[type="file"]'
|
||
try:
|
||
self.wait_for_element_present(
|
||
css_selector, timeout=settings.MINI_TIMEOUT
|
||
)
|
||
except Exception:
|
||
time.sleep(0.14)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
try:
|
||
self.show_elements(css_selector)
|
||
except Exception:
|
||
time.sleep(0.16)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
the_class = $elements[index].getAttribute('class');
|
||
new_class = the_class.replaceAll('hidden', 'visible');
|
||
$elements[index].setAttribute('class', new_class);}"""
|
||
% css_selector
|
||
)
|
||
with suppress(Exception):
|
||
self.execute_script(script)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["sh_fc", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def disable_beforeunload(self):
|
||
"""This prevents: "Leave Site? Changes you made may not be saved."
|
||
on Chromium browsers (Chrome or Edge).
|
||
SB already sets "dom.disable_beforeunload" for Firefox options."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
if (
|
||
self.is_chromium()
|
||
and self.driver.current_url.startswith("http")
|
||
):
|
||
with suppress(Exception):
|
||
self.driver.execute_script("window.onbeforeunload=null;")
|
||
|
||
def get_domain_url(self, url):
|
||
self.__check_scope()
|
||
return page_utils.get_domain_url(url)
|
||
|
||
def get_active_element_css(self):
|
||
return js_utils.get_active_element_css(self.driver)
|
||
|
||
def get_beautiful_soup(self, source=None):
|
||
"""BeautifulSoup is a toolkit for dissecting an HTML document
|
||
and extracting what you need. It's great for screen-scraping!
|
||
See: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ """
|
||
from bs4 import BeautifulSoup
|
||
|
||
if not source:
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible(
|
||
"body", timeout=settings.MINI_TIMEOUT
|
||
)
|
||
source = self.get_page_source()
|
||
return BeautifulSoup(source, "html.parser")
|
||
|
||
def get_unique_links(self):
|
||
"""Get all unique links in the html of the page source.
|
||
Page links include those obtained from:
|
||
"a"->"href", "img"->"src", "link"->"href", and "script"->"src"."""
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.08)
|
||
if self.undetectable:
|
||
time.sleep(0.02)
|
||
with suppress(Exception):
|
||
self.wait_for_element_present("body", timeout=1.5)
|
||
self.wait_for_element_visible("body", timeout=1.5)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.25)
|
||
if self.undetectable:
|
||
time.sleep(0.123)
|
||
soup = self.get_beautiful_soup(self.get_page_source())
|
||
page_url = self.get_current_url()
|
||
return page_utils._get_unique_links(page_url, soup)
|
||
|
||
def get_link_status_code(
|
||
self,
|
||
link,
|
||
allow_redirects=False,
|
||
timeout=5,
|
||
verify=False,
|
||
):
|
||
"""Get the status code of a link.
|
||
If the timeout is set to less than 1, it becomes 1.
|
||
If the timeout is exceeded by requests.head(), it will return a 404.
|
||
If "verify" is False, will ignore certificate errors.
|
||
For a list of available status codes, see:
|
||
https://en.wikipedia.org/wiki/List_of_HTTP_status_codes """
|
||
if self.__requests_timeout:
|
||
timeout = self.__requests_timeout
|
||
if timeout < 1:
|
||
timeout = 1
|
||
return page_utils._get_link_status_code(
|
||
link,
|
||
allow_redirects=allow_redirects,
|
||
timeout=timeout,
|
||
verify=verify,
|
||
)
|
||
|
||
def assert_link_status_code_is_not_404(self, link):
|
||
status_code = str(self.get_link_status_code(link))
|
||
bad_link_str = 'Error: "%s" returned a 404!' % link
|
||
self.assertNotEqual(status_code, "404", bad_link_str)
|
||
|
||
def __get_link_if_404_error(self, link):
|
||
status_code = str(self.get_link_status_code(link))
|
||
if status_code == "404":
|
||
# Verify again to be sure. (In case of multi-threading overload.)
|
||
status_code = str(self.get_link_status_code(link))
|
||
if status_code == "404":
|
||
return link
|
||
else:
|
||
return None
|
||
else:
|
||
return None
|
||
|
||
def assert_no_404_errors(self, multithreaded=True, timeout=None):
|
||
"""Assert no 404 errors from page links obtained from:
|
||
"a"->"href", "img"->"src", "link"->"href", and "script"->"src".
|
||
Timeout is on a per-link basis using the "requests" library.
|
||
If timeout is None, uses the one set in get_link_status_code().
|
||
(That timeout value is currently set to 5 seconds per link.)
|
||
(A 404 error represents a broken link on a web page.)"""
|
||
all_links = self.get_unique_links()
|
||
links = []
|
||
for link in all_links:
|
||
if (
|
||
"data:" not in link
|
||
and "tel:" not in link
|
||
and "mailto:" not in link
|
||
and "javascript:" not in link
|
||
and "://fonts.gstatic.com" not in link
|
||
and "://fonts.googleapis.com" not in link
|
||
and "://googleads.g.doubleclick.net" not in link
|
||
):
|
||
links.append(link)
|
||
if timeout:
|
||
if not isinstance(timeout, (int, float)):
|
||
raise Exception('Expecting a numeric value for "timeout"!')
|
||
if timeout < 0:
|
||
raise Exception('The "timeout" cannot be a negative number!')
|
||
self.__requests_timeout = timeout
|
||
broken_links = []
|
||
if multithreaded:
|
||
from multiprocessing.dummy import Pool as ThreadPool
|
||
|
||
pool = ThreadPool(10)
|
||
results = pool.map(self.__get_link_if_404_error, links)
|
||
pool.close()
|
||
pool.join()
|
||
for result in results:
|
||
if result:
|
||
broken_links.append(result)
|
||
else:
|
||
broken_links = []
|
||
for link in links:
|
||
if self.__get_link_if_404_error(link):
|
||
broken_links.append(link)
|
||
self.__requests_timeout = None # Reset the requests.head() timeout
|
||
if len(broken_links) > 0:
|
||
broken_links = sorted(broken_links)
|
||
bad_links_str = "\n".join(broken_links)
|
||
if len(broken_links) == 1:
|
||
self.fail("Broken link detected:\n%s" % bad_links_str)
|
||
elif len(broken_links) > 1:
|
||
self.fail("Broken links detected:\n%s" % bad_links_str)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT NO 404 ERRORS"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_no_404_errors(self._language)
|
||
messenger_post = "<b>%s</b>" % a_t
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
|
||
def print_unique_links_with_status_codes(self):
|
||
"""Finds all unique links in the html of the page source
|
||
and then prints out those links with their status codes.
|
||
Format: ["link" -> "status_code"] (per line)
|
||
Page links include those obtained from:
|
||
"a"->"href", "img"->"src", "link"->"href", and "script"->"src"."""
|
||
page_url = self.get_current_url()
|
||
soup = self.get_beautiful_soup(self.get_page_source())
|
||
page_utils._print_unique_links_with_status_codes(page_url, soup)
|
||
|
||
def __fix_unicode_conversion(self, text):
|
||
"""Fixing Chinese characters when converting from PDF to HTML."""
|
||
text = text.replace("\u2f8f", "\u884c")
|
||
text = text.replace("\u2f45", "\u65b9")
|
||
text = text.replace("\u2f08", "\u4eba")
|
||
text = text.replace("\u2f70", "\u793a")
|
||
text = text.replace("\xe2\xbe\x8f", "\xe8\xa1\x8c")
|
||
text = text.replace("\xe2\xbd\xb0", "\xe7\xa4\xba")
|
||
text = text.replace("\xe2\xbe\x8f", "\xe8\xa1\x8c")
|
||
text = text.replace("\xe2\xbd\x85", "\xe6\x96\xb9")
|
||
return text
|
||
|
||
def __get_type_checked_text(self, text):
|
||
"""Do type-checking on text. Then return it when valid.
|
||
If the text is acceptable, return the text or str(text).
|
||
If the text is not acceptable, raise a Python Exception."""
|
||
if isinstance(text, str):
|
||
return text
|
||
elif isinstance(text, (int, float)):
|
||
return str(text) # Convert num to string
|
||
elif isinstance(text, bool):
|
||
raise Exception("text must be a string! Boolean found!")
|
||
elif type(text).__name__ == "NoneType":
|
||
raise Exception("text must be a string! NoneType found!")
|
||
elif isinstance(text, list):
|
||
raise Exception("text must be a string! List found!")
|
||
elif isinstance(text, tuple):
|
||
raise Exception("text must be a string! Tuple found!")
|
||
elif isinstance(text, set):
|
||
raise Exception("text must be a string! Set found!")
|
||
elif isinstance(text, dict):
|
||
raise Exception("text must be a string! Dict found!")
|
||
else:
|
||
return str(text)
|
||
|
||
def get_pdf_text(
|
||
self,
|
||
pdf,
|
||
page=None,
|
||
maxpages=None,
|
||
password=None,
|
||
codec="utf-8",
|
||
wrap=False,
|
||
nav=False,
|
||
override=False,
|
||
caching=True,
|
||
):
|
||
"""Gets text from a PDF file.
|
||
PDF can be either a URL or a file path on the local file system.
|
||
@Params
|
||
pdf - The URL or file path of the PDF file.
|
||
page - The page number (or a list of page numbers) of the PDF.
|
||
If a page number is provided, looks only at that page.
|
||
(1 is the first page, 2 is the second page, etc.)
|
||
If no page number is provided, returns all PDF text.
|
||
maxpages - Instead of providing a page number, you can provide
|
||
the number of pages to use from the beginning.
|
||
password - If the PDF is password-protected, enter it here.
|
||
codec - The compression format for character encoding.
|
||
(The default codec used by this method is 'utf-8'.)
|
||
wrap - Replaces ' \n' with ' ' so that individual sentences
|
||
from a PDF don't get broken up into separate lines when
|
||
getting converted into text format.
|
||
nav - If PDF is a URL, navigates to the URL in the browser first.
|
||
(Not needed because the PDF will be downloaded anyway.)
|
||
override - If the PDF file to be downloaded already exists in the
|
||
downloaded_files/ folder, that PDF will be used
|
||
instead of downloading it again.
|
||
caching - If resources should be cached via pdfminer."""
|
||
import warnings
|
||
|
||
with warnings.catch_warnings():
|
||
warnings.simplefilter("ignore", category=UserWarning)
|
||
pip_find_lock = fasteners.InterProcessLock(
|
||
constants.PipInstall.FINDLOCK
|
||
)
|
||
with pip_find_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(constants.PipInstall.FINDLOCK)
|
||
if sys.version_info < (3, 9):
|
||
# Fix bug in newer cryptography for Python 3.7 and 3.8:
|
||
# "pyo3_runtime.PanicException: Python API call failed"
|
||
try:
|
||
import cryptography
|
||
if cryptography.__version__ != "39.0.2":
|
||
del cryptography # To get newer ver
|
||
shared_utils.pip_install(
|
||
"cryptography", version="39.0.2"
|
||
)
|
||
import cryptography
|
||
except Exception:
|
||
shared_utils.pip_install(
|
||
"cryptography", version="39.0.2"
|
||
)
|
||
try:
|
||
from pdfminer.high_level import extract_text
|
||
except Exception:
|
||
shared_utils.pip_install("pdfminer.six")
|
||
from pdfminer.high_level import extract_text
|
||
if not password:
|
||
password = ""
|
||
if not maxpages:
|
||
maxpages = 0
|
||
if not pdf.lower().endswith(".pdf"):
|
||
raise Exception("%s is not a PDF file! (Expecting a .pdf)" % pdf)
|
||
file_path = None
|
||
if page_utils.is_valid_url(pdf):
|
||
downloads_folder = download_helper.get_downloads_folder()
|
||
if nav:
|
||
if self.get_current_url() != pdf:
|
||
self.open(pdf)
|
||
file_name = pdf.split("/")[-1]
|
||
file_path = os.path.join(downloads_folder, file_name)
|
||
if not os.path.exists(file_path):
|
||
self.download_file(pdf)
|
||
elif override:
|
||
self.download_file(pdf)
|
||
else:
|
||
if not os.path.exists(pdf):
|
||
raise Exception("%s is not a valid URL or file path!" % pdf)
|
||
file_path = os.path.abspath(pdf)
|
||
page_search = None # (Pages are delimited by '\x0c')
|
||
if isinstance(page, list):
|
||
pages = page
|
||
page_search = []
|
||
for page in pages:
|
||
page_search.append(page - 1)
|
||
elif isinstance(page, int):
|
||
page = page - 1
|
||
if page < 0:
|
||
page = 0
|
||
page_search = [page]
|
||
else:
|
||
page_search = None
|
||
pdf_text = extract_text(
|
||
file_path,
|
||
password="",
|
||
page_numbers=page_search,
|
||
maxpages=maxpages,
|
||
caching=caching,
|
||
codec=codec,
|
||
)
|
||
pdf_text = self.__fix_unicode_conversion(pdf_text)
|
||
if wrap:
|
||
pdf_text = pdf_text.replace(" \n", " ")
|
||
return pdf_text.strip()
|
||
|
||
def assert_pdf_text(
|
||
self,
|
||
pdf,
|
||
text,
|
||
page=None,
|
||
maxpages=None,
|
||
password=None,
|
||
codec="utf-8",
|
||
wrap=True,
|
||
nav=False,
|
||
override=False,
|
||
caching=True,
|
||
):
|
||
"""Asserts text in a PDF file.
|
||
PDF can be either a URL or a file path on the local file system.
|
||
@Params
|
||
pdf - The URL or file path of the PDF file.
|
||
text - The expected text to verify in the PDF.
|
||
page - The page number of the PDF to use (optional).
|
||
If a page number is provided, looks only at that page.
|
||
(1 is the first page, 2 is the second page, etc.)
|
||
If no page number is provided, looks at all the pages.
|
||
maxpages - Instead of providing a page number, you can provide
|
||
the number of pages to use from the beginning.
|
||
password - If the PDF is password-protected, enter it here.
|
||
codec - The compression format for character encoding.
|
||
(The default codec used by this method is 'utf-8'.)
|
||
wrap - Replaces ' \n' with ' ' so that individual sentences
|
||
from a PDF don't get broken up into separate lines when
|
||
getting converted into text format.
|
||
nav - If PDF is a URL, navigates to the URL in the browser first.
|
||
(Not needed because the PDF will be downloaded anyway.)
|
||
override - If the PDF file to be downloaded already exists in the
|
||
downloaded_files/ folder, that PDF will be used
|
||
instead of downloading it again.
|
||
caching - If resources should be cached via pdfminer."""
|
||
text = self.__fix_unicode_conversion(text)
|
||
if not codec:
|
||
codec = "utf-8"
|
||
pdf_text = self.get_pdf_text(
|
||
pdf,
|
||
page=page,
|
||
maxpages=maxpages,
|
||
password=password,
|
||
codec=codec,
|
||
wrap=wrap,
|
||
nav=nav,
|
||
override=override,
|
||
caching=caching,
|
||
)
|
||
if isinstance(page, int):
|
||
if text not in pdf_text:
|
||
self.fail(
|
||
"PDF [%s] is missing expected text [%s] on "
|
||
"page [%s]!" % (pdf, text, page)
|
||
)
|
||
else:
|
||
if text not in pdf_text:
|
||
self.fail(
|
||
"PDF [%s] is missing expected text [%s]!" % (pdf, text)
|
||
)
|
||
return True
|
||
|
||
def create_folder(self, folder):
|
||
"""Creates a folder of the given name if it doesn't already exist."""
|
||
if folder.endswith("/"):
|
||
folder = folder[:-1]
|
||
if len(folder) < 1:
|
||
raise Exception("Minimum folder name length = 1.")
|
||
if not os.path.exists(folder):
|
||
with suppress(Exception):
|
||
os.makedirs(folder)
|
||
|
||
def choose_file(
|
||
self, selector, file_path, by="css selector", timeout=None
|
||
):
|
||
"""This method is used to choose a file to upload to a website.
|
||
It works by populating a file-chooser "input" field of type="file".
|
||
A relative file_path will get converted into an absolute file_path.
|
||
|
||
Example usage:
|
||
self.choose_file('input[type="file"]', "my_dir/my_file.txt") """
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
abs_path = os.path.abspath(file_path)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.02)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.08)
|
||
if self.is_element_visible(selector, by=by):
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
else:
|
||
choose_file_selector = 'input[type="file"]'
|
||
if self.is_element_present(choose_file_selector):
|
||
if not self.is_element_visible(choose_file_selector):
|
||
self.show_file_choosers()
|
||
if self.is_element_visible(selector, by=by):
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.__scroll_to_element(element, selector, by)
|
||
pre_action_url = None
|
||
if self.demo_mode:
|
||
with suppress(Exception):
|
||
pre_action_url = self.driver.current_url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sele_file_path = [selector, file_path]
|
||
action = ["chfil", sele_file_path, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if isinstance(abs_path, (int, float)):
|
||
abs_path = str(abs_path)
|
||
try:
|
||
if self.browser == "safari":
|
||
try:
|
||
element.send_keys(abs_path)
|
||
except NoSuchElementException:
|
||
pass # May get this error on Safari even if upload works.
|
||
else:
|
||
element.send_keys(abs_path)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.16)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
if self.browser == "safari":
|
||
try:
|
||
element.send_keys(abs_path)
|
||
except NoSuchElementException:
|
||
pass # May get this error on Safari even if upload works.
|
||
else:
|
||
element.send_keys(abs_path)
|
||
if self.demo_mode:
|
||
if self.driver.current_url != pre_action_url:
|
||
if not js_utils.is_jquery_activated(self.driver):
|
||
js_utils.add_js_link(self.driver, constants.JQuery.MIN_JS)
|
||
self.__demo_mode_pause_if_active()
|
||
else:
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def save_element_as_image_file(
|
||
self, selector, file_name, folder=None, overlay_text=""
|
||
):
|
||
"""Take a screenshot of an element and save it as an image file.
|
||
If no folder is specified, will save it to the current folder.
|
||
If overlay_text is provided, will add that to the saved image."""
|
||
element = self.wait_for_element_visible(selector)
|
||
element_png = element.screenshot_as_png
|
||
if len(file_name.split(".")[0]) < 1:
|
||
raise Exception("Error: file_name length must be > 0.")
|
||
if not file_name.endswith(".png"):
|
||
file_name = file_name + ".png"
|
||
image_file_path = None
|
||
if folder:
|
||
if folder.endswith(os.sep):
|
||
folder = folder[:-1]
|
||
if len(folder) > 0:
|
||
self.create_folder(folder)
|
||
image_file_path = os.path.join(folder, file_name)
|
||
if not image_file_path:
|
||
image_file_path = file_name
|
||
with open(image_file_path, "wb") as file:
|
||
file.write(element_png)
|
||
# Add a text overlay if given
|
||
if isinstance(overlay_text, str) and len(overlay_text) > 0:
|
||
pip_find_lock = fasteners.InterProcessLock(
|
||
constants.PipInstall.FINDLOCK
|
||
)
|
||
with pip_find_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(constants.PipInstall.FINDLOCK)
|
||
try:
|
||
from PIL import Image, ImageDraw
|
||
except Exception:
|
||
shared_utils.pip_install("Pillow")
|
||
try:
|
||
from PIL import Image, ImageDraw
|
||
except Exception as e:
|
||
if (
|
||
sys.version_info >= (3, 12)
|
||
and "symbol not found" in e.msg
|
||
):
|
||
raise Exception(
|
||
"PIL / Pillow is not supported on Python %s"
|
||
% ".".join(str(s) for s in sys.version_info)
|
||
)
|
||
else:
|
||
raise
|
||
text_rows = overlay_text.split("\n")
|
||
len_text_rows = len(text_rows)
|
||
max_width = 0
|
||
for text_row in text_rows:
|
||
if len(text_row) > max_width:
|
||
max_width = len(text_row)
|
||
image = Image.open(image_file_path)
|
||
draw = ImageDraw.Draw(image)
|
||
draw.rectangle(
|
||
(0, 0, int(max_width * 8.32) + 10, 23 * len_text_rows + 2),
|
||
fill=(236, 236, 28),
|
||
)
|
||
draw.text(
|
||
(4, 2), # Coordinates
|
||
overlay_text, # Text
|
||
fill=(8, 38, 176), # Color
|
||
font_size=18, # Font Size
|
||
)
|
||
image.save(image_file_path, "PNG", quality=100, optimize=True)
|
||
|
||
def download_file(self, file_url, destination_folder=None):
|
||
"""Downloads the file from the url to the destination folder.
|
||
If no destination folder is specified, the default one is used.
|
||
(The default folder for downloads is "./downloaded_files")"""
|
||
download_file_lock = fasteners.InterProcessLock(
|
||
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
|
||
)
|
||
with download_file_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(
|
||
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
|
||
)
|
||
if not destination_folder:
|
||
destination_folder = constants.Files.DOWNLOADS_FOLDER
|
||
if not os.path.exists(destination_folder):
|
||
os.makedirs(destination_folder)
|
||
page_utils._download_file_to(file_url, destination_folder)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
url_dest = [file_url, destination_folder]
|
||
action = ["do_fi", url_dest, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def save_file_as(self, file_url, new_file_name, destination_folder=None):
|
||
"""Similar to self.download_file(), except that you get to rename the
|
||
file being downloaded to whatever you want."""
|
||
download_file_lock = fasteners.InterProcessLock(
|
||
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
|
||
)
|
||
with download_file_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(
|
||
constants.MultiBrowser.DOWNLOAD_FILE_LOCK
|
||
)
|
||
if not destination_folder:
|
||
destination_folder = constants.Files.DOWNLOADS_FOLDER
|
||
if not os.path.exists(destination_folder):
|
||
os.makedirs(destination_folder)
|
||
page_utils._download_file_to(
|
||
file_url, destination_folder, new_file_name
|
||
)
|
||
|
||
def save_data_as(self, data, file_name, destination_folder=None):
|
||
"""Saves the data specified to the file specified.
|
||
If no destination folder is specified, the default one is used.
|
||
(The default folder = "./downloaded_files")
|
||
Use "." as the destination folder for the current directory."""
|
||
if not destination_folder:
|
||
destination_folder = constants.Files.DOWNLOADS_FOLDER
|
||
page_utils._save_data_as(data, destination_folder, file_name)
|
||
|
||
def append_data_to_file(self, data, file_name, destination_folder=None):
|
||
"""Appends the data specified to the file specified.
|
||
If no destination folder is specified, the default one is used.
|
||
(The default folder = "./downloaded_files")
|
||
Use "." as the folder for the current directory."""
|
||
if not destination_folder:
|
||
destination_folder = constants.Files.DOWNLOADS_FOLDER
|
||
page_utils._append_data_to_file(data, destination_folder, file_name)
|
||
|
||
def get_file_data(self, file_name, folder=None):
|
||
"""Gets the data from the file specified.
|
||
If no folder is specified, the default one is used.
|
||
The default folder = "./downloaded_files"
|
||
For the "latest_logs/" test data folders, use:
|
||
self.data_path OR self.data_abspath
|
||
Use "." as the folder for the current directory."""
|
||
if not folder:
|
||
folder = constants.Files.DOWNLOADS_FOLDER
|
||
return page_utils._get_file_data(folder, file_name)
|
||
|
||
def get_downloads_folder(self):
|
||
"""Returns the path of the SeleniumBase "downloaded_files/" folder.
|
||
Calling self.download_file(file_url) will put that file in here.
|
||
With the exception of Safari, IE, and Chromium Guest Mode,
|
||
any clicks that download files will also use this folder
|
||
rather than using the browser's default "downloads/" path."""
|
||
self.__check_scope()
|
||
return download_helper.get_downloads_folder()
|
||
|
||
def get_browser_downloads_folder(self):
|
||
"""Returns the path that is used when a click initiates a download.
|
||
SeleniumBase overrides the system path to be "downloaded_files/"
|
||
The path can't be changed on Safari, IE, or Chromium Guest Mode.
|
||
The same problem occurs when using an out-of-date chromedriver."""
|
||
self.__check_scope()
|
||
if self.is_chromium() and self.guest_mode and not self.headless:
|
||
# Guest Mode (non-headless) can force the default downloads path
|
||
return os.path.join(os.path.expanduser("~"), "downloads")
|
||
elif self.browser == "safari" or self.browser == "ie":
|
||
# Can't change the system [Downloads Folder] on Safari or IE
|
||
return os.path.join(os.path.expanduser("~"), "downloads")
|
||
elif (
|
||
"chrome" in self.driver.capabilities
|
||
and int(self.get_chromedriver_version().split(".")[0]) < 73
|
||
and self.headless
|
||
):
|
||
return os.path.join(os.path.expanduser("~"), "downloads")
|
||
elif (
|
||
"chrome" in self.driver.capabilities
|
||
and int(self.get_chromedriver_version().split(".")[0]) >= 110
|
||
and int(self.get_chromedriver_version().split(".")[0]) <= 112
|
||
and self.headless
|
||
):
|
||
return os.path.abspath(".")
|
||
else:
|
||
return download_helper.get_downloads_folder()
|
||
return os.path.join(os.path.expanduser("~"), "downloads")
|
||
|
||
def get_downloaded_files(self, regex=None, browser=False):
|
||
"""Returns a list of files in the [Downloads Folder].
|
||
Depending on settings, that dir may have other files.
|
||
If regex is provided, uses that to filter results."""
|
||
df = self.get_downloads_folder()
|
||
if browser:
|
||
df = self.get_browser_downloads_folder()
|
||
if not os.path.exists(df):
|
||
return []
|
||
elif regex:
|
||
return [fn for fn in os.listdir(df) if re.match(regex, fn)]
|
||
else:
|
||
return os.listdir(df)
|
||
|
||
def get_path_of_downloaded_file(self, file, browser=False):
|
||
"""Returns the full OS path of the downloaded file."""
|
||
self.__check_scope()
|
||
if browser:
|
||
return os.path.join(self.get_browser_downloads_folder(), file)
|
||
else:
|
||
return os.path.join(self.get_downloads_folder(), file)
|
||
|
||
def get_data_from_downloaded_file(self, file, timeout=None, browser=False):
|
||
"""Returns the contents of the downloaded file specified."""
|
||
self.assert_downloaded_file(file, timeout=timeout, browser=browser)
|
||
fpath = self.get_path_of_downloaded_file(file, browser=browser)
|
||
file_io_lock = fasteners.InterProcessLock(
|
||
constants.MultiBrowser.FILE_IO_LOCK
|
||
)
|
||
with file_io_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(constants.MultiBrowser.FILE_IO_LOCK)
|
||
with open(fpath, "r") as f:
|
||
data = f.read().strip()
|
||
return data
|
||
|
||
def is_downloaded_file_present(self, file, browser=False):
|
||
"""Returns True if the file exists in the pre-set [Downloads Folder].
|
||
For browser click-initiated downloads, SeleniumBase will override
|
||
the system [Downloads Folder] to be "./downloaded_files/",
|
||
but that path can't be overridden when using Safari, IE,
|
||
or Chromium Guest Mode, which keeps the default system path.
|
||
self.download_file(file_url) will always use "./downloaded_files/".
|
||
@Params
|
||
file - The filename of the downloaded file.
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
return os.path.exists(
|
||
self.get_path_of_downloaded_file(file, browser=browser)
|
||
)
|
||
|
||
def is_downloaded_file_regex_present(self, regex, browser=False):
|
||
"""Returns True if the filename regex exists in the [Downloads Folder].
|
||
Uses Python regex via the "re" library for string-matching on the name.
|
||
@Params
|
||
regex - The filename regex of the downloaded file.
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
df = self.get_downloads_folder()
|
||
if browser:
|
||
df = self.get_browser_downloads_folder()
|
||
matches = [fn for fn in os.listdir(df) if re.match(regex, fn)]
|
||
return len(matches) >= 1
|
||
|
||
def delete_downloaded_file_if_present(self, file, browser=False):
|
||
"""Deletes the file from the [Downloads Folder] if the file exists.
|
||
For browser click-initiated downloads, SeleniumBase will override
|
||
the system [Downloads Folder] to be "./downloaded_files/",
|
||
but that path can't be overridden when using Safari, IE,
|
||
or Chromium Guest Mode, which keeps the default system path.
|
||
self.download_file(file_url) will always use "./downloaded_files/".
|
||
@Params
|
||
file - The filename to be deleted from the [Downloads Folder].
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
if self.is_downloaded_file_present(file, browser=browser):
|
||
file_path = self.get_path_of_downloaded_file(file, browser=browser)
|
||
with suppress(Exception):
|
||
os.remove(file_path)
|
||
|
||
def delete_downloaded_file(self, file, browser=False):
|
||
"""Same as self.delete_downloaded_file_if_present()
|
||
Deletes the file from the [Downloads Folder] if the file exists.
|
||
For browser click-initiated downloads, SeleniumBase will override
|
||
the system [Downloads Folder] to be "./downloaded_files/",
|
||
but that path can't be overridden when using Safari, IE,
|
||
or Chromium Guest Mode, which keeps the default system path.
|
||
self.download_file(file_url) will always use "./downloaded_files/".
|
||
@Params
|
||
file - The filename to be deleted from the [Downloads Folder].
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
if self.is_downloaded_file_present(file, browser=browser):
|
||
file_path = self.get_path_of_downloaded_file(file, browser=browser)
|
||
with suppress(Exception):
|
||
os.remove(file_path)
|
||
|
||
def assert_downloaded_file(self, file, timeout=None, browser=False):
|
||
"""Asserts that the file exists in SeleniumBase's [Downloads Folder].
|
||
For browser click-initiated downloads, SeleniumBase will override
|
||
the system [Downloads Folder] to be "./downloaded_files/",
|
||
but that path can't be overridden when using Safari, IE,
|
||
or Chromium Guest Mode, which keeps the default system path.
|
||
self.download_file(file_url) will always use "./downloaded_files/".
|
||
@Params
|
||
file - The filename of the downloaded file.
|
||
timeout - The time (seconds) to wait for the download to complete.
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
self.__check_scope()
|
||
df = self.get_downloads_folder()
|
||
if browser:
|
||
df = self.get_browser_downloads_folder()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (timeout * 1000.0)
|
||
downloaded_file_path = self.get_path_of_downloaded_file(file, browser)
|
||
found = False
|
||
for x in range(int(timeout)):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
try:
|
||
self.assertTrue(
|
||
os.path.exists(downloaded_file_path),
|
||
"File [%s] was not found in the downloads folder [%s]!"
|
||
% (file, df),
|
||
)
|
||
found = True
|
||
break
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(1)
|
||
if not found and not os.path.exists(downloaded_file_path):
|
||
message = (
|
||
"File {%s} was not found in the downloads folder {%s} "
|
||
"after %s seconds! (Or the download didn't complete!)"
|
||
% (file, df, timeout)
|
||
)
|
||
page_actions.timeout_exception("NoSuchFileException", message)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_df", file, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if self.demo_mode:
|
||
messenger_post = "<b>ASSERT DOWNLOADED FILE</b>: [%s]" % file
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def assert_downloaded_file_regex(self, regex, timeout=None, browser=False):
|
||
"""Assert the filename regex exists in SeleniumBase's Downloads Folder.
|
||
Uses Python regex via the "re" library for string-matching on the name.
|
||
@Params
|
||
regex - The filename regex of the downloaded file.
|
||
timeout - The time (seconds) to wait for the download to complete.
|
||
browser - If True, uses the path set by click-initiated downloads.
|
||
If False, uses the self.download_file(file_url) path.
|
||
Those paths are usually the same. (browser-dependent)."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (timeout * 1000.0)
|
||
found = False
|
||
df = self.get_downloads_folder()
|
||
if browser:
|
||
df = self.get_browser_downloads_folder()
|
||
for x in range(int(timeout)):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
try:
|
||
matches = [fn for fn in os.listdir(df) if re.match(regex, fn)]
|
||
self.assertTrue(
|
||
len(matches) >= 1,
|
||
"Regex [%s] was not found in the downloads folder [%s]!"
|
||
% (regex, df),
|
||
)
|
||
found = True
|
||
break
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(1)
|
||
if not found:
|
||
message = (
|
||
"Regex {%s} was not found in the downloads folder {%s} "
|
||
"after %s seconds! (Or the download didn't complete!)"
|
||
% (regex, df, timeout)
|
||
)
|
||
page_actions.timeout_exception("NoSuchFileException", message)
|
||
if self.demo_mode:
|
||
messenger_post = (
|
||
"<b>ASSERT DOWNLOADED FILE REGEX</b>: [%s]" % regex
|
||
)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def assert_data_in_downloaded_file(
|
||
self, data, file, timeout=None, browser=False
|
||
):
|
||
"""Assert that the expected data exists in the downloaded file."""
|
||
self.assert_downloaded_file(file, timeout=timeout, browser=browser)
|
||
expected = data.strip()
|
||
actual = self.get_data_from_downloaded_file(file, browser=browser)
|
||
if expected not in actual:
|
||
message = (
|
||
"Expected data [%s] is not in downloaded file [%s]!"
|
||
% (expected, file)
|
||
)
|
||
raise Exception(message)
|
||
return True
|
||
|
||
def assert_true(self, expr, msg=None):
|
||
"""Asserts that the expression is True.
|
||
Raises an exception if the statement if False."""
|
||
self.assertTrue(expr, msg=msg)
|
||
|
||
def assert_false(self, expr, msg=None):
|
||
"""Asserts that the expression is False.
|
||
Raises an exception if the statement if True."""
|
||
self.assertFalse(expr, msg=msg)
|
||
|
||
def assert_equal(self, first, second, msg=None):
|
||
"""Asserts that the two values are equal.
|
||
Raises an exception if the values are not equal."""
|
||
self.assertEqual(first, second, msg=msg)
|
||
|
||
def assert_not_equal(self, first, second, msg=None):
|
||
"""Asserts that the two values are not equal.
|
||
Raises an exception if the values are equal."""
|
||
self.assertNotEqual(first, second, msg=msg)
|
||
|
||
def assert_in(self, first, second, msg=None):
|
||
"""Asserts that the first string is in the second string.
|
||
Raises an exception if the first string is not in the second."""
|
||
self.assertIn(first, second, msg=msg)
|
||
|
||
def assert_not_in(self, first, second, msg=None):
|
||
"""Asserts that the first string is not in the second string.
|
||
Raises an exception if the first string is in the second string."""
|
||
self.assertNotIn(first, second, msg=msg)
|
||
|
||
def assert_raises(self, *args, **kwargs):
|
||
"""Asserts that the following block of code raises an exception.
|
||
Raises an exception if the block of code has no exception.
|
||
Usage Example =>
|
||
# Verify that the expected exception is raised.
|
||
with self.assert_raises(Exception):
|
||
raise Exception("Expected Exception!") """
|
||
return self.assertRaises(*args, **kwargs)
|
||
|
||
def wait_for_attribute(
|
||
self, selector, attribute, value=None, by="css selector", timeout=None
|
||
):
|
||
"""Raises an exception if the element attribute/value is not found.
|
||
If the value is not specified, the attribute only needs to exist.
|
||
Returns the element that contains the attribute if successful.
|
||
Default timeout = LARGE_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element_attribute(selector, attribute, value)
|
||
return
|
||
elif self.__is_shadow_selector(selector):
|
||
return self.__wait_for_shadow_attribute_present(
|
||
selector, attribute, value=value, timeout=timeout
|
||
)
|
||
return page_actions.wait_for_attribute(
|
||
self.driver,
|
||
selector,
|
||
attribute,
|
||
value=value,
|
||
by=by,
|
||
timeout=timeout,
|
||
)
|
||
|
||
def assert_attribute(
|
||
self, selector, attribute, value=None, by="css selector", timeout=None
|
||
):
|
||
"""Raises an exception if the element attribute/value is not found.
|
||
If the value is not specified, the attribute only needs to exist.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element_attribute(selector, attribute, value)
|
||
return
|
||
self.wait_for_attribute(
|
||
selector, attribute, value=value, by=by, timeout=timeout
|
||
)
|
||
if (
|
||
self.demo_mode
|
||
and not self.__is_shadow_selector(selector)
|
||
and self.is_element_visible(selector, by=by)
|
||
):
|
||
a_a = "ASSERT ATTRIBUTE"
|
||
i_n = "in"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_a = SD.translate_assert_attribute(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
if not value:
|
||
messenger_post = "<b>%s</b>: [%s] %s %s: %s" % (
|
||
a_a,
|
||
attribute,
|
||
i_n,
|
||
by.upper(),
|
||
selector,
|
||
)
|
||
else:
|
||
messenger_post = '<b>%s</b>: [%s="%s"] %s %s: %s' % (
|
||
a_a,
|
||
attribute,
|
||
value,
|
||
i_n,
|
||
by.upper(),
|
||
selector,
|
||
)
|
||
self.__highlight_with_assert_success(messenger_post, selector, by)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
value = value.replace("\\", "\\\\")
|
||
sel_att_val = [selector, attribute, value]
|
||
action = ["as_at", sel_att_val, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_title(self, title):
|
||
"""Asserts that the web page title matches the expected title.
|
||
When a web page initially loads, the title starts as the URL,
|
||
but then the title switches over to the actual page title.
|
||
In Recorder Mode, this assertion is skipped because the Recorder
|
||
changes the page title to the selector of the hovered element."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_title(title)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
expected = title.strip()
|
||
actual = self.get_page_title().strip()
|
||
error = (
|
||
"Expected page title [%s] does not match the actual title [%s]!"
|
||
)
|
||
if not self.recorder_mode:
|
||
try:
|
||
self.assertEqual(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_page_title().strip()
|
||
try:
|
||
self.assertEqual(
|
||
expected, actual, error % (expected, actual)
|
||
)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_page_title().strip()
|
||
self.assertEqual(
|
||
expected, actual, error % (expected, actual)
|
||
)
|
||
if self.demo_mode and not self.recorder_mode:
|
||
a_t = "ASSERT TITLE"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_title(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_t, expected)
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_ti", expected, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_title_contains(self, substring):
|
||
"""Asserts that the title substring appears in the web page title.
|
||
When a web page initially loads, the title starts as the URL,
|
||
but then the title switches over to the actual page title.
|
||
In Recorder Mode, this assertion is skipped because the Recorder
|
||
changes the page title to the selector of the hovered element."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_title_contains(substring)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
expected = substring.strip()
|
||
actual = self.get_page_title().strip()
|
||
error = (
|
||
"Expected title substring [%s] does not appear "
|
||
"in the actual page title [%s]!"
|
||
)
|
||
if not self.recorder_mode:
|
||
try:
|
||
self.assertIn(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_page_title().strip()
|
||
try:
|
||
self.assertIn(
|
||
expected, actual, error % (expected, actual)
|
||
)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_page_title().strip()
|
||
self.assertIn(
|
||
expected, actual, error % (expected, actual)
|
||
)
|
||
if self.demo_mode and not self.recorder_mode:
|
||
a_t = "ASSERT TITLE CONTAINS"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_title_contains(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_t, expected)
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_tc", expected, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_url(self, url):
|
||
"""Asserts that the web page URL matches the expected URL."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_url(url)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
expected = url.strip()
|
||
actual = self.get_current_url().strip()
|
||
error = "Expected URL [%s] does not match the actual URL [%s]!"
|
||
try:
|
||
self.assertEqual(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_current_url().strip()
|
||
try:
|
||
self.assertEqual(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_current_url().strip()
|
||
self.assertEqual(expected, actual, error % (expected, actual))
|
||
if self.demo_mode and not self.recorder_mode:
|
||
a_u = "ASSERT URL"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_u = SD.translate_assert_url(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_u, expected)
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["a_url", expected, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_url_contains(self, substring):
|
||
"""Asserts that the URL substring appears in the full URL."""
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_url_contains(substring)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
expected = substring.strip()
|
||
actual = self.get_current_url().strip()
|
||
error = (
|
||
"Expected URL substring [%s] does not appear "
|
||
"in the full URL [%s]!"
|
||
)
|
||
try:
|
||
self.assertIn(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_current_url().strip()
|
||
try:
|
||
self.assertIn(expected, actual, error % (expected, actual))
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(2)
|
||
actual = self.get_current_url().strip()
|
||
self.assertIn(expected, actual, error % (expected, actual))
|
||
if self.demo_mode and not self.recorder_mode:
|
||
a_u = "ASSERT URL CONTAINS"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_u = SD.translate_assert_url_contains(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_u, expected)
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["a_u_c", expected, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_no_js_errors(self, exclude=[]):
|
||
"""Asserts current URL has no "SEVERE"-level JavaScript errors.
|
||
Works ONLY on Chromium browsers (Chrome or Edge).
|
||
Does NOT work on Firefox, IE, Safari, or some other browsers:
|
||
* See https://github.com/SeleniumHQ/selenium/issues/1161
|
||
Based on the following Stack Overflow solution:
|
||
* https://stackoverflow.com/a/41150512/7058266
|
||
@Params
|
||
exclude -->
|
||
A list of substrings or a single comma-separated string of
|
||
substrings for filtering out error URLs that contain them.
|
||
URLs that contain any excluded substring will get excluded
|
||
from the final errors list that's used with the assertion.
|
||
Examples:
|
||
self.assert_no_js_errors()
|
||
self.assert_no_js_errors(exclude=["/api.", "/analytics."])
|
||
self.assert_no_js_errors(exclude="//api.go,/analytics.go")
|
||
self.assert_no_js_errors(exclude=["Uncaught SyntaxError"])
|
||
self.assert_no_js_errors(exclude=["TypeError", "SyntaxE"]) """
|
||
self.__check_scope()
|
||
if exclude and not isinstance(exclude, (list, tuple)):
|
||
exclude = str(exclude).replace(" ", "").split(",")
|
||
time.sleep(0.1) # May take a moment for errors to appear after loads.
|
||
try:
|
||
browser_logs = self.driver.get_log("browser")
|
||
except (ValueError, WebDriverException):
|
||
# If unable to get browser logs, skip the assert and return.
|
||
return
|
||
messenger_library = "//cdnjs.cloudflare.com/ajax/libs/messenger"
|
||
underscore_library = "//cdnjs.cloudflare.com/ajax/libs/underscore"
|
||
errors = []
|
||
for entry in browser_logs:
|
||
if entry["level"] == "SEVERE":
|
||
if (
|
||
messenger_library not in entry["message"]
|
||
and underscore_library not in entry["message"]
|
||
):
|
||
# Add errors if not caused by SeleniumBase dependencies
|
||
if not exclude:
|
||
errors.append(entry)
|
||
else:
|
||
found = False
|
||
message = entry["message"]
|
||
for substring in exclude:
|
||
substring = str(substring)
|
||
if (
|
||
len(substring) > 0
|
||
and substring in message
|
||
):
|
||
found = True
|
||
break
|
||
if not found:
|
||
errors.append(entry)
|
||
if len(errors) > 0:
|
||
for n in range(len(errors)):
|
||
f_t_l_r = " - Failed to load resource"
|
||
u_c_s_e = " Uncaught SyntaxError: "
|
||
u_c_t_e = " Uncaught TypeError: "
|
||
if f_t_l_r in errors[n]["message"]:
|
||
url = errors[n]["message"].split(f_t_l_r)[0]
|
||
if "status of 400" in errors[n]["message"]:
|
||
errors[n] = {"Error 400 (Bad Request)": url}
|
||
elif "status of 401" in errors[n]["message"]:
|
||
errors[n] = {"Error 401 (Unauthorized)": url}
|
||
elif "status of 402" in errors[n]["message"]:
|
||
errors[n] = {"Error 402 (Payment Required)": url}
|
||
elif "status of 403" in errors[n]["message"]:
|
||
errors[n] = {"Error 403 (Forbidden)": url}
|
||
elif "status of 404" in errors[n]["message"]:
|
||
errors[n] = {"Error 404 (Not Found)": url}
|
||
elif "status of 405" in errors[n]["message"]:
|
||
errors[n] = {"Error 405 (Method Not Allowed)": url}
|
||
elif "status of 406" in errors[n]["message"]:
|
||
errors[n] = {"Error 406 (Not Acceptable)": url}
|
||
elif "status of 407" in errors[n]["message"]:
|
||
errors[n] = {"Error 407 (Proxy Auth Required)": url}
|
||
elif "status of 408" in errors[n]["message"]:
|
||
errors[n] = {"Error 408 (Request Timeout)": url}
|
||
elif "status of 409" in errors[n]["message"]:
|
||
errors[n] = {"Error 409 (Conflict)": url}
|
||
elif "status of 410" in errors[n]["message"]:
|
||
errors[n] = {"Error 410 (Gone)": url}
|
||
else:
|
||
errors[n] = {"Failed to load resource": url}
|
||
elif u_c_s_e in errors[n]["message"]:
|
||
url = errors[n]["message"].split(u_c_s_e)[0]
|
||
error = errors[n]["message"].split(u_c_s_e)[1]
|
||
errors[n] = {"Uncaught SyntaxError (%s)" % error: url}
|
||
elif u_c_t_e in errors[n]["message"]:
|
||
url = errors[n]["message"].split(u_c_t_e)[0]
|
||
error = errors[n]["message"].split(u_c_t_e)[1]
|
||
errors[n] = {"Uncaught TypeError (%s)" % error: url}
|
||
er_str = str(errors)
|
||
er_str = er_str.replace("[{", "[\n{").replace("}, {", "},\n{")
|
||
current_url = self.get_current_url()
|
||
self.fail(
|
||
"JavaScript errors found on %s => %s" % (current_url, er_str)
|
||
)
|
||
if self.demo_mode:
|
||
if self.is_chromium():
|
||
a_t = "ASSERT NO JS ERRORS"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_no_js_errors(self._language)
|
||
messenger_post = "<b>%s</b>" % a_t
|
||
self.__highlight_with_assert_success(messenger_post, "html")
|
||
|
||
def __activate_html_inspector(self):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.05)
|
||
js_utils.activate_html_inspector(self.driver)
|
||
|
||
def inspect_html(self):
|
||
"""Inspects the Page HTML with HTML-Inspector.
|
||
(https://github.com/philipwalton/html-inspector)
|
||
(https://cdnjs.com/libraries/html-inspector)
|
||
Prints the results and also returns them."""
|
||
self.__activate_html_inspector()
|
||
self.wait_for_ready_state_complete()
|
||
script = """HTMLInspector.inspect();"""
|
||
try:
|
||
self.execute_script(script)
|
||
except Exception:
|
||
# If unable to load the JavaScript, skip inspection and return.
|
||
msg = "(Unable to load HTML-Inspector JS! Inspection Skipped!)"
|
||
print("\n" + msg)
|
||
return msg
|
||
time.sleep(0.1)
|
||
browser_logs = []
|
||
try:
|
||
browser_logs = self.driver.get_log("browser")
|
||
except (ValueError, WebDriverException):
|
||
# If unable to get browser logs, skip the assert and return.
|
||
msg = "(Unable to Inspect HTML! -> Only works on Chromium!)"
|
||
print("\n" + msg)
|
||
return msg
|
||
messenger_library = "//cdnjs.cloudflare.com/ajax/libs/messenger"
|
||
url = self.get_current_url()
|
||
header = "\n* HTML Inspection Results: %s" % url
|
||
results = [header]
|
||
row_count = 0
|
||
for entry in browser_logs:
|
||
message = entry["message"]
|
||
if "0:6053 " in message:
|
||
message = message.split("0:6053")[1]
|
||
message = message.replace("\\u003C", "<")
|
||
message = message.replace(" and should not be used", "")
|
||
if message.startswith(' "') and message.count('"') == 2:
|
||
message = message.split('"')[1]
|
||
if "but not found in any stylesheet" in message:
|
||
continue
|
||
if not is_windows:
|
||
message = "⚠️ " + message
|
||
else:
|
||
message = "!-> " + message # CMD prompt compatibility
|
||
if messenger_library not in message:
|
||
if message not in results:
|
||
results.append(message)
|
||
row_count += 1
|
||
if row_count > 0:
|
||
results.append("* (See the Console output for details!)")
|
||
else:
|
||
results.append("* (No issues detected!)")
|
||
results = "\n".join(results)
|
||
print(results)
|
||
return results
|
||
|
||
def is_valid_url(self, url):
|
||
"""Return True if the url is a valid url."""
|
||
return page_utils.is_valid_url(url)
|
||
|
||
def is_alert_present(self):
|
||
try:
|
||
self.driver.switch_to.alert
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def is_online(self):
|
||
"""Return True if connected to the Internet."""
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.evaluate("navigator.onLine;")
|
||
return self.execute_script("return navigator.onLine;")
|
||
|
||
def is_connected(self):
|
||
"""
|
||
Return True if WebDriver is connected to the browser.
|
||
Note that the stealthy CDP-Driver isn't a WebDriver.
|
||
In CDP Mode, the CDP-Driver controls the web browser.
|
||
The CDP-Driver can be connected while WebDriver isn't.
|
||
"""
|
||
return self.driver.is_connected()
|
||
|
||
def is_chromium(self):
|
||
"""Return True if the browser is Chrome or Edge."""
|
||
self.__check_scope()
|
||
if self.__is_cdp_swap_needed():
|
||
return True
|
||
chromium = False
|
||
if (
|
||
"chrome" in self.driver.capabilities
|
||
or "msedge" in self.driver.capabilities
|
||
):
|
||
chromium = True
|
||
return chromium
|
||
|
||
def __fail_if_not_using_chrome(self, method):
|
||
chrome = False
|
||
if "chrome" in self.driver.capabilities:
|
||
chrome = True
|
||
if not chrome:
|
||
browser_name = self.driver.capabilities["browserName"]
|
||
message = (
|
||
'Error: "%s" should only be called by tests '
|
||
'running with "--browser=chrome" / "--chrome"! '
|
||
'You should add an "if" statement to your code before calling '
|
||
"this method if using browsers that are Not Chrome! "
|
||
'The browser detected was: "%s".' % (method, browser_name)
|
||
)
|
||
raise NotUsingChromeException(message)
|
||
|
||
def __fail_if_not_using_chromium(self, method):
|
||
if not self.is_chromium():
|
||
browser_name = self.driver.capabilities["browserName"]
|
||
message = (
|
||
'Error: "%s" should only be called by tests '
|
||
'running with a Chromium browser! (Chrome or Edge) '
|
||
'You should add an "if" statement to your code before calling '
|
||
"this method if using browsers that are Not Chromium! "
|
||
'The browser detected was: "%s".' % (method, browser_name)
|
||
)
|
||
raise NotUsingChromiumException(message)
|
||
|
||
def get_chrome_version(self):
|
||
self.__check_scope()
|
||
self.__fail_if_not_using_chrome("get_chrome_version()")
|
||
if "browserVersion" in self.driver.capabilities:
|
||
chrome_version = self.driver.capabilities["browserVersion"]
|
||
else:
|
||
chrome_version = "(Unknown Version)"
|
||
return chrome_version
|
||
|
||
def get_chromium_version(self):
|
||
self.__check_scope()
|
||
self.__fail_if_not_using_chromium("get_chromium_version()")
|
||
return self.__get_major_browser_version()
|
||
|
||
def get_chromedriver_version(self):
|
||
self.__check_scope()
|
||
self.__fail_if_not_using_chrome("get_chromedriver_version()")
|
||
chrome_dict = self.driver.capabilities["chrome"]
|
||
chromedriver_version = chrome_dict["chromedriverVersion"]
|
||
chromedriver_version = chromedriver_version.split(" ")[0]
|
||
return chromedriver_version
|
||
|
||
def get_chromium_driver_version(self):
|
||
self.__check_scope()
|
||
self.__fail_if_not_using_chromium("get_chromium_version()")
|
||
driver_version = None
|
||
if "chrome" in self.driver.capabilities:
|
||
chrome_dict = self.driver.capabilities["chrome"]
|
||
driver_version = chrome_dict["chromedriverVersion"]
|
||
driver_version = driver_version.split(" ")[0]
|
||
elif "msedge" in self.driver.capabilities:
|
||
edge_dict = self.driver.capabilities["msedge"]
|
||
driver_version = edge_dict["msedgedriverVersion"]
|
||
driver_version = driver_version.split(" ")[0]
|
||
return driver_version
|
||
|
||
def get_mfa_code(self, totp_key=None):
|
||
"""Same as get_totp_code() and get_google_auth_password().
|
||
Returns a time-based one-time password based on the
|
||
Google Authenticator algorithm for multi-factor authentication.
|
||
If the "totp_key" is not specified, this method defaults
|
||
to using the one provided in [seleniumbase/config/settings.py].
|
||
Google Authenticator codes expire & change at 30-sec intervals.
|
||
If the fetched password expires in the next 1.2 seconds, waits
|
||
for a new one before returning it (may take up to 1.2 seconds).
|
||
See https://pyotp.readthedocs.io/en/latest/ for details."""
|
||
import pyotp
|
||
|
||
if not totp_key:
|
||
totp_key = settings.TOTP_KEY
|
||
|
||
epoch_interval = time.time() / 30.0
|
||
cycle_lifespan = float(epoch_interval) - int(epoch_interval)
|
||
if float(cycle_lifespan) > 0.96:
|
||
# Password expires in the next 1.2 seconds. Wait for a new one.
|
||
for i in range(30):
|
||
time.sleep(0.04)
|
||
epoch_interval = time.time() / 30.0
|
||
cycle_lifespan = float(epoch_interval) - int(epoch_interval)
|
||
if not float(cycle_lifespan) > 0.96:
|
||
# The new password cycle has begun
|
||
break
|
||
|
||
totp = pyotp.TOTP(totp_key)
|
||
return str(totp.now())
|
||
|
||
def enter_mfa_code(
|
||
self, selector, totp_key=None, by="css selector", timeout=None
|
||
):
|
||
"""Enters into the field a Multi-Factor Authentication TOTP Code.
|
||
If the "totp_key" is not specified, this method defaults
|
||
to using the one provided in [seleniumbase/config/settings.py].
|
||
The TOTP code is generated by the Google Authenticator Algorithm.
|
||
This method will automatically press ENTER after typing the code."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.__is_cdp_swap_needed():
|
||
mfa_code = self.get_mfa_code(totp_key)
|
||
self.cdp.type(selector, mfa_code + "\n", timeout=timeout)
|
||
return
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
sel_key = [css_selector, totp_key]
|
||
origin = self.get_origin()
|
||
action = ["e_mfa", sel_key, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
# Sometimes Sign-In leaves the origin... Save work first.
|
||
self.save_recorded_actions()
|
||
mfa_code = self.get_mfa_code(totp_key)
|
||
self.update_text(selector, mfa_code + "\n", by=by, timeout=timeout)
|
||
|
||
def convert_css_to_xpath(self, css):
|
||
return css_to_xpath.convert_css_to_xpath(css)
|
||
|
||
def convert_xpath_to_css(self, xpath):
|
||
return xpath_to_css.convert_xpath_to_css(xpath)
|
||
|
||
def convert_to_css_selector(self, selector, by):
|
||
"""This method converts a selector to a CSS_SELECTOR.
|
||
jQuery commands require a CSS_SELECTOR for finding elements.
|
||
This method should only be used for jQuery/JavaScript actions.
|
||
Pure JavaScript doesn't support using a:contains("LINK_TEXT")."""
|
||
return js_utils.convert_to_css_selector(selector, by)
|
||
|
||
def set_value(
|
||
self, selector, text, by="css selector", timeout=None, scroll=True
|
||
):
|
||
"""This method uses JavaScript to update a text field."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.set_value(selector, text)
|
||
return
|
||
self.wait_for_ready_state_complete()
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
original_selector = selector
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
self.__demo_mode_highlight_if_active(original_selector, by)
|
||
if scroll and not self.demo_mode and not self.slow_mode:
|
||
self.scroll_to(original_selector, by=by, timeout=timeout)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
if not scroll and not self.demo_mode and not self.slow_mode:
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.06)
|
||
text = self.__get_type_checked_text(text)
|
||
value = re.escape(text)
|
||
value = self.__escape_quotes_if_needed(value)
|
||
pre_escape_css_selector = css_selector
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
the_type = None
|
||
if ":contains\\(" not in css_selector:
|
||
get_type_script = (
|
||
"""return document.querySelector('%s').getAttribute('type');"""
|
||
% css_selector
|
||
)
|
||
the_type = self.execute_script(get_type_script) # Used later
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sel_tex = [pre_escape_css_selector, text]
|
||
if the_type == "range" and ":contains\\(" not in css_selector:
|
||
action = ["s_val", sel_tex, origin, time_stamp]
|
||
else:
|
||
action = ["js_ty", sel_tex, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if ":contains\\(" not in css_selector:
|
||
script = """document.querySelector('%s').value='%s';""" % (
|
||
css_selector,
|
||
value,
|
||
)
|
||
self.execute_script(script)
|
||
else:
|
||
element = self.wait_for_element_present(
|
||
original_selector, by=by, timeout=timeout
|
||
)
|
||
script = """arguments[0].value='%s';""" % value
|
||
self.execute_script(script, element)
|
||
if text.endswith("\n"):
|
||
element = self.wait_for_element_present(
|
||
original_selector, by=by, timeout=timeout
|
||
)
|
||
element.send_keys(Keys.RETURN)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
if the_type == "range" and ":contains\\(" not in css_selector:
|
||
# 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.execute_script(mouse_move_script)
|
||
elif the_type == "range" and ":contains\\(" in css_selector:
|
||
with suppress(Exception):
|
||
element = self.wait_for_element_present(
|
||
original_selector, by=by, timeout=1
|
||
)
|
||
mouse_move_script = (
|
||
"""m_elm = arguments[0];"""
|
||
"""m_evt = new Event('mousemove');"""
|
||
"""m_elm.dispatchEvent(m_evt);"""
|
||
)
|
||
self.execute_script(mouse_move_script, element)
|
||
self.__demo_mode_pause_if_active()
|
||
if not self.demo_mode and not self.slow_mode:
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.04)
|
||
|
||
def js_update_text(self, selector, text, by="css selector", timeout=None):
|
||
"""JavaScript + send_keys are used to update a text field.
|
||
Performs self.set_value() and triggers event listeners.
|
||
If text ends in "\n", set_value() presses RETURN after.
|
||
Works faster than send_keys() alone due to the JS call."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
text = self.__get_type_checked_text(text)
|
||
self.set_value(selector, text, by=by, timeout=timeout)
|
||
if not text.endswith("\n"):
|
||
with suppress(Exception):
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout=0.2
|
||
)
|
||
element.send_keys(" " + Keys.BACK_SPACE)
|
||
|
||
def js_type(self, selector, text, by="css selector", timeout=None):
|
||
"""Same as self.js_update_text()
|
||
JavaScript + send_keys are used to update a text field.
|
||
Performs self.set_value() and triggers event listeners.
|
||
If text ends in "\n", set_value() presses RETURN after.
|
||
Works faster than send_keys() alone due to the JS call."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.js_update_text(selector, text, by=by, timeout=timeout)
|
||
|
||
def set_text(self, selector, text, by="css selector", timeout=None):
|
||
"""Same as self.js_update_text()
|
||
JavaScript + send_keys are used to update a text field.
|
||
Performs self.set_value() and triggers event listeners.
|
||
If text ends in "\n", set_value() presses RETURN after.
|
||
Works faster than send_keys() alone due to the JS call.
|
||
If not an input or textarea, sets textContent instead."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
if element.tag_name.lower() in ["input", "textarea"]:
|
||
self.js_update_text(selector, text, by=by, timeout=timeout)
|
||
else:
|
||
self.set_text_content(selector, text, by=by, timeout=timeout)
|
||
|
||
def set_text_content(
|
||
self, selector, text, by="css selector", timeout=None, scroll=False
|
||
):
|
||
"""This method uses JavaScript to set an element's textContent.
|
||
If the element is an input or textarea, sets the value instead."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
if element.tag_name.lower() in ["input", "textarea"]:
|
||
self.js_update_text(selector, text, by=by, timeout=timeout)
|
||
return
|
||
original_selector = selector
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if scroll:
|
||
self.__demo_mode_highlight_if_active(original_selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.scroll_to(original_selector, by=by, timeout=timeout)
|
||
text = self.__get_type_checked_text(text)
|
||
value = re.escape(text)
|
||
value = self.__escape_quotes_if_needed(value)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
if ":contains\\(" not in css_selector:
|
||
script = """document.querySelector('%s').textContent='%s';""" % (
|
||
css_selector,
|
||
value,
|
||
)
|
||
self.execute_script(script)
|
||
else:
|
||
script = """arguments[0].textContent='%s';""" % value
|
||
self.execute_script(script, element)
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def jquery_update_text(
|
||
self, selector, text, by="css selector", timeout=None
|
||
):
|
||
"""This method uses jQuery to update a text field.
|
||
If the text string ends with the newline character,
|
||
Selenium finishes the call, which simulates pressing
|
||
{Enter/Return} after the text is entered.
|
||
This method also triggers event listeners."""
|
||
self.__check_scope()
|
||
original_selector = selector
|
||
original_by = by
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
self.__demo_mode_highlight_if_active(selector, by)
|
||
self.scroll_to(selector, by=by)
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = selector
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
text = re.escape(text)
|
||
text = self.__escape_quotes_if_needed(text)
|
||
update_text_script = """jQuery('%s').val('%s');""" % (selector, text)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
sel_tex = [css_selector, text]
|
||
action = ["jq_ty", sel_tex, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
self.safe_execute_script(update_text_script)
|
||
if text.endswith("\n"):
|
||
element = self.wait_for_element_present(
|
||
original_selector, by=original_by, timeout=0.2
|
||
)
|
||
element.send_keys(Keys.RETURN)
|
||
else:
|
||
with suppress(Exception):
|
||
element = self.wait_for_element_present(
|
||
original_selector, by=original_by, timeout=0.2
|
||
)
|
||
element.send_keys(" " + Keys.BACK_SPACE)
|
||
self.__demo_mode_pause_if_active()
|
||
|
||
def jquery_type(self, selector, text, by="css selector", timeout=None):
|
||
"""Same as self.jquery_update_text()
|
||
JQuery is used to update a text field.
|
||
Performs jQuery(selector).val(text); and triggers event listeners.
|
||
If text ends in "\n", presses RETURN after."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.jquery_update_text(selector, text, by=by, timeout=timeout)
|
||
|
||
def get_value(self, selector, by="css selector", timeout=None):
|
||
"""This method uses JavaScript to get the value of an input field.
|
||
(Works on both input fields and textarea fields.)"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
original_selector = selector
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
self.__demo_mode_highlight_if_active(original_selector, by)
|
||
if not self.demo_mode and not self.slow_mode:
|
||
self.scroll_to(original_selector, by=by, timeout=timeout)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
if ":contains\\(" not in css_selector:
|
||
script = """return document.querySelector('%s').value;""" % (
|
||
css_selector
|
||
)
|
||
value = self.execute_script(script)
|
||
else:
|
||
element = self.wait_for_element_present(selector, by=by, timeout=1)
|
||
script = """return arguments[0].value;"""
|
||
value = self.execute_script(script, element)
|
||
return value
|
||
|
||
def set_time_limit(self, time_limit):
|
||
self.__check_scope()
|
||
if time_limit:
|
||
try:
|
||
sb_config.time_limit = float(time_limit)
|
||
except Exception:
|
||
sb_config.time_limit = None
|
||
else:
|
||
sb_config.time_limit = None
|
||
if sb_config.time_limit and sb_config.time_limit > 0:
|
||
sb_config.time_limit_ms = int(sb_config.time_limit * 1000.0)
|
||
self.time_limit = sb_config.time_limit
|
||
else:
|
||
self.time_limit = None
|
||
sb_config.time_limit = None
|
||
sb_config.time_limit_ms = None
|
||
|
||
def set_default_timeout(self, timeout):
|
||
"""This method changes the default timeout values of test methods
|
||
for the duration of the current test.
|
||
Effected timeouts: (used by methods that wait for elements)
|
||
* settings.SMALL_TIMEOUT - (default value: 6 seconds)
|
||
* settings.LARGE_TIMEOUT - (default value: 10 seconds)
|
||
The minimum allowable default timeout is: 0.5 seconds.
|
||
The maximum allowable default timeout is: 60.0 seconds.
|
||
(Test methods can still override timeouts outside that range.)"""
|
||
self.__check_scope()
|
||
if not isinstance(timeout, (int, float)):
|
||
raise Exception('Expecting a numeric value for "timeout"!')
|
||
if timeout < 0:
|
||
raise Exception('The "timeout" cannot be a negative number!')
|
||
timeout = float(timeout)
|
||
# Min default timeout: 0.5 seconds. Max default timeout: 60.0 seconds.
|
||
min_timeout = 0.5
|
||
max_timeout = 60.0
|
||
if timeout < min_timeout:
|
||
logging.info("Minimum default timeout = %s" % min_timeout)
|
||
timeout = min_timeout
|
||
elif timeout > max_timeout:
|
||
logging.info("Maximum default timeout = %s" % max_timeout)
|
||
timeout = max_timeout
|
||
self.__overrided_default_timeouts = True
|
||
sb_config._is_timeout_changed = True
|
||
settings.SMALL_TIMEOUT = timeout
|
||
settings.LARGE_TIMEOUT = timeout
|
||
|
||
def reset_default_timeout(self):
|
||
"""Reset default timeout values to the original from settings.py
|
||
This method reverts the changes made by set_default_timeout()"""
|
||
if self.__overrided_default_timeouts:
|
||
if sb_config._SMALL_TIMEOUT and sb_config._LARGE_TIMEOUT:
|
||
settings.SMALL_TIMEOUT = sb_config._SMALL_TIMEOUT
|
||
settings.LARGE_TIMEOUT = sb_config._LARGE_TIMEOUT
|
||
sb_config._is_timeout_changed = False
|
||
self.__overrided_default_timeouts = False
|
||
|
||
def fail(self, msg="fail() called!"):
|
||
"""Fail immediately, with the given message."""
|
||
super().fail(msg)
|
||
|
||
def skip(self, reason=""):
|
||
"""Mark the test as Skipped."""
|
||
self.__check_scope()
|
||
if self.dashboard:
|
||
test_id = self.__get_test_id_2()
|
||
if hasattr(self, "_using_sb_fixture"):
|
||
test_id = sb_config._test_id
|
||
if (
|
||
test_id in sb_config._results.keys()
|
||
and sb_config._results[test_id] == "Passed"
|
||
):
|
||
# Duplicate tearDown() called where test already passed
|
||
self.__passed_then_skipped = True
|
||
self.__will_be_skipped = True
|
||
sb_config._results[test_id] = "Skipped"
|
||
if hasattr(self, "with_db_reporting") and self.with_db_reporting:
|
||
if self.is_pytest:
|
||
self.__skip_reason = reason
|
||
else:
|
||
self._nose_skip_reason = reason
|
||
# Add skip reason to the logs
|
||
if not hasattr(self, "_using_sb_fixture"):
|
||
test_id = self.__get_test_id() # Recalculate the test id
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
browser = self.browser
|
||
if not reason:
|
||
reason = "No skip reason given"
|
||
log_helper.log_skipped_test_data(
|
||
self, test_logpath, self.driver, browser, reason
|
||
)
|
||
self._was_skipped = True
|
||
# Finally skip the test for real
|
||
self.skipTest(reason)
|
||
|
||
############
|
||
|
||
# Console Log controls
|
||
|
||
def start_recording_console_logs(self):
|
||
"""Starts recording console logs. Logs are saved to: "console.logs".
|
||
To get those logs later, call "self.get_recorded_console_logs()".
|
||
If navigating to a new page, then the current recorded logs will be
|
||
lost, and you'll have to call start_recording_console_logs() again.
|
||
# Link1: https://stackoverflow.com/a/19846113/7058266
|
||
# Link2: https://stackoverflow.com/a/74196986/7058266 """
|
||
self.execute_script(
|
||
"""
|
||
console.stdlog = console.log.bind(console);
|
||
console.logs = [];
|
||
console.log = function(){
|
||
console.logs.push(Array.from(arguments));
|
||
console.stdlog.apply(console, arguments);
|
||
}
|
||
"""
|
||
)
|
||
|
||
def console_log_string(self, string):
|
||
"""Log a string to the Web Browser's Console.
|
||
Example:
|
||
self.console_log_string("Hello World!") """
|
||
self.execute_script("""console.log(`%s`);""" % string)
|
||
|
||
def console_log_script(self, script):
|
||
"""Log output of JavaScript to the Web Browser's Console.
|
||
Example:
|
||
self.console_log_script('document.querySelector("h2").textContent') """
|
||
self.execute_script("""console.log(%s);""" % script)
|
||
|
||
def get_recorded_console_logs(self):
|
||
"""Get console logs recorded after "start_recording_console_logs()"."""
|
||
logs = []
|
||
with suppress(Exception):
|
||
logs = self.execute_script("return console.logs;")
|
||
return logs
|
||
|
||
############
|
||
|
||
# Application "Local Storage" controls
|
||
|
||
def __is_valid_storage_url(self):
|
||
url = self.get_current_url()
|
||
if url and len(url) > 0:
|
||
if ("http:") in url or ("https:") in url or ("file:") in url:
|
||
return True
|
||
return False
|
||
|
||
def set_local_storage_item(self, key, value):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Local Storage is not available here!")
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.set_local_storage_item(key, value)
|
||
return
|
||
self.execute_script(
|
||
"window.localStorage.setItem('{}', '{}');".format(key, value)
|
||
)
|
||
|
||
def get_local_storage_item(self, key):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Local Storage is not available here!")
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_local_storage_item(key)
|
||
return self.execute_script(
|
||
"return window.localStorage.getItem('{}');".format(key)
|
||
)
|
||
|
||
def remove_local_storage_item(self, key):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Local Storage is not available here!")
|
||
self.execute_script(
|
||
"window.localStorage.removeItem('{}');".format(key)
|
||
)
|
||
|
||
def clear_local_storage(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
return
|
||
self.execute_script("window.localStorage.clear();")
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["c_l_s", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def get_local_storage_keys(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Local Storage is not available here!")
|
||
return self.execute_script(
|
||
"var ls = window.localStorage, keys = []; "
|
||
"for (var i = 0; i < ls.length; ++i) "
|
||
" keys[i] = ls.key(i); "
|
||
"return keys;"
|
||
)
|
||
|
||
def get_local_storage_items(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Local Storage is not available here!")
|
||
return self.execute_script(
|
||
r"var ls = window.localStorage, items = {}; "
|
||
"for (var i = 0, k; i < ls.length; ++i) "
|
||
" items[k = ls.key(i)] = ls.getItem(k); "
|
||
"return items;"
|
||
)
|
||
|
||
# Application "Session Storage" controls
|
||
|
||
def set_session_storage_item(self, key, value):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Session Storage is not available here!")
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.set_session_storage_item(key, value)
|
||
return
|
||
self.execute_script(
|
||
"window.sessionStorage.setItem('{}', '{}');".format(key, value)
|
||
)
|
||
|
||
def get_session_storage_item(self, key):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Session Storage is not available here!")
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.get_session_storage_item(key)
|
||
return self.execute_script(
|
||
"return window.sessionStorage.getItem('{}');".format(key)
|
||
)
|
||
|
||
def remove_session_storage_item(self, key):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Session Storage is not available here!")
|
||
self.execute_script(
|
||
"window.sessionStorage.removeItem('{}');".format(key)
|
||
)
|
||
|
||
def clear_session_storage(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
return
|
||
if not self.recorder_mode:
|
||
self.execute_script("window.sessionStorage.clear();")
|
||
else:
|
||
recorder_keys = [
|
||
"recorder_mode",
|
||
"recorded_actions",
|
||
"recorder_title",
|
||
"pause_recorder",
|
||
"recorder_activated",
|
||
]
|
||
keys = self.get_session_storage_keys()
|
||
for key in keys:
|
||
if key not in recorder_keys:
|
||
self.remove_session_storage_item(key)
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["c_s_s", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
|
||
def get_session_storage_keys(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Session Storage is not available here!")
|
||
return self.execute_script(
|
||
"var ls = window.sessionStorage, keys = []; "
|
||
"for (var i = 0; i < ls.length; ++i) "
|
||
" keys[i] = ls.key(i); "
|
||
"return keys;"
|
||
)
|
||
|
||
def get_session_storage_items(self):
|
||
self.__check_scope()
|
||
if not self.__is_valid_storage_url():
|
||
raise WebDriverException("Session Storage is not available here!")
|
||
return self.execute_script(
|
||
r"var ls = window.sessionStorage, items = {}; "
|
||
"for (var i = 0, k; i < ls.length; ++i) "
|
||
" items[k = ls.key(i)] = ls.getItem(k); "
|
||
"return items;"
|
||
)
|
||
|
||
############
|
||
|
||
# Methods ONLY for the selenium-wire integration ("--wire")
|
||
|
||
def set_wire_proxy(self, string):
|
||
"""Set a proxy server for selenium-wire mode ("--wire")
|
||
NOTE: This method ONLY works while using "--wire" mode!
|
||
Examples:
|
||
self.set_wire_proxy("SERVER:PORT")
|
||
self.set_wire_proxy("socks5://SERVER:PORT")
|
||
self.set_wire_proxy("USERNAME:PASSWORD@SERVER:PORT") """
|
||
if not string:
|
||
self.driver.proxy = {}
|
||
return
|
||
the_http = "http"
|
||
the_https = "https"
|
||
if string.startswith("socks4://"):
|
||
the_http = "socks4"
|
||
the_https = "socks4"
|
||
elif string.startswith("socks5://"):
|
||
the_http = "socks5"
|
||
the_https = "socks5"
|
||
string = string.split("//")[-1]
|
||
self.driver.proxy = {
|
||
"http": "%s://%s" % (the_http, string),
|
||
"https": "%s://%s" % (the_https, string),
|
||
"no_proxy": "localhost,127.0.0.1",
|
||
}
|
||
|
||
############
|
||
|
||
# Duplicates (Avoids name confusion when migrating from other frameworks.)
|
||
|
||
def open_url(self, url):
|
||
"""Same as self.open()"""
|
||
self.open(url)
|
||
|
||
def visit(self, url):
|
||
"""Same as self.open()"""
|
||
self.open(url)
|
||
|
||
def visit_url(self, url):
|
||
"""Same as self.open()"""
|
||
self.open(url)
|
||
|
||
def goto(self, url):
|
||
"""Same as self.open()"""
|
||
self.open(url)
|
||
|
||
def go_to(self, url):
|
||
"""Same as self.open()"""
|
||
self.open(url)
|
||
|
||
def reload(self):
|
||
"""Same as self.refresh_page()"""
|
||
self.refresh_page()
|
||
|
||
def reload_page(self):
|
||
"""Same as self.refresh_page()"""
|
||
self.refresh_page()
|
||
|
||
def open_new_tab(self, switch_to=True):
|
||
"""Same as self.open_new_window()"""
|
||
self.open_new_window(switch_to=switch_to)
|
||
|
||
def switch_to_tab(self, tab, timeout=None):
|
||
"""Same as self.switch_to_window()
|
||
Switches control of the browser to the specified window.
|
||
The window can be an integer: 0 -> 1st tab, 1 -> 2nd tab, etc...
|
||
Or it can be a list item from self.driver.window_handles """
|
||
self.switch_to_window(window=tab, timeout=timeout)
|
||
|
||
def switch_to_default_tab(self):
|
||
"""Same as self.switch_to_default_window()"""
|
||
self.switch_to_default_window()
|
||
|
||
def switch_to_newest_tab(self):
|
||
"""Same as self.switch_to_newest_window()"""
|
||
self.switch_to_newest_window()
|
||
|
||
def input(
|
||
self, selector, text, by="css selector", timeout=None, retry=False
|
||
):
|
||
"""Same as self.update_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.update_text(selector, text, by=by, timeout=timeout, retry=retry)
|
||
|
||
def fill(
|
||
self, selector, text, by="css selector", timeout=None, retry=False
|
||
):
|
||
"""Same as self.update_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.update_text(selector, text, by=by, timeout=timeout, retry=retry)
|
||
|
||
def write(
|
||
self, selector, text, by="css selector", timeout=None, retry=False
|
||
):
|
||
"""Same as self.update_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.update_text(selector, text, by=by, timeout=timeout, retry=retry)
|
||
|
||
def click_link(self, link_text, timeout=None):
|
||
"""Same as self.click_link_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.click_link_text(link_text, timeout=timeout)
|
||
|
||
def click_partial_link(self, partial_link_text, timeout=None):
|
||
"""Same as self.click_partial_link_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.click_partial_link_text(partial_link_text, timeout=timeout)
|
||
|
||
def right_click(self, selector, by="css selector", timeout=None):
|
||
"""Same as self.context_click()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.context_click(selector, by=by, timeout=timeout)
|
||
|
||
def hover_on_element(self, selector, by="css selector", timeout=None):
|
||
"""Same as self.hover()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.hover(selector, by=by, timeout=timeout)
|
||
|
||
def hover_over_element(self, selector, by="css selector", timeout=None):
|
||
"""Same as self.hover()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.hover(selector, by=by, timeout=timeout)
|
||
|
||
def wait_for_element_visible(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.wait_for_element()"""
|
||
self.__check_scope()
|
||
self.__skip_if_esc()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.select(selector, timeout=timeout)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__get_shadow_element(selector, timeout)
|
||
return page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def wait_for_element_clickable(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Waits for the element to be clickable, but does NOT click it."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.select(selector, timeout=timeout)
|
||
elif self.__is_shadow_selector(selector):
|
||
# If a shadow selector, use visible instead of clickable
|
||
return self.__wait_for_shadow_element_visible(selector, timeout)
|
||
return page_actions.wait_for_element_clickable(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def wait_for_element_not_present(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.wait_for_element_absent()
|
||
Waits for an element to no longer appear in the HTML of a page.
|
||
A hidden element still counts as appearing in the page HTML.
|
||
If waiting for elements to be hidden instead of nonexistent,
|
||
use wait_for_element_not_visible() instead."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.wait_for_element_absent(selector, timeout=timeout)
|
||
return True
|
||
return page_actions.wait_for_element_absent(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def select_all(self, selector, by="css selector", limit=0):
|
||
return self.find_elements(selector, by=by, limit=limit)
|
||
|
||
def assert_link(self, link_text, timeout=None):
|
||
"""Same as self.assert_link_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.assert_link_text(link_text, timeout=timeout)
|
||
|
||
def assert_element_not_present(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.assert_element_absent()
|
||
Raises an exception if the element stays present.
|
||
A hidden element counts as a present element, which fails this assert.
|
||
If you want to assert that elements are hidden instead of nonexistent,
|
||
use assert_element_not_visible() instead.
|
||
(Note that hidden elements are still present in the HTML of the page.)
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.wait_for_element_absent(selector, by=by, timeout=timeout)
|
||
return True
|
||
|
||
def get_google_auth_password(self, totp_key=None):
|
||
"""Same as self.get_mfa_code()"""
|
||
return self.get_mfa_code(totp_key=totp_key)
|
||
|
||
def get_google_auth_code(self, totp_key=None):
|
||
"""Same as self.get_mfa_code()"""
|
||
return self.get_mfa_code(totp_key=totp_key)
|
||
|
||
def get_totp_code(self, totp_key=None):
|
||
"""Same as self.get_mfa_code()"""
|
||
return self.get_mfa_code(totp_key=totp_key)
|
||
|
||
def enter_totp_code(
|
||
self, selector, totp_key=None, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.enter_mfa_code()"""
|
||
return self.enter_mfa_code(
|
||
selector=selector, totp_key=totp_key, by=by, timeout=timeout
|
||
)
|
||
|
||
def clear_all_cookies(self):
|
||
"""Same as self.delete_all_cookies()"""
|
||
self.delete_all_cookies()
|
||
|
||
def delete_local_storage(self):
|
||
"""Same as self.clear_local_storage()"""
|
||
self.clear_local_storage()
|
||
|
||
def delete_session_storage(self):
|
||
"""Same as clear_session_storage()"""
|
||
self.clear_session_storage()
|
||
|
||
def assert_no_broken_links(self, multithreaded=True):
|
||
"""Same as self.assert_no_404_errors()"""
|
||
self.assert_no_404_errors(multithreaded=multithreaded)
|
||
|
||
def wait(self, seconds):
|
||
"""Same as self.sleep() - Some JS frameworks use this method name."""
|
||
self.sleep(seconds)
|
||
|
||
def block_ads(self):
|
||
"""Same as self.ad_block()"""
|
||
self.ad_block()
|
||
|
||
def _check_browser(self):
|
||
"""This method raises an exception if the active window is closed.
|
||
(This provides a much cleaner exception message in this situation.)"""
|
||
page_actions._reconnect_if_disconnected(self.driver)
|
||
active_window = None
|
||
with suppress(Exception):
|
||
active_window = self.driver.current_window_handle # Fails if None
|
||
if not active_window:
|
||
raise NoSuchWindowException("Active window was already closed!")
|
||
|
||
def _print(self, msg):
|
||
"""Same as Python's print(), but also prints during multithreaded runs.
|
||
Normally, Python's print() command won't print for multithreaded tests.
|
||
Here's an example of running tests using multithreading: "pytest -n=4".
|
||
Here's how to print directly from sys without using a print() command:
|
||
To force a print during multithreaded tests, use: "sys.stderr.write()".
|
||
To print without the new-line character end, use: "sys.stdout.write()".
|
||
"""
|
||
if hasattr(sb_config, "_multithreaded") and sb_config._multithreaded:
|
||
if not isinstance(msg, str):
|
||
with suppress(Exception):
|
||
msg = str(msg)
|
||
sys.stderr.write(msg + "\n")
|
||
else:
|
||
print(msg)
|
||
|
||
############
|
||
|
||
def add_css_link(self, css_link):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_css_link(self.driver, css_link)
|
||
|
||
def add_js_link(self, js_link):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_js_link(self.driver, js_link)
|
||
|
||
def add_css_style(self, css_style):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_css_style(self.driver, css_style)
|
||
|
||
def add_js_code_from_link(self, js_link):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_js_code_from_link(self.driver, js_link)
|
||
|
||
def add_js_code(self, js_code):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_js_code(self.driver, js_code)
|
||
|
||
def add_meta_tag(self, http_equiv=None, content=None):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.add_meta_tag(
|
||
self.driver, http_equiv=http_equiv, content=content
|
||
)
|
||
|
||
############
|
||
|
||
def activate_messenger(self):
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.activate_messenger(self.driver)
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def set_messenger_theme(
|
||
self, theme="default", location="default", max_messages="default"
|
||
):
|
||
"""Sets a theme for posting messages.
|
||
Themes: ["flat", "future", "block", "air", "ice"]
|
||
Locations: ["top_left", "top_center", "top_right",
|
||
"bottom_left", "bottom_center", "bottom_right"]
|
||
max_messages: The limit of concurrent messages to display."""
|
||
self.__check_scope()
|
||
if not self.__is_cdp_swap_needed():
|
||
self._check_browser()
|
||
if not theme:
|
||
theme = "default" # "flat"
|
||
if not location:
|
||
location = "default" # "bottom_right"
|
||
if not max_messages:
|
||
max_messages = "default" # "8"
|
||
else:
|
||
max_messages = str(max_messages) # Value must be in string format
|
||
js_utils.set_messenger_theme(
|
||
self.driver,
|
||
theme=theme,
|
||
location=location,
|
||
max_messages=max_messages,
|
||
)
|
||
|
||
def post_message(self, message, duration=None, pause=True, style="info"):
|
||
"""Post a message on the screen with Messenger.
|
||
Arguments:
|
||
message: The message to display.
|
||
duration: The time until the message vanishes. (Default: 2.55s)
|
||
pause: If True, the program waits until the message completes.
|
||
style: "info", "success", or "error".
|
||
|
||
You can also post messages by using =>
|
||
self.execute_script('Messenger().post("My Message")') """
|
||
self.__check_scope()
|
||
if not self.__is_cdp_swap_needed():
|
||
self._check_browser()
|
||
if style not in ["info", "success", "error"]:
|
||
style = "info"
|
||
if not duration:
|
||
if not self.message_duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
else:
|
||
duration = self.message_duration
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
try:
|
||
js_utils.post_message(self.driver, message, duration, style=style)
|
||
except Exception:
|
||
print(" * %s message: %s" % (style.upper(), message))
|
||
if pause:
|
||
duration = float(duration) + 0.15
|
||
time.sleep(float(duration))
|
||
|
||
def post_message_and_highlight(self, message, selector, by="css selector"):
|
||
"""Post a message on the screen and highlight an element.
|
||
Arguments:
|
||
message: The message to display.
|
||
selector: The selector of the Element to highlight.
|
||
by: The type of selector to search by. (Default: "css selector")"""
|
||
self.__check_scope()
|
||
self.__highlight_with_assert_success(message, selector, by=by)
|
||
|
||
def post_success_message(self, message, duration=None, pause=True):
|
||
"""Post a success message on the screen with Messenger.
|
||
Arguments:
|
||
message: The success message to display.
|
||
duration: The time until the message vanishes. (Default: 2.55s)
|
||
pause: If True, the program waits until the message completes."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
if not duration:
|
||
if not self.message_duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
else:
|
||
duration = self.message_duration
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
try:
|
||
js_utils.post_message(
|
||
self.driver, message, duration, style="success"
|
||
)
|
||
except Exception:
|
||
print(" * SUCCESS message: %s" % message)
|
||
if pause:
|
||
duration = float(duration) + 0.15
|
||
time.sleep(float(duration))
|
||
|
||
def post_error_message(self, message, duration=None, pause=True):
|
||
"""Post an error message on the screen with Messenger.
|
||
Arguments:
|
||
message: The error message to display.
|
||
duration: The time until the message vanishes. (Default: 2.55s)
|
||
pause: If True, the program waits until the message completes."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
if not duration:
|
||
if not self.message_duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
else:
|
||
duration = self.message_duration
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
try:
|
||
js_utils.post_message(
|
||
self.driver, message, duration, style="error"
|
||
)
|
||
except Exception:
|
||
print(" * ERROR message: %s" % message)
|
||
if pause:
|
||
duration = float(duration) + 0.15
|
||
time.sleep(float(duration))
|
||
|
||
############
|
||
|
||
def generate_referral(self, start_page, destination_page, selector=None):
|
||
"""This method opens the start_page, creates a referral link there,
|
||
and clicks on that link, which goes to the destination_page.
|
||
If a selector is given, clicks that on the destination_page,
|
||
which can prevent an artificial rise in website bounce-rate.
|
||
(This generates real traffic for testing analytics software.)"""
|
||
self.__check_scope()
|
||
if not page_utils.is_valid_url(destination_page):
|
||
raise Exception(
|
||
"Exception: destination_page {%s} is not a valid URL!"
|
||
% destination_page
|
||
)
|
||
if start_page:
|
||
if not page_utils.is_valid_url(start_page):
|
||
raise Exception(
|
||
"Exception: start_page {%s} is not a valid URL! "
|
||
"(Use an empty string or None to start from current page.)"
|
||
% start_page
|
||
)
|
||
self.open(start_page)
|
||
time.sleep(0.08)
|
||
self.wait_for_ready_state_complete()
|
||
referral_link = (
|
||
"""<body>"""
|
||
"""<a class='analytics referral test' href='%s' """
|
||
"""style='font-family: Arial,sans-serif; """
|
||
"""font-size: 30px; color: #18a2cd'>"""
|
||
"""Magic Link Button</a></body>""" % destination_page
|
||
)
|
||
self.execute_script(
|
||
'''document.body.outerHTML = \"%s\"''' % referral_link
|
||
)
|
||
# Now click the generated button
|
||
self.click("a.analytics.referral.test", timeout=2)
|
||
time.sleep(0.15)
|
||
if selector:
|
||
self.click(selector)
|
||
time.sleep(0.15)
|
||
|
||
def generate_traffic(
|
||
self, start_page, destination_page, loops=1, selector=None
|
||
):
|
||
"""Similar to generate_referral(), but can do multiple loops.
|
||
If a selector is given, clicks that on the destination_page,
|
||
which can prevent an artificial rise in website bounce-rate."""
|
||
self.__check_scope()
|
||
for loop in range(loops):
|
||
self.generate_referral(
|
||
start_page, destination_page, selector=selector
|
||
)
|
||
time.sleep(0.05)
|
||
|
||
def generate_referral_chain(self, pages):
|
||
"""Use this method to chain the action of creating button links on
|
||
one website page that will take you to the next page.
|
||
(When you want to create a referral to a website for traffic
|
||
generation without increasing the bounce rate, you'll want to visit
|
||
at least one additional page on that site with a button click.)"""
|
||
self.__check_scope()
|
||
if not isinstance(pages, (list, tuple)):
|
||
raise Exception(
|
||
"Exception: Expecting a list of website pages for chaining!"
|
||
)
|
||
if len(pages) < 2:
|
||
raise Exception(
|
||
"Exception: At least two website pages required for chaining!"
|
||
)
|
||
for page in pages:
|
||
if not page_utils.is_valid_url(page):
|
||
raise Exception(
|
||
"Exception: Website page {%s} is not a valid URL!" % page
|
||
)
|
||
for page in pages:
|
||
self.generate_referral(None, page)
|
||
|
||
def generate_traffic_chain(self, pages, loops=1):
|
||
"""Similar to generate_referral_chain(), but for multiple loops."""
|
||
self.__check_scope()
|
||
for loop in range(loops):
|
||
self.generate_referral_chain(pages)
|
||
time.sleep(0.05)
|
||
|
||
############
|
||
|
||
def wait_for_element_present(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Waits for an element to appear in the HTML of a page.
|
||
The element does not need be visible (it may be hidden)."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.select(selector, timeout=timeout)
|
||
elif self.__is_shadow_selector(selector):
|
||
return self.__wait_for_shadow_element_present(selector, timeout)
|
||
return page_actions.wait_for_element_present(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def wait_for_element(self, selector, by="css selector", timeout=None):
|
||
"""Waits for an element to appear in the HTML of a page.
|
||
The element must be visible (it cannot be hidden)."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.select(selector, timeout=timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["wf_el", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__get_shadow_element(selector, timeout)
|
||
return page_actions.wait_for_element_visible(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
|
||
def get_element(self, selector, by="css selector", timeout=None):
|
||
"""Same as wait_for_element_present() - returns the element.
|
||
The element does not need be visible (it may be hidden)."""
|
||
return self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
|
||
def locator(self, selector, by="css selector", timeout=None):
|
||
"""Same as wait_for_element_present() - returns the element.
|
||
The element does not need be visible (it may be hidden)."""
|
||
return self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
|
||
def wait_for_selector(self, selector, by="css selector", timeout=None):
|
||
"""Same as wait_for_element_present() - returns the element.
|
||
The element does not need be visible (it may be hidden)."""
|
||
return self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
|
||
def wait_for_query_selector(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Waits for an element to appear in the HTML of a page.
|
||
The element does not need be visible (it may be hidden).
|
||
This method uses document.querySelector() over Selenium."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.select(css_selector, timeout=timeout)
|
||
return js_utils.wait_for_css_query_selector(
|
||
self.driver, css_selector, timeout
|
||
)
|
||
|
||
def assert_element_present(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_element_present(), but returns nothing.
|
||
Waits for an element to appear in the HTML of a page.
|
||
The element does not need be visible (it may be hidden).
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if isinstance(selector, list):
|
||
self.assert_elements_present(selector, by=by, timeout=timeout)
|
||
return True
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element_present(selector, timeout=timeout)
|
||
return True
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_shadow_element_present(selector)
|
||
return True
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_ep", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_elements_present(self, *args, **kwargs):
|
||
"""Similar to self.assert_element_present(),
|
||
but can assert that multiple elements are present in the HTML.
|
||
The input is a list of elements.
|
||
Optional kwargs include "by" and "timeout" (used by all selectors).
|
||
Raises an exception if any of the elements are not visible.
|
||
Examples:
|
||
self.assert_elements_present("head", "style", "script", "body")
|
||
OR
|
||
self.assert_elements_present(["head", "body", "h1", "h2"]) """
|
||
self.__check_scope()
|
||
selectors = []
|
||
timeout = None
|
||
by = By.CSS_SELECTOR
|
||
for kwarg in kwargs:
|
||
if kwarg == "timeout":
|
||
timeout = kwargs["timeout"]
|
||
elif kwarg == "by":
|
||
by = kwargs["by"]
|
||
elif kwarg == "selector":
|
||
selector = kwargs["selector"]
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
elif isinstance(selector, list):
|
||
selectors_list = selector
|
||
for selector in selectors_list:
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
else:
|
||
raise Exception('Unknown kwarg: "%s"!' % kwarg)
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
for arg in args:
|
||
if isinstance(arg, list):
|
||
for selector in arg:
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
elif isinstance(arg, str):
|
||
selectors.append(arg)
|
||
for selector in selectors:
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_shadow_element_visible(selector)
|
||
continue
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
continue
|
||
return True
|
||
|
||
def find_element(self, selector, by="css selector", timeout=None):
|
||
"""Same as wait_for_element_visible() - returns the element"""
|
||
return self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
|
||
def assert_element(self, selector, by="css selector", timeout=None):
|
||
"""Similar to wait_for_element_visible(), but returns nothing.
|
||
As above, raises an exception if nothing can be found.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element(selector, timeout=timeout)
|
||
return True
|
||
if isinstance(selector, list):
|
||
self.assert_elements(selector, by=by, timeout=timeout)
|
||
return True
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_shadow_element_visible(selector)
|
||
return True
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
original_selector = selector
|
||
if self.demo_mode:
|
||
selector, by = self.__recalculate_selector(
|
||
selector, by, xp_ok=False
|
||
)
|
||
a_t = "ASSERT"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert(self._language)
|
||
messenger_post = "<b>%s %s</b>: %s" % (a_t, by.upper(), selector)
|
||
self.__highlight_with_assert_success(messenger_post, selector, by)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_el", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_element_visible(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Same as self.assert_element()
|
||
As above, raises an exception if nothing can be found."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.assert_element(selector, by=by, timeout=timeout)
|
||
return True
|
||
|
||
def assert_elements(self, *args, **kwargs):
|
||
"""Similar to self.assert_element(), but can assert multiple elements.
|
||
The input is a list of elements.
|
||
Optional kwargs include "by" and "timeout" (used by all selectors).
|
||
Raises an exception if any of the elements are not visible.
|
||
Examples:
|
||
self.assert_elements("h1", "h2", "h3")
|
||
OR
|
||
self.assert_elements(["h1", "h2", "h3"]) """
|
||
self.__check_scope()
|
||
selectors = []
|
||
timeout = None
|
||
by = By.CSS_SELECTOR
|
||
for kwarg in kwargs:
|
||
if kwarg == "timeout":
|
||
timeout = kwargs["timeout"]
|
||
elif kwarg == "by":
|
||
by = kwargs["by"]
|
||
elif kwarg == "selector":
|
||
selector = kwargs["selector"]
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
elif isinstance(selector, list):
|
||
selectors_list = selector
|
||
for selector in selectors_list:
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
else:
|
||
raise Exception('Unknown kwarg: "%s"!' % kwarg)
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
for arg in args:
|
||
if isinstance(arg, list):
|
||
for selector in arg:
|
||
if isinstance(selector, str):
|
||
selectors.append(selector)
|
||
elif isinstance(arg, str):
|
||
selectors.append(arg)
|
||
for selector in selectors:
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_shadow_element_visible(selector)
|
||
continue
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
if self.demo_mode:
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
a_t = "ASSERT"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert(self._language)
|
||
messenger_post = "<b>%s %s</b>: %s" % (
|
||
a_t, by.upper(), selector
|
||
)
|
||
self.__highlight_with_assert_success(
|
||
messenger_post, selector, by
|
||
)
|
||
continue
|
||
return True
|
||
|
||
def assert_elements_visible(self, *args, **kwargs):
|
||
"""Same as self.assert_elements()
|
||
Raises an exception if any element cannot be found."""
|
||
return self.assert_elements(*args, **kwargs)
|
||
|
||
############
|
||
|
||
def wait_for_text_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
text = self.__get_type_checked_text(text)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.find_element(selector, timeout=timeout)
|
||
elif self.__is_shadow_selector(selector):
|
||
return self.__wait_for_shadow_text_visible(text, selector, timeout)
|
||
return page_actions.wait_for_text_visible(
|
||
self.driver, text, selector, by, timeout
|
||
)
|
||
|
||
def wait_for_exact_text_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__wait_for_exact_shadow_text_visible(
|
||
text, selector, timeout
|
||
)
|
||
return page_actions.wait_for_exact_text_visible(
|
||
self.driver, text, selector, by, timeout
|
||
)
|
||
|
||
def wait_for_non_empty_text_visible(
|
||
self, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Searches for any text in the element of the given selector.
|
||
Returns the element if it has visible text within the timeout.
|
||
Raises an exception if the element has no text within the timeout.
|
||
Whitespace-only text is considered empty text."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
return self.__wait_for_non_empty_shadow_text_visible(
|
||
selector, timeout
|
||
)
|
||
return page_actions.wait_for_non_empty_text_visible(
|
||
self.driver, selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def wait_for_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""The shorter version of wait_for_text_visible()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def wait_for_exact_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""The shorter version of wait_for_exact_text_visible()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_exact_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def wait_for_non_empty_text(
|
||
self, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""The shorter version of wait_for_non_empty_text_visible()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_non_empty_text_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def find_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Same as wait_for_text_visible() - returns the element"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def find_exact_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Same as wait_for_exact_text_visible() - returns the element"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_exact_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def find_non_empty_text(
|
||
self, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Same as wait_for_non_empty_text_visible() - returns the element"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_non_empty_text_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
|
||
def assert_text_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Same as assert_text()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.assert_text(text, selector, by=by, timeout=timeout)
|
||
|
||
def assert_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_text_visible()
|
||
Raises an exception if the element or the text is not found.
|
||
The text only needs to be a subset within the complete text.
|
||
The text can be a string or a list/tuple of text substrings.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if isinstance(text, (list, tuple)):
|
||
text_list = text
|
||
for _text in text_list:
|
||
self.wait_for_text_visible(
|
||
_text, selector, by=by, timeout=timeout
|
||
)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT TEXT"
|
||
i_n = "in"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b>: {%s} %s %s: %s" % (
|
||
a_t, _text, i_n, by.upper(), selector
|
||
)
|
||
self.__highlight_with_assert_success(
|
||
messenger_post, selector, by
|
||
)
|
||
elif self.__is_cdp_swap_needed():
|
||
self.cdp.assert_text(text, selector, timeout=timeout)
|
||
return True
|
||
elif not self.is_connected():
|
||
self.connect()
|
||
elif self.__is_shadow_selector(selector):
|
||
self.__assert_shadow_text_visible(text, selector, timeout)
|
||
return True
|
||
else:
|
||
self.wait_for_text_visible(text, selector, by=by, timeout=timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT TEXT"
|
||
i_n = "in"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b>: {%s} %s %s: %s" % (
|
||
a_t, text, i_n, by.upper(), selector
|
||
)
|
||
self.__highlight_with_assert_success(
|
||
messenger_post, selector, by
|
||
)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["as_te", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_exact_text(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Similar to assert_text(), but the text must be exact,
|
||
rather than exist as a subset of the full text.
|
||
(Extra whitespace at the beginning or the end doesn't count.)
|
||
Raises an exception if the element or the text is not found.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_exact_text(text, selector, timeout=timeout)
|
||
return True
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_exact_shadow_text_visible(text, selector, timeout)
|
||
return True
|
||
self.wait_for_exact_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT EXACT TEXT"
|
||
i_n = "in"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_exact_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b>: {%s} %s %s: %s" % (
|
||
a_t, text, i_n, by.upper(), selector
|
||
)
|
||
self.__highlight_with_assert_success(messenger_post, selector, by)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["as_et", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_non_empty_text(
|
||
self, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Assert that the element has any non-empty text visible.
|
||
Raises an exception if the element has no text within the timeout.
|
||
Whitespace-only text is considered empty text."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_shadow_selector(selector):
|
||
self.__assert_non_empty_shadow_text_visible(selector, timeout)
|
||
return True
|
||
self.wait_for_non_empty_text_visible(selector, by=by, timeout=timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT NON-EMPTY TEXT"
|
||
i_n = "in"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_non_empty_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b> %s %s: %s" % (
|
||
a_t, i_n, by.upper(), selector
|
||
)
|
||
self.__highlight_with_assert_success(messenger_post, selector, by)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
if by == By.XPATH:
|
||
selector = original_selector
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["asnet", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
############
|
||
|
||
def wait_for_link_text_present(self, link_text, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (timeout * 1000.0)
|
||
for x in range(int(timeout * 5)):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
try:
|
||
if not self.is_link_text_present(link_text):
|
||
raise Exception(
|
||
"Link text {%s} was not found!" % link_text
|
||
)
|
||
return
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.2)
|
||
message = "Link text {%s} was not found after %s seconds!" % (
|
||
link_text,
|
||
timeout,
|
||
)
|
||
page_actions.timeout_exception("LinkTextNotFoundException", message)
|
||
|
||
def wait_for_partial_link_text_present(self, link_text, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (timeout * 1000.0)
|
||
for x in range(int(timeout * 5)):
|
||
shared_utils.check_if_time_limit_exceeded()
|
||
try:
|
||
if not self.is_partial_link_text_present(link_text):
|
||
raise Exception(
|
||
"Partial Link text {%s} was not found!" % link_text
|
||
)
|
||
return
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.2)
|
||
message = (
|
||
"Partial Link text {%s} was not found after %s seconds!"
|
||
"" % (link_text, timeout)
|
||
)
|
||
page_actions.timeout_exception("LinkTextNotFoundException", message)
|
||
|
||
def wait_for_link_text_visible(self, link_text, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_element_visible(
|
||
link_text, by="link text", timeout=timeout
|
||
)
|
||
|
||
def wait_for_link_text(self, link_text, timeout=None):
|
||
"""The shorter version of wait_for_link_text_visible()"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_link_text_visible(link_text, timeout=timeout)
|
||
|
||
def find_link_text(self, link_text, timeout=None):
|
||
"""Same as wait_for_link_text_visible() - returns the element"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_link_text_visible(link_text, timeout=timeout)
|
||
|
||
def assert_link_text(self, link_text, timeout=None):
|
||
"""Similar to wait_for_link_text_visible(), but returns nothing.
|
||
As above, raises an exception if nothing can be found.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.find_element(link_text, timeout=timeout)
|
||
return
|
||
self.wait_for_link_text_visible(link_text, timeout=timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT LINK TEXT"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_link_text(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_t, link_text)
|
||
self.__highlight_with_assert_success(
|
||
messenger_post, link_text, by="link text"
|
||
)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["as_lt", link_text, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def wait_for_partial_link_text(self, partial_link_text, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_element_visible(
|
||
partial_link_text, by="partial link text", timeout=timeout
|
||
)
|
||
|
||
def find_partial_link_text(self, partial_link_text, timeout=None):
|
||
"""Same as wait_for_partial_link_text() - returns the element."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_partial_link_text(
|
||
partial_link_text, timeout=timeout
|
||
)
|
||
|
||
def assert_partial_link_text(self, partial_link_text, timeout=None):
|
||
"""Similar to wait_for_partial_link_text(), but returns nothing.
|
||
As above, raises an exception if nothing can be found.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.wait_for_partial_link_text(partial_link_text, timeout=timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT PARTIAL LINK TEXT"
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_link_text(self._language)
|
||
messenger_post = "<b>%s</b>: {%s}" % (a_t, partial_link_text)
|
||
self.__highlight_with_assert_success(
|
||
messenger_post, partial_link_text, by="partial link text"
|
||
)
|
||
return True
|
||
|
||
############
|
||
|
||
def wait_for_element_absent(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Waits for an element to no longer appear in the HTML of a page.
|
||
A hidden element counts as a present element, which fails this assert.
|
||
If waiting for elements to be hidden instead of nonexistent,
|
||
use wait_for_element_not_visible() instead."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.wait_for_element_absent(selector, timeout=timeout)
|
||
return True
|
||
return page_actions.wait_for_element_absent(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def assert_element_absent(self, selector, by="css selector", timeout=None):
|
||
"""Similar to wait_for_element_absent()
|
||
As above, raises an exception if the element stays present.
|
||
A hidden element counts as a present element, which fails this assert.
|
||
If you want to assert that elements are hidden instead of nonexistent,
|
||
use assert_element_not_visible() instead.
|
||
(Note that hidden elements are still present in the HTML of the page.)
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element_absent(selector, timeout=timeout)
|
||
return True
|
||
self.wait_for_element_absent(selector, by=by, timeout=timeout)
|
||
return True
|
||
|
||
############
|
||
|
||
def wait_for_element_not_visible(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Waits for an element to no longer be visible on a page.
|
||
The element can be non-existent in the HTML or hidden on the page
|
||
to qualify as not visible."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
original_selector = selector
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.wait_for_element_not_visible(selector, timeout=timeout)
|
||
return True
|
||
return page_actions.wait_for_element_not_visible(
|
||
self.driver,
|
||
selector,
|
||
by,
|
||
timeout=timeout,
|
||
original_selector=original_selector,
|
||
)
|
||
|
||
def assert_element_not_visible(
|
||
self, selector, by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_element_not_visible()
|
||
As above, raises an exception if the element stays visible.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
if self.__is_cdp_swap_needed():
|
||
self.cdp.assert_element_not_visible(selector, timeout=timeout)
|
||
return True
|
||
self.wait_for_element_not_visible(selector, by=by, timeout=timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["asenv", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
############
|
||
|
||
def wait_for_text_not_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
if self.__is_cdp_swap_needed():
|
||
return self.cdp.wait_for_text_not_visible(
|
||
text, selector=selector, timeout=timeout
|
||
)
|
||
return page_actions.wait_for_text_not_visible(
|
||
self.driver, text, selector, by, timeout
|
||
)
|
||
|
||
def wait_for_exact_text_not_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
return page_actions.wait_for_exact_text_not_visible(
|
||
self.driver, text, selector, by, timeout
|
||
)
|
||
|
||
def assert_text_not_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_text_not_visible()
|
||
Raises an exception if the text is still visible after timeout.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.wait_for_text_not_visible(text, selector, by=by, timeout=timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["astnv", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
def assert_exact_text_not_visible(
|
||
self, text, selector="body", by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_exact_text_not_visible()
|
||
Raises an exception if the exact text is still visible after timeout.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.wait_for_exact_text_not_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["aetnv", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return True
|
||
|
||
############
|
||
|
||
def wait_for_attribute_not_present(
|
||
self, selector, attribute, value=None, by="css selector", timeout=None
|
||
):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
return page_actions.wait_for_attribute_not_present(
|
||
self.driver, selector, attribute, value, by, timeout
|
||
)
|
||
|
||
def assert_attribute_not_present(
|
||
self, selector, attribute, value=None, by="css selector", timeout=None
|
||
):
|
||
"""Similar to wait_for_attribute_not_present()
|
||
Raises an exception if the attribute is still present after timeout.
|
||
Returns True if successful. Default timeout = SMALL_TIMEOUT."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return self.wait_for_attribute_not_present(
|
||
selector, attribute, value=value, by=by, timeout=timeout
|
||
)
|
||
|
||
############
|
||
|
||
def wait_for_and_accept_alert(self, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
alert = page_actions.wait_for_and_accept_alert(self.driver, timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["acc_a", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return alert
|
||
|
||
def wait_for_and_dismiss_alert(self, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
alert = page_actions.wait_for_and_dismiss_alert(self.driver, timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["dis_a", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return alert
|
||
|
||
def wait_for_and_switch_to_alert(self, timeout=None):
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.LARGE_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return page_actions.wait_for_and_switch_to_alert(self.driver, timeout)
|
||
|
||
############
|
||
|
||
def accept_alert(self, timeout=None):
|
||
"""Same as wait_for_and_accept_alert(), but smaller default T_O"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
alert = page_actions.wait_for_and_accept_alert(self.driver, timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["acc_a", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return alert
|
||
|
||
def dismiss_alert(self, timeout=None):
|
||
"""Same as wait_for_and_dismiss_alert(), but smaller default T_O"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
alert = page_actions.wait_for_and_dismiss_alert(self.driver, timeout)
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["dis_a", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
return alert
|
||
|
||
def switch_to_alert(self, timeout=None):
|
||
"""Same as wait_for_and_switch_to_alert(), but smaller default T_O"""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
return page_actions.wait_for_and_switch_to_alert(self.driver, timeout)
|
||
|
||
############
|
||
|
||
def quit_extra_driver(self, driver=None):
|
||
"""Quits the driver only if it's not the default/initial driver.
|
||
If a driver is given, quits that, otherwise quits the active driver.
|
||
Raises an Exception if quitting the default/initial driver.
|
||
Should only be called if a test has already called get_new_driver().
|
||
Afterwards, self.driver points to the default/initial driver
|
||
if self.driver was the one being quit.
|
||
----
|
||
If a test never calls get_new_driver(), this method isn't needed.
|
||
SeleniumBase automatically quits browsers after tests have ended.
|
||
Even if tests do call get_new_driver(), you don't need to use this
|
||
method unless you want to quit extra browsers before a test ends.
|
||
----
|
||
Terminology and important details:
|
||
* Active driver: The one self.driver is set to. Used within methods.
|
||
* Default/initial driver: The one that is spun up when tests start.
|
||
Initially, the active driver and the default driver are the same.
|
||
The active driver can change when one of these methods is called:
|
||
> self.get_new_driver()
|
||
> self.switch_to_default_driver()
|
||
> self.switch_to_driver()
|
||
> self.quit_extra_driver() """
|
||
self.__check_scope()
|
||
if not driver:
|
||
driver = self.driver
|
||
if type(driver).__name__ == "NoneType":
|
||
raise Exception("The driver to quit was a NoneType variable!")
|
||
elif (
|
||
not hasattr(driver, "get")
|
||
or not hasattr(driver, "name")
|
||
or not hasattr(driver, "quit")
|
||
or not hasattr(driver, "capabilities")
|
||
or not hasattr(driver, "window_handles")
|
||
):
|
||
raise Exception("The driver to quit does not match a Driver!")
|
||
elif self._reuse_session and driver == self._default_driver:
|
||
raise Exception(
|
||
"Cannot quit the initial driver in --reuse-session mode!\n"
|
||
"This is done automatically after all tests have ended.\n"
|
||
"Use this method only if get_new_driver() has been called."
|
||
)
|
||
elif (
|
||
driver == self._default_driver
|
||
or (driver in self._drivers_list and len(self._drivers_list) == 1)
|
||
):
|
||
raise Exception(
|
||
"Cannot quit the default/initial driver!\n"
|
||
"This is done automatically at the end of each test.\n"
|
||
"Use this method only if get_new_driver() has been called."
|
||
)
|
||
try:
|
||
if (
|
||
not is_windows
|
||
or self.browser == "ie"
|
||
or driver.service.process
|
||
):
|
||
driver.quit()
|
||
except AttributeError:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
if driver in self._drivers_list:
|
||
self._drivers_list.remove(driver)
|
||
if driver in self._drivers_browser_map:
|
||
del self._drivers_browser_map[driver]
|
||
# If the driver to quit was the active driver, switch drivers
|
||
if driver == self.driver:
|
||
self.switch_to_default_driver()
|
||
try:
|
||
self._check_browser()
|
||
except Exception:
|
||
self._default_driver = self._drivers_list[-1]
|
||
self.switch_to_default_driver()
|
||
|
||
############
|
||
|
||
def __assert_eq(self, *args, **kwargs):
|
||
"""Minified assert_equal() using only the list diff."""
|
||
minified_exception = None
|
||
try:
|
||
self.assertEqual(*args, **kwargs)
|
||
except Exception as e:
|
||
str_e = str(e)
|
||
minified_exception = "\nAssertionError:\n"
|
||
lines = str_e.split("\n")
|
||
countdown = 3
|
||
countdown_on = False
|
||
first_differing = False
|
||
skip_lines = False
|
||
for line in lines:
|
||
if countdown_on:
|
||
if not skip_lines:
|
||
minified_exception += line + "\n"
|
||
countdown = countdown - 1
|
||
if countdown == 0:
|
||
countdown_on = False
|
||
skip_lines = False
|
||
elif line.startswith("First differing"):
|
||
first_differing = True
|
||
countdown_on = True
|
||
countdown = 3
|
||
minified_exception += line + "\n"
|
||
elif line.startswith("First list"):
|
||
countdown_on = True
|
||
countdown = 3
|
||
if not first_differing:
|
||
minified_exception += line + "\n"
|
||
else:
|
||
skip_lines = True
|
||
elif line.startswith("F"):
|
||
countdown_on = True
|
||
countdown = 3
|
||
minified_exception += line + "\n"
|
||
elif line.startswith(("+", "-")):
|
||
minified_exception += line + "\n"
|
||
elif line.startswith("?"):
|
||
minified_exception += line + "\n"
|
||
elif line.strip().startswith("*"):
|
||
minified_exception += line + "\n"
|
||
if minified_exception:
|
||
raise VisualException(minified_exception)
|
||
|
||
def _process_visual_baseline_logs(self):
|
||
if not (
|
||
(python3_11_or_newer and py311_patch2)
|
||
or "--pdb" in sys.argv
|
||
):
|
||
return
|
||
self.__process_visual_baseline_logs()
|
||
|
||
def __process_visual_baseline_logs(self):
|
||
"""Save copies of baseline PNGs in "./latest_logs" during failures.
|
||
Also create a side_by_side.html file for visual comparisons."""
|
||
test_logpath = os.path.join(self.log_path, self.__get_test_id())
|
||
for baseline_copy_tuple in self.__visual_baseline_copies:
|
||
baseline_path = baseline_copy_tuple[0]
|
||
baseline_copy_name = baseline_copy_tuple[1]
|
||
b_c_alt_name = baseline_copy_tuple[2]
|
||
latest_png_path = baseline_copy_tuple[3]
|
||
latest_copy_name = baseline_copy_tuple[4]
|
||
l_c_alt_name = baseline_copy_tuple[5]
|
||
baseline_copy_path = os.path.join(test_logpath, baseline_copy_name)
|
||
b_c_alt_path = os.path.join(test_logpath, b_c_alt_name)
|
||
latest_copy_path = os.path.join(test_logpath, latest_copy_name)
|
||
l_c_alt_path = os.path.join(test_logpath, l_c_alt_name)
|
||
if len(self.__visual_baseline_copies) == 1:
|
||
baseline_copy_path = b_c_alt_path
|
||
latest_copy_path = l_c_alt_path
|
||
if (
|
||
os.path.exists(baseline_path)
|
||
and not os.path.exists(baseline_copy_path)
|
||
):
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
shutil.copy(baseline_path, baseline_copy_path)
|
||
if (
|
||
os.path.exists(latest_png_path)
|
||
and not os.path.exists(latest_copy_path)
|
||
):
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
shutil.copy(latest_png_path, latest_copy_path)
|
||
if len(self.__visual_baseline_copies) != 1:
|
||
return # Skip the rest when deferred visual asserts are used
|
||
the_html = visual_helper.get_sbs_html()
|
||
file_path = os.path.join(test_logpath, constants.SideBySide.HTML_FILE)
|
||
out_file = codecs.open(file_path, "w+", encoding="utf-8")
|
||
out_file.writelines(the_html)
|
||
out_file.close()
|
||
|
||
def check_window(
|
||
self,
|
||
name="default",
|
||
level=0,
|
||
baseline=False,
|
||
check_domain=True,
|
||
full_diff=False,
|
||
):
|
||
"""*** Automated Visual Testing with SeleniumBase ***
|
||
|
||
The first time a test calls self.check_window() for a unique "name"
|
||
parameter provided, it will set a visual baseline, meaning that it
|
||
creates a folder, saves the URL to a file, saves the current window
|
||
screenshot to a file, and creates the following three files
|
||
with the listed data saved:
|
||
tags_level1.txt -> HTML tags from the window
|
||
tags_level2.txt -> HTML tags + attributes from the window
|
||
tags_level3.txt -> HTML tags + attributes/values from the window
|
||
|
||
Baseline folders are named based on the test name and the name
|
||
parameter passed to self.check_window(). The same test can store
|
||
multiple baseline folders.
|
||
|
||
If the baseline is being set/reset, the "level" doesn't matter.
|
||
|
||
After the first run of self.check_window(), it will compare the
|
||
HTML tags of the latest window to the one from the initial run.
|
||
Here's how the level system works:
|
||
* level=0 ->
|
||
DRY RUN ONLY - Will perform comparisons to the baseline (and
|
||
print out any differences that are found) but
|
||
won't fail the test even if differences exist.
|
||
* level=1 ->
|
||
HTML tags are compared to tags_level1.txt
|
||
* level=2 ->
|
||
HTML tags are compared to tags_level1.txt and
|
||
HTML tags/attributes are compared to tags_level2.txt
|
||
* level=3 ->
|
||
HTML tags are compared to tags_level1.txt and
|
||
HTML tags + attributes are compared to tags_level2.txt and
|
||
HTML tags + attributes/values are compared to tags_level3.txt
|
||
As shown, Level-3 is the most strict, Level-1 is the least strict.
|
||
If the comparisons from the latest window to the existing baseline
|
||
don't match, the current test will fail, except for Level-0 tests.
|
||
|
||
You can reset the visual baseline on the command line by using:
|
||
--visual_baseline
|
||
As long as "--visual_baseline" is used on the command line while
|
||
running tests, the self.check_window() method cannot fail because
|
||
it will rebuild the visual baseline rather than comparing the html
|
||
tags of the latest run to the existing baseline. If there are any
|
||
expected layout changes to a website that you're testing, you'll
|
||
need to reset the baseline to prevent unnecessary failures.
|
||
|
||
self.check_window() will fail with "Page Domain Mismatch Failure"
|
||
if the page domain doesn't match the domain of the baseline,
|
||
unless "check_domain" is set to False when calling check_window().
|
||
|
||
If you want to use self.check_window() to compare a web page to
|
||
a later version of itself from within the same test run, you can
|
||
add the parameter "baseline=True" to the first time you call
|
||
self.check_window() in a test to use that as the baseline. This
|
||
only makes sense if you're calling self.check_window() more than
|
||
once with the same name parameter in the same test.
|
||
|
||
If "full_diff" is set to False, the error output will only
|
||
include the first differing element in the list comparison.
|
||
Set "full_diff" to True if you want to see the full output.
|
||
|
||
Automated Visual Testing with self.check_window() is not very
|
||
effective for websites that have dynamic content that changes
|
||
the layout and structure of web pages. For those, you're much
|
||
better off using regular SeleniumBase functional testing.
|
||
|
||
Example usage:
|
||
self.check_window(name="testing", level=0)
|
||
self.check_window(name="xkcd_home", level=1)
|
||
self.check_window(name="github_page", level=2)
|
||
self.check_window(name="wikipedia_page", level=3) """
|
||
self.wait_for_ready_state_complete()
|
||
with suppress(Exception):
|
||
self.wait_for_element_visible(
|
||
"body", timeout=settings.MINI_TIMEOUT
|
||
)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.08)
|
||
if level == "0":
|
||
level = 0
|
||
if level == "1":
|
||
level = 1
|
||
if level == "2":
|
||
level = 2
|
||
if level == "3":
|
||
level = 3
|
||
if level != 0 and level != 1 and level != 2 and level != 3:
|
||
raise Exception('Parameter "level" must be set to 0, 1, 2, or 3!')
|
||
|
||
if self.demo_mode:
|
||
message = (
|
||
"WARNING: Using check_window() from Demo Mode may lead "
|
||
"to unexpected results caused by Demo Mode HTML changes."
|
||
)
|
||
logging.info(message)
|
||
|
||
test_id = self.__get_display_id().split("::")[-1]
|
||
|
||
if not name or len(name) < 1:
|
||
name = "default"
|
||
name = str(name)
|
||
|
||
visual_helper.visual_baseline_folder_setup()
|
||
baseline_dir = constants.VisualBaseline.STORAGE_FOLDER
|
||
visual_baseline_path = os.path.join(baseline_dir, test_id, name)
|
||
page_url_file = os.path.join(visual_baseline_path, "page_url.txt")
|
||
baseline_png = "baseline.png"
|
||
baseline_png_path = os.path.join(visual_baseline_path, baseline_png)
|
||
latest_png = "latest.png"
|
||
latest_png_path = os.path.join(visual_baseline_path, latest_png)
|
||
level_1_file = os.path.join(visual_baseline_path, "tags_level_1.txt")
|
||
level_2_file = os.path.join(visual_baseline_path, "tags_level_2.txt")
|
||
level_3_file = os.path.join(visual_baseline_path, "tags_level_3.txt")
|
||
|
||
set_baseline = False
|
||
if baseline or self.visual_baseline:
|
||
set_baseline = True
|
||
if not os.path.exists(visual_baseline_path):
|
||
set_baseline = True
|
||
try:
|
||
os.makedirs(visual_baseline_path)
|
||
except Exception:
|
||
pass # Only reachable during multi-threaded test runs
|
||
if not os.path.exists(page_url_file):
|
||
set_baseline = True
|
||
if not os.path.exists(baseline_png_path):
|
||
set_baseline = True
|
||
if not os.path.exists(level_1_file):
|
||
set_baseline = True
|
||
if not os.path.exists(level_2_file):
|
||
set_baseline = True
|
||
if not os.path.exists(level_3_file):
|
||
set_baseline = True
|
||
|
||
page_url = self.get_current_url()
|
||
soup = self.get_beautiful_soup()
|
||
html_tags = soup.body.find_all()
|
||
level_1 = [[tag.name] for tag in html_tags]
|
||
level_1 = json.loads(json.dumps(level_1)) # Tuples become lists
|
||
level_2 = [[tag.name, sorted(tag.attrs.keys())] for tag in html_tags]
|
||
level_2 = json.loads(json.dumps(level_2)) # Tuples become lists
|
||
level_3 = [[tag.name, sorted(tag.attrs.items())] for tag in html_tags]
|
||
level_3 = json.loads(json.dumps(level_3)) # Tuples become lists
|
||
|
||
if set_baseline:
|
||
self.save_screenshot(
|
||
baseline_png, visual_baseline_path, selector="body"
|
||
)
|
||
out_file = codecs.open(page_url_file, "w+", encoding="utf-8")
|
||
out_file.writelines(page_url)
|
||
out_file.close()
|
||
out_file = codecs.open(level_1_file, "w+", encoding="utf-8")
|
||
out_file.writelines(json.dumps(level_1))
|
||
out_file.close()
|
||
out_file = codecs.open(level_2_file, "w+", encoding="utf-8")
|
||
out_file.writelines(json.dumps(level_2))
|
||
out_file.close()
|
||
out_file = codecs.open(level_3_file, "w+", encoding="utf-8")
|
||
out_file.writelines(json.dumps(level_3))
|
||
out_file.close()
|
||
|
||
baseline_path = os.path.join(visual_baseline_path, baseline_png)
|
||
baseline_copy_name = "baseline_%s.png" % name
|
||
b_c_alt_name = "baseline.png"
|
||
latest_copy_name = "baseline_diff_%s.png" % name
|
||
l_c_alt_name = "baseline_diff.png"
|
||
baseline_copy_tuple = (
|
||
baseline_path, baseline_copy_name, b_c_alt_name,
|
||
latest_png_path, latest_copy_name, l_c_alt_name,
|
||
)
|
||
self.__visual_baseline_copies.append(baseline_copy_tuple)
|
||
|
||
is_level_0_failure = False
|
||
if not set_baseline:
|
||
self.save_screenshot(
|
||
latest_png, visual_baseline_path, selector="body"
|
||
)
|
||
f = open(page_url_file, "r")
|
||
page_url_data = f.read().strip()
|
||
f.close()
|
||
f = open(level_1_file, "r")
|
||
level_1_data = json.loads(f.read())
|
||
f.close()
|
||
f = open(level_2_file, "r")
|
||
level_2_data = json.loads(f.read())
|
||
f.close()
|
||
f = open(level_3_file, "r")
|
||
level_3_data = json.loads(f.read())
|
||
f.close()
|
||
|
||
domain_fail = (
|
||
"\n*\nPage Domain Mismatch Failure: "
|
||
"Current Page Domain doesn't match the Page Domain of the "
|
||
"Baseline! Can't compare two completely different sites! "
|
||
"Run with --visual_baseline to reset the baseline!"
|
||
)
|
||
level_1_failure = (
|
||
"\n*\n*** Exception: <Level 1> Visual Diff Failure:\n"
|
||
"* HTML tags don't match the baseline!"
|
||
)
|
||
level_2_failure = (
|
||
"\n*\n*** Exception: <Level 2> Visual Diff Failure:\n"
|
||
"* HTML tag attribute names don't match the baseline!"
|
||
)
|
||
level_3_failure = (
|
||
"\n*\n*** Exception: <Level 3> Visual Diff Failure:\n"
|
||
"* HTML tag attribute values don't match the baseline!"
|
||
)
|
||
|
||
page_domain = self.get_domain_url(page_url)
|
||
page_data_domain = self.get_domain_url(page_url_data)
|
||
unittest.TestCase.maxDiff = 65536 # 2^16
|
||
if level != 0 and check_domain:
|
||
self.assertEqual(page_data_domain, page_domain, domain_fail)
|
||
if level == 3:
|
||
if not full_diff:
|
||
self.__assert_eq(level_3_data, level_3, level_3_failure)
|
||
else:
|
||
self.assertEqual(level_3_data, level_3, level_3_failure)
|
||
if level == 2:
|
||
if not full_diff:
|
||
self.__assert_eq(level_2_data, level_2, level_2_failure)
|
||
else:
|
||
self.assertEqual(level_2_data, level_2, level_2_failure)
|
||
if level == 1:
|
||
if not full_diff:
|
||
self.__assert_eq(level_1_data, level_1, level_1_failure)
|
||
else:
|
||
self.assertEqual(level_1_data, level_1, level_1_failure)
|
||
if level == 0:
|
||
try:
|
||
if check_domain:
|
||
self.assertEqual(
|
||
page_domain, page_data_domain, domain_fail
|
||
)
|
||
try:
|
||
if not full_diff:
|
||
self.__assert_eq(
|
||
level_1_data, level_1, level_1_failure
|
||
)
|
||
else:
|
||
self.assertEqual(
|
||
level_1_data, level_1, level_1_failure
|
||
)
|
||
except Exception as e:
|
||
print(e)
|
||
try:
|
||
if not full_diff:
|
||
self.__assert_eq(
|
||
level_2_data, level_2, level_2_failure
|
||
)
|
||
else:
|
||
self.assertEqual(
|
||
level_2_data, level_2, level_2_failure
|
||
)
|
||
except Exception as e:
|
||
print(e)
|
||
if not full_diff:
|
||
self.__assert_eq(
|
||
level_3_data, level_3, level_3_failure
|
||
)
|
||
else:
|
||
self.assertEqual(
|
||
level_3_data, level_3, level_3_failure
|
||
)
|
||
except Exception as e:
|
||
print(e) # Level-0 Dry Run (Only print the differences)
|
||
is_level_0_failure = True
|
||
unittest.TestCase.maxDiff = None # Reset unittest.TestCase.maxDiff
|
||
# Since the check passed, do not save an extra copy of the baseline
|
||
del self.__visual_baseline_copies[-1] # .pop() returns the element
|
||
if is_level_0_failure:
|
||
# Generating the side_by_side.html file for Level-0 failures
|
||
test_logpath = os.path.join(self.log_path, self.__get_test_id())
|
||
if (
|
||
not os.path.exists(baseline_path)
|
||
or not os.path.exists(latest_png_path)
|
||
):
|
||
return
|
||
self.__level_0_visual_f = True
|
||
if not os.path.exists(test_logpath):
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
baseline_copy_path = os.path.join(test_logpath, baseline_copy_name)
|
||
latest_copy_path = os.path.join(test_logpath, latest_copy_name)
|
||
if (
|
||
not os.path.exists(baseline_copy_path)
|
||
and not os.path.exists(latest_copy_path)
|
||
):
|
||
shutil.copy(baseline_path, baseline_copy_path)
|
||
shutil.copy(latest_png_path, latest_copy_path)
|
||
the_html = visual_helper.get_sbs_html(
|
||
baseline_copy_name, latest_copy_name
|
||
)
|
||
alpha_n_d_name = "".join([x if x.isalnum() else "_" for x in name])
|
||
side_by_side_name = "side_by_side_%s.html" % alpha_n_d_name
|
||
file_path = os.path.join(test_logpath, side_by_side_name)
|
||
out_file = codecs.open(file_path, "w+", encoding="utf-8")
|
||
out_file.writelines(the_html)
|
||
out_file.close()
|
||
|
||
############
|
||
|
||
def __get_new_timeout(self, timeout):
|
||
"""When using --timeout_multiplier=#.#"""
|
||
self.__check_scope()
|
||
try:
|
||
timeout_multiplier = float(self.timeout_multiplier)
|
||
if timeout_multiplier <= 0.5:
|
||
timeout_multiplier = 0.5
|
||
timeout = int(math.ceil(timeout_multiplier * timeout))
|
||
return timeout
|
||
except Exception:
|
||
# Wrong data type for timeout_multiplier (expecting int or float)
|
||
return timeout
|
||
|
||
############
|
||
|
||
def __is_cdp_swap_needed(self):
|
||
"""If the driver is disconnected, use a CDP method when available."""
|
||
return shared_utils.is_cdp_swap_needed(self.driver)
|
||
|
||
############
|
||
|
||
def __check_scope(self):
|
||
if hasattr(self, "browser"): # self.browser stores the type of browser
|
||
return # All good: setUp() already initialized variables in "self"
|
||
else:
|
||
message = (
|
||
"\n It looks like you are trying to call a SeleniumBase method"
|
||
"\n from outside the scope of your test class's `self` object,"
|
||
"\n which is initialized by calling BaseCase's setUp() method."
|
||
"\n The `self` object is where all test variables are defined."
|
||
"\n If you created a custom setUp() method (that overrided the"
|
||
"\n the default one), make sure to call super().setUp() in it."
|
||
"\n When using page objects, be sure to pass the `self` object"
|
||
"\n from your test class into your page object methods so that"
|
||
"\n they can call BaseCase class methods with all the required"
|
||
"\n variables, which are initialized during the setUp() method"
|
||
"\n that runs automatically before all tests called by pytest."
|
||
)
|
||
raise OutOfScopeException(message)
|
||
|
||
############
|
||
|
||
def __get_exception_message(self):
|
||
"""This method extracts the message from an exception if there
|
||
was an exception that occurred during the test, assuming
|
||
that the exception was in a try/except block and not thrown."""
|
||
exception_info = sys.exc_info()[1]
|
||
if hasattr(exception_info, "msg"):
|
||
exc_message = exception_info.msg
|
||
elif hasattr(exception_info, "message"):
|
||
exc_message = exception_info.message
|
||
elif hasattr(exception_info, "args") and len(exception_info.args) == 1:
|
||
exc_message = exception_info.args[0]
|
||
else:
|
||
exc_message = sys.exc_info()
|
||
return exc_message
|
||
|
||
def __add_deferred_assert_failure(self, fs=False):
|
||
"""Add a deferred_assert failure to a list for future processing."""
|
||
self.__check_scope()
|
||
current_url = self.driver.current_url
|
||
message = self.__get_exception_message()
|
||
count = self.__deferred_assert_count
|
||
self.__deferred_assert_failures.append(
|
||
"DEFERRED ASSERT #%s: (%s) %s\n" % (count, current_url, message)
|
||
)
|
||
if fs:
|
||
self.save_screenshot_to_logs(name="deferred_#%s" % count)
|
||
|
||
############
|
||
|
||
def deferred_assert_element(
|
||
self, selector, by="css selector", timeout=None, fs=False
|
||
):
|
||
"""A non-terminating assertion for an element visible on the page.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__deferred_assert_count += 1
|
||
with suppress(Exception):
|
||
url = self.get_current_url()
|
||
if url == self.__last_url_of_deferred_assert:
|
||
timeout = 0.6 # Was already on page (full wait not needed)
|
||
else:
|
||
self.__last_url_of_deferred_assert = url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["da_el", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
try:
|
||
self.wait_for_element_visible(selector, by=by, timeout=timeout)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def deferred_assert_element_present(
|
||
self, selector, by="css selector", timeout=None, fs=False
|
||
):
|
||
"""A non-terminating assertion for an element present in the page html.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__deferred_assert_count += 1
|
||
with suppress(Exception):
|
||
url = self.get_current_url()
|
||
if url == self.__last_url_of_deferred_assert:
|
||
timeout = 0.6 # Was already on page (full wait not needed)
|
||
else:
|
||
self.__last_url_of_deferred_assert = url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["da_ep", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
try:
|
||
self.wait_for_element_present(selector, by=by, timeout=timeout)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def deferred_assert_text(
|
||
self, text, selector="body", by="css selector", timeout=None, fs=False
|
||
):
|
||
"""A non-terminating assertion for text from an element on a page.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__deferred_assert_count += 1
|
||
with suppress(Exception):
|
||
url = self.get_current_url()
|
||
if url == self.__last_url_of_deferred_assert:
|
||
timeout = 0.6 # Was already on page (full wait not needed)
|
||
else:
|
||
self.__last_url_of_deferred_assert = url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["da_te", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
try:
|
||
self.wait_for_text_visible(text, selector, by=by, timeout=timeout)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def deferred_assert_exact_text(
|
||
self, text, selector="body", by="css selector", timeout=None, fs=False
|
||
):
|
||
"""A non-terminating assertion for exact text from an element.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__deferred_assert_count += 1
|
||
with suppress(Exception):
|
||
url = self.get_current_url()
|
||
if url == self.__last_url_of_deferred_assert:
|
||
timeout = 0.6 # Was already on page (full wait not needed)
|
||
else:
|
||
self.__last_url_of_deferred_assert = url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
text_selector = [text, selector]
|
||
action = ["da_et", text_selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
try:
|
||
self.wait_for_exact_text_visible(
|
||
text, selector, by=by, timeout=timeout
|
||
)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def deferred_assert_non_empty_text(
|
||
self,
|
||
selector="body",
|
||
by="css selector",
|
||
timeout=None,
|
||
fs=False,
|
||
):
|
||
"""A non-terminating assertion for non-empty element text.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
if not timeout:
|
||
timeout = settings.MINI_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.MINI_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__deferred_assert_count += 1
|
||
with suppress(Exception):
|
||
url = self.get_current_url()
|
||
if url == self.__last_url_of_deferred_assert:
|
||
timeout = 0.6 # Was already on page (full wait not needed)
|
||
else:
|
||
self.__last_url_of_deferred_assert = url
|
||
if self.recorder_mode and self.__current_url_is_recordable():
|
||
if self.get_session_storage_item("pause_recorder") == "no":
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["danet", selector, origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
try:
|
||
self.wait_for_non_empty_text_visible(
|
||
selector, by=by, timeout=timeout
|
||
)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def deferred_check_window(
|
||
self,
|
||
name="default",
|
||
level=0,
|
||
baseline=False,
|
||
check_domain=True,
|
||
full_diff=False,
|
||
fs=False,
|
||
):
|
||
"""A non-terminating assertion for the check_window() method.
|
||
Failures will be saved until the process_deferred_asserts()
|
||
method is called from inside a test, likely at the end of it.
|
||
If "fs" is set to True, a failure screenshot is saved to the
|
||
"latest_logs/" folder for that assertion failure. Otherwise,
|
||
only the last page screenshot is taken for all failures when
|
||
calling the process_deferred_asserts() method."""
|
||
self.__check_scope()
|
||
self.__deferred_assert_count += 1
|
||
try:
|
||
self.check_window(
|
||
name=name,
|
||
level=level,
|
||
baseline=baseline,
|
||
check_domain=check_domain,
|
||
full_diff=full_diff,
|
||
)
|
||
return True
|
||
except Exception:
|
||
self.__add_deferred_assert_failure(fs=fs)
|
||
return False
|
||
|
||
def process_deferred_asserts(self, print_only=False):
|
||
"""To be used with any test that uses deferred_asserts, which are
|
||
non-terminating verifications that only raise exceptions
|
||
after this method is called.
|
||
This is useful for pages with multiple elements to be checked when
|
||
you want to find as many bugs as possible in a single test run
|
||
before having all the exceptions get raised simultaneously.
|
||
Might be more useful if this method is called after processing all
|
||
the deferred asserts on a single html page so that the failure
|
||
screenshot matches the location of the deferred asserts.
|
||
If "print_only" is set to True, the exception won't get raised."""
|
||
if self.recorder_mode:
|
||
time_stamp = self.execute_script("return Date.now();")
|
||
origin = self.get_origin()
|
||
action = ["pr_da", "", origin, time_stamp]
|
||
self.__extra_actions.append(action)
|
||
if self.__deferred_assert_failures:
|
||
exception_output = ""
|
||
exception_output += "\n***** DEFERRED ASSERTION FAILURES:\n"
|
||
exception_output += "TEST: %s\n\n" % self.id()
|
||
all_failing_checks = self.__deferred_assert_failures
|
||
self.__deferred_assert_failures = []
|
||
for tb in all_failing_checks:
|
||
exception_output += "%s\n" % tb
|
||
if print_only:
|
||
print(exception_output)
|
||
else:
|
||
raise Exception(exception_output.replace("\\n", "\n"))
|
||
|
||
############
|
||
|
||
# Alternate naming scheme for the "deferred_assert" methods.
|
||
|
||
def delayed_assert_element(
|
||
self, selector, by="css selector", timeout=None, fs=False
|
||
):
|
||
"""Same as self.deferred_assert_element()"""
|
||
return self.deferred_assert_element(
|
||
selector=selector, by=by, timeout=timeout, fs=fs
|
||
)
|
||
|
||
def delayed_assert_element_present(
|
||
self, selector, by="css selector", timeout=None, fs=False
|
||
):
|
||
"""Same as self.deferred_assert_element_present()"""
|
||
return self.deferred_assert_element_present(
|
||
selector=selector, by=by, timeout=timeout, fs=fs
|
||
)
|
||
|
||
def delayed_assert_text(
|
||
self, text, selector="body", by="css selector", timeout=None, fs=False
|
||
):
|
||
"""Same as self.deferred_assert_text()"""
|
||
return self.deferred_assert_text(
|
||
text=text, selector=selector, by=by, timeout=timeout, fs=fs
|
||
)
|
||
|
||
def delayed_assert_exact_text(
|
||
self, text, selector="body", by="css selector", timeout=None, fs=False
|
||
):
|
||
"""Same as self.deferred_assert_exact_text()"""
|
||
return self.deferred_assert_exact_text(
|
||
text=text, selector=selector, by=by, timeout=timeout, fs=fs
|
||
)
|
||
|
||
def delayed_assert_non_empty_text(
|
||
self,
|
||
selector="body",
|
||
by="css selector",
|
||
timeout=None,
|
||
fs=False,
|
||
):
|
||
"""Same as self.deferred_assert_non_empty_text()"""
|
||
return self.deferred_assert_non_empty_text(
|
||
selector=selector, by=by, timeout=timeout, fs=fs
|
||
)
|
||
|
||
def delayed_check_window(
|
||
self,
|
||
name="default",
|
||
level=0,
|
||
baseline=False,
|
||
check_domain=True,
|
||
full_diff=False,
|
||
fs=False,
|
||
):
|
||
"""Same as self.deferred_check_window()"""
|
||
return self.deferred_check_window(
|
||
name=name,
|
||
level=level,
|
||
baseline=baseline,
|
||
check_domain=check_domain,
|
||
full_diff=full_diff,
|
||
fs=fs,
|
||
)
|
||
|
||
def process_delayed_asserts(self, print_only=False):
|
||
"""Same as self.process_deferred_asserts()"""
|
||
self.process_deferred_asserts(print_only=print_only)
|
||
|
||
############
|
||
|
||
def create_presentation(
|
||
self, name=None, theme="default", transition="default"
|
||
):
|
||
"""Creates a Reveal-JS presentation that you can add slides to.
|
||
@Params
|
||
name - If creating multiple presentations at the same time,
|
||
use this to specify the name of the current presentation.
|
||
theme - Set a theme with a unique style for the presentation.
|
||
Valid themes: "serif" (default), "sky", "white", "black",
|
||
"simple", "league", "moon", "night",
|
||
"beige", "blood", and "solarized".
|
||
transition - Set a transition between slides.
|
||
Valid transitions: "none" (default), "slide", "fade",
|
||
"zoom", "convex", and "concave"."""
|
||
if not name:
|
||
name = "default"
|
||
if not theme or theme == "default":
|
||
theme = "serif"
|
||
valid_themes = [
|
||
"serif",
|
||
"white",
|
||
"black",
|
||
"beige",
|
||
"simple",
|
||
"sky",
|
||
"league",
|
||
"moon",
|
||
"night",
|
||
"blood",
|
||
"solarized",
|
||
]
|
||
theme = theme.lower()
|
||
if theme not in valid_themes:
|
||
raise Exception(
|
||
"Theme {%s} not found! Valid themes: %s"
|
||
% (theme, valid_themes)
|
||
)
|
||
if not transition or transition == "default":
|
||
transition = "none"
|
||
valid_transitions = [
|
||
"none",
|
||
"slide",
|
||
"fade",
|
||
"zoom",
|
||
"convex",
|
||
"concave",
|
||
]
|
||
transition = transition.lower()
|
||
if transition not in valid_transitions:
|
||
raise Exception(
|
||
"Transition {%s} not found! Valid transitions: %s"
|
||
% (transition, valid_transitions)
|
||
)
|
||
reveal_theme_css = None
|
||
if theme == "serif":
|
||
reveal_theme_css = constants.Reveal.SERIF_MIN_CSS
|
||
elif theme == "sky":
|
||
reveal_theme_css = constants.Reveal.SKY_MIN_CSS
|
||
elif theme == "white":
|
||
reveal_theme_css = constants.Reveal.WHITE_MIN_CSS
|
||
elif theme == "black":
|
||
reveal_theme_css = constants.Reveal.BLACK_MIN_CSS
|
||
elif theme == "simple":
|
||
reveal_theme_css = constants.Reveal.SIMPLE_MIN_CSS
|
||
elif theme == "league":
|
||
reveal_theme_css = constants.Reveal.LEAGUE_MIN_CSS
|
||
elif theme == "moon":
|
||
reveal_theme_css = constants.Reveal.MOON_MIN_CSS
|
||
elif theme == "night":
|
||
reveal_theme_css = constants.Reveal.NIGHT_MIN_CSS
|
||
elif theme == "beige":
|
||
reveal_theme_css = constants.Reveal.BEIGE_MIN_CSS
|
||
elif theme == "blood":
|
||
reveal_theme_css = constants.Reveal.BLOOD_MIN_CSS
|
||
elif theme == "solarized":
|
||
reveal_theme_css = constants.Reveal.SOLARIZED_MIN_CSS
|
||
else:
|
||
# Use the default if unable to determine the theme
|
||
reveal_theme_css = constants.Reveal.SERIF_MIN_CSS
|
||
new_presentation = (
|
||
"<html>\n"
|
||
"<head>\n"
|
||
'<meta charset="utf-8">\n'
|
||
'<meta http-equiv="Content-Type" content="text/html">\n'
|
||
'<meta name="viewport" content="shrink-to-fit=no">\n'
|
||
'<link rel="stylesheet" href="%s">\n'
|
||
'<link rel="stylesheet" href="%s">\n'
|
||
"<style>\n"
|
||
"pre{background-color:#fbe8d4;border-radius:8px;}\n"
|
||
"div[flex_div]{height:68vh;margin:0;align-items:center;"
|
||
"justify-content:center;}\n"
|
||
"img[rounded]{border-radius:16px;max-width:64%%;}\n"
|
||
"</style>\n"
|
||
"</head>\n\n"
|
||
"<body>\n"
|
||
"<!-- Generated by SeleniumBase - https://seleniumbase.io -->\n"
|
||
'<div class="reveal">\n'
|
||
'<div class="slides">\n'
|
||
% (constants.Reveal.MIN_CSS, reveal_theme_css)
|
||
)
|
||
self._presentation_slides[name] = []
|
||
self._presentation_slides[name].append(new_presentation)
|
||
self._presentation_transition[name] = transition
|
||
|
||
def add_slide(
|
||
self,
|
||
content=None,
|
||
image=None,
|
||
code=None,
|
||
iframe=None,
|
||
content2=None,
|
||
notes=None,
|
||
transition=None,
|
||
name=None,
|
||
):
|
||
"""Allows the user to add slides to a presentation.
|
||
@Params
|
||
content - The HTML content to display on the presentation slide.
|
||
image - Attach an image (from a URL link) to the slide.
|
||
code - Attach code of any programming language to the slide.
|
||
Language-detection will be used to add syntax formatting.
|
||
iframe - Attach an iframe (from a URL link) to the slide.
|
||
content2 - HTML content to display after adding an image or code.
|
||
notes - Additional notes to include with the slide.
|
||
ONLY SEEN if show_notes is set for the presentation.
|
||
transition - Set a transition between slides. (overrides previous)
|
||
Valid transitions: "none" (default), "slide", "fade",
|
||
"zoom", "convex", and "concave".
|
||
name - If creating multiple presentations at the same time,
|
||
use this to select the presentation to add slides to."""
|
||
if not name:
|
||
name = "default"
|
||
if name not in self._presentation_slides:
|
||
# Create a presentation if it doesn't already exist
|
||
self.create_presentation(name=name)
|
||
if not content:
|
||
content = ""
|
||
if not content2:
|
||
content2 = ""
|
||
if not notes:
|
||
notes = ""
|
||
if not transition:
|
||
transition = self._presentation_transition[name]
|
||
elif transition == "default":
|
||
transition = "none"
|
||
valid_transitions = [
|
||
"none",
|
||
"slide",
|
||
"fade",
|
||
"zoom",
|
||
"convex",
|
||
"concave",
|
||
]
|
||
transition = transition.lower()
|
||
if transition not in valid_transitions:
|
||
raise Exception(
|
||
"Transition {%s} not found! Valid transitions: %s"
|
||
"" % (transition, valid_transitions)
|
||
)
|
||
add_line = ""
|
||
if content.startswith("<"):
|
||
add_line = "\n"
|
||
html = '\n<section data-transition="%s">%s%s' % (
|
||
transition,
|
||
add_line,
|
||
content,
|
||
)
|
||
if image:
|
||
html += '\n<div flex_div><img rounded src="%s" /></div>' % image
|
||
if code:
|
||
html += "\n<div></div>"
|
||
html += '\n<pre class="prettyprint">\n%s</pre>' % code
|
||
if iframe:
|
||
html += (
|
||
"\n<div></div>"
|
||
'\n<iframe src="%s" style="width:92%%;height:550px;" '
|
||
'title="iframe content"></iframe>' % iframe
|
||
)
|
||
add_line = ""
|
||
if content2.startswith("<"):
|
||
add_line = "\n"
|
||
if content2:
|
||
html += "%s%s" % (add_line, content2)
|
||
html += '\n<aside class="notes">%s</aside>' % notes
|
||
html += "\n</section>\n"
|
||
if "<mk-0>" not in html and "<mk-1>" not in html:
|
||
self._presentation_slides[name].append(html)
|
||
else:
|
||
# Generate multiple slides with <mark> and </mark>
|
||
replacements = False
|
||
for num in range(32):
|
||
if "<mk-%s>" % num in html and "</mk-%s>" % num in html:
|
||
replacements = True
|
||
new_html = html
|
||
new_html = new_html.replace("<mk-%s>" % num, "<mark>")
|
||
new_html = new_html.replace("</mk-%s>" % num, "</mark>")
|
||
for num2 in range(32):
|
||
if num2 == num:
|
||
continue
|
||
if "<mk-%s>" % num2 not in new_html and num2 >= 2:
|
||
break
|
||
new_html = new_html.replace("<mk-%s>" % num2, "")
|
||
new_html = new_html.replace("</mk-%s>" % num2, "")
|
||
self._presentation_slides[name].append(new_html)
|
||
else:
|
||
if num >= 2:
|
||
break
|
||
if not replacements:
|
||
# A <mark> is missing a closing tag. Do one.
|
||
self._presentation_slides[name].append(html)
|
||
|
||
def save_presentation(
|
||
self, name=None, filename=None, show_notes=False, interval=0
|
||
):
|
||
"""Saves a Reveal-JS Presentation to a file for later use.
|
||
@Params
|
||
name - If creating multiple presentations at the same time,
|
||
use this to select the one you wish to use.
|
||
filename - The name of the HTML file that you wish to
|
||
save the presentation to. (filename must end in ".html")
|
||
show_notes - When set to True, the Notes feature becomes enabled,
|
||
which allows presenters to see notes next to slides.
|
||
interval - The delay time between autoplaying slides. (in seconds)
|
||
If set to 0 (default), autoplay is disabled."""
|
||
if not name:
|
||
name = "default"
|
||
if not filename:
|
||
filename = "my_presentation.html"
|
||
if name not in self._presentation_slides:
|
||
raise Exception("Presentation {%s} does not exist!" % name)
|
||
if not filename.endswith(".html"):
|
||
raise Exception('Presentation file must end in ".html"!')
|
||
if not interval:
|
||
interval = 0
|
||
if interval == 0 and self.interval:
|
||
interval = float(self.interval)
|
||
if not isinstance(interval, (int, float)):
|
||
raise Exception('Expecting a numeric value for "interval"!')
|
||
if interval < 0:
|
||
raise Exception('The "interval" cannot be a negative number!')
|
||
interval_ms = float(interval) * 1000.0
|
||
show_notes_str = "false"
|
||
if show_notes:
|
||
show_notes_str = "true"
|
||
the_html = ""
|
||
for slide in self._presentation_slides[name]:
|
||
the_html += slide
|
||
the_html += (
|
||
"\n</div>\n"
|
||
"</div>\n"
|
||
'<script src="%s"></script>\n'
|
||
'<script src="%s"></script>\n'
|
||
"<script>Reveal.initialize("
|
||
"{showNotes: %s, slideNumber: true, progress: true, hash: false, "
|
||
"autoSlide: %s,});"
|
||
"</script>\n"
|
||
"</body>\n"
|
||
"</html>\n"
|
||
% (
|
||
constants.Reveal.MIN_JS,
|
||
constants.PrettifyJS.RUN_PRETTIFY_JS,
|
||
show_notes_str,
|
||
interval_ms,
|
||
)
|
||
)
|
||
|
||
# Remove duplicate ChartMaker library declarations
|
||
chart_libs = """
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
""" % (
|
||
constants.HighCharts.HC_JS,
|
||
constants.HighCharts.EXPORTING_JS,
|
||
constants.HighCharts.EXPORT_DATA_JS,
|
||
constants.HighCharts.ACCESSIBILITY_JS,
|
||
)
|
||
if the_html.count(chart_libs) > 1:
|
||
chart_libs_comment = "<!-- HighCharts Libraries Imported -->"
|
||
the_html = the_html.replace(chart_libs, chart_libs_comment)
|
||
# Only need to import the HighCharts libraries once
|
||
the_html = the_html.replace(chart_libs_comment, chart_libs, 1)
|
||
saved_presentations_folder = constants.Presentations.SAVED_FOLDER
|
||
if saved_presentations_folder.endswith("/"):
|
||
saved_presentations_folder = saved_presentations_folder[:-1]
|
||
if not os.path.exists(saved_presentations_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(saved_presentations_folder)
|
||
file_path = os.path.join(saved_presentations_folder, filename)
|
||
out_file = codecs.open(file_path, "w+", encoding="utf-8")
|
||
out_file.writelines(the_html)
|
||
out_file.close()
|
||
if self._output_file_saves:
|
||
print("\n>>> [%s] was saved!\n" % file_path)
|
||
return file_path
|
||
|
||
def begin_presentation(
|
||
self, name=None, filename=None, show_notes=False, interval=0
|
||
):
|
||
"""Begin a Reveal-JS Presentation in the web browser.
|
||
@Params
|
||
name - If creating multiple presentations at the same time,
|
||
use this to select the one you wish to use.
|
||
filename - The name of the HTML file that you wish to
|
||
save the presentation to. (filename must end in ".html")
|
||
show_notes - When set to True, the Notes feature becomes enabled,
|
||
which allows presenters to see notes next to slides.
|
||
interval - The delay time between autoplaying slides. (in seconds)
|
||
If set to 0 (default), autoplay is disabled."""
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
return # Presentations should not run in headless mode.
|
||
if not name:
|
||
name = "default"
|
||
if not filename:
|
||
filename = "my_presentation.html"
|
||
if name not in self._presentation_slides:
|
||
raise Exception("Presentation {%s} does not exist!" % name)
|
||
if not filename.endswith(".html"):
|
||
raise Exception('Presentation file must end in ".html"!')
|
||
if not interval:
|
||
interval = 0
|
||
if interval == 0 and self.interval:
|
||
interval = float(self.interval)
|
||
if not isinstance(interval, (int, float)):
|
||
raise Exception('Expecting a numeric value for "interval"!')
|
||
if interval < 0:
|
||
raise Exception('The "interval" cannot be a negative number!')
|
||
end_slide = (
|
||
'\n<section data-transition="none">\n'
|
||
'<p class="End_Presentation_Now"> </p>\n</section>\n'
|
||
)
|
||
self._presentation_slides[name].append(end_slide)
|
||
file_path = self.save_presentation(
|
||
name=name,
|
||
filename=filename,
|
||
show_notes=show_notes,
|
||
interval=interval,
|
||
)
|
||
self._presentation_slides[name].pop()
|
||
self.open_html_file(file_path)
|
||
presentation_folder = constants.Presentations.SAVED_FOLDER
|
||
with suppress(Exception):
|
||
while (
|
||
len(self.driver.window_handles) > 0
|
||
and presentation_folder in self.get_current_url()
|
||
):
|
||
time.sleep(0.05)
|
||
if self.is_element_visible(
|
||
"section.present p.End_Presentation_Now"
|
||
):
|
||
break
|
||
time.sleep(0.05)
|
||
|
||
############
|
||
|
||
def create_pie_chart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
data_name=None,
|
||
unit=None,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript pie chart using "HighCharts".
|
||
@Params
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one.
|
||
title - The title displayed for the chart.
|
||
subtitle - The subtitle displayed for the chart.
|
||
data_name - The series name. Useful for multi-series charts.
|
||
If no data_name, will default to using "Series 1".
|
||
unit - The description label given to the chart's y-axis values.
|
||
libs - The option to include Chart libraries (JS and CSS files).
|
||
Should be set to True (default) for the first time creating
|
||
a chart on a web page. If creating multiple charts on the
|
||
same web page, you won't need to re-import the libraries
|
||
when creating additional charts.
|
||
labels - If True, displays labels on the chart for data points.
|
||
legend - If True, displays the data point legend on the chart."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not data_name:
|
||
data_name = ""
|
||
style = "pie"
|
||
self.__create_highchart(
|
||
chart_name=chart_name,
|
||
title=title,
|
||
subtitle=subtitle,
|
||
style=style,
|
||
data_name=data_name,
|
||
unit=unit,
|
||
libs=libs,
|
||
labels=labels,
|
||
legend=legend,
|
||
)
|
||
|
||
def create_bar_chart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
data_name=None,
|
||
unit=None,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript bar chart using "HighCharts".
|
||
@Params
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one.
|
||
title - The title displayed for the chart.
|
||
subtitle - The subtitle displayed for the chart.
|
||
data_name - The series name. Useful for multi-series charts.
|
||
If no data_name, will default to using "Series 1".
|
||
unit - The description label given to the chart's y-axis values.
|
||
libs - The option to include Chart libraries (JS and CSS files).
|
||
Should be set to True (default) for the first time creating
|
||
a chart on a web page. If creating multiple charts on the
|
||
same web page, you won't need to re-import the libraries
|
||
when creating additional charts.
|
||
labels - If True, displays labels on the chart for data points.
|
||
legend - If True, displays the data point legend on the chart."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not data_name:
|
||
data_name = ""
|
||
style = "bar"
|
||
self.__create_highchart(
|
||
chart_name=chart_name,
|
||
title=title,
|
||
subtitle=subtitle,
|
||
style=style,
|
||
data_name=data_name,
|
||
unit=unit,
|
||
libs=libs,
|
||
labels=labels,
|
||
legend=legend,
|
||
)
|
||
|
||
def create_column_chart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
data_name=None,
|
||
unit=None,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript column chart using "HighCharts".
|
||
@Params
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one.
|
||
title - The title displayed for the chart.
|
||
subtitle - The subtitle displayed for the chart.
|
||
data_name - The series name. Useful for multi-series charts.
|
||
If no data_name, will default to using "Series 1".
|
||
unit - The description label given to the chart's y-axis values.
|
||
libs - The option to include Chart libraries (JS and CSS files).
|
||
Should be set to True (default) for the first time creating
|
||
a chart on a web page. If creating multiple charts on the
|
||
same web page, you won't need to re-import the libraries
|
||
when creating additional charts.
|
||
labels - If True, displays labels on the chart for data points.
|
||
legend - If True, displays the data point legend on the chart."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not data_name:
|
||
data_name = ""
|
||
style = "column"
|
||
self.__create_highchart(
|
||
chart_name=chart_name,
|
||
title=title,
|
||
subtitle=subtitle,
|
||
style=style,
|
||
data_name=data_name,
|
||
unit=unit,
|
||
libs=libs,
|
||
labels=labels,
|
||
legend=legend,
|
||
)
|
||
|
||
def create_line_chart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
data_name=None,
|
||
unit=None,
|
||
zero=False,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript line chart using "HighCharts".
|
||
@Params
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one.
|
||
title - The title displayed for the chart.
|
||
subtitle - The subtitle displayed for the chart.
|
||
data_name - The series name. Useful for multi-series charts.
|
||
If no data_name, will default to using "Series 1".
|
||
unit - The description label given to the chart's y-axis values.
|
||
zero - If True, the y-axis always starts at 0. (Default: False).
|
||
libs - The option to include Chart libraries (JS and CSS files).
|
||
Should be set to True (default) for the first time creating
|
||
a chart on a web page. If creating multiple charts on the
|
||
same web page, you won't need to re-import the libraries
|
||
when creating additional charts.
|
||
labels - If True, displays labels on the chart for data points.
|
||
legend - If True, displays the data point legend on the chart."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not data_name:
|
||
data_name = ""
|
||
style = "line"
|
||
self.__create_highchart(
|
||
chart_name=chart_name,
|
||
title=title,
|
||
subtitle=subtitle,
|
||
style=style,
|
||
data_name=data_name,
|
||
unit=unit,
|
||
zero=zero,
|
||
libs=libs,
|
||
labels=labels,
|
||
legend=legend,
|
||
)
|
||
|
||
def create_area_chart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
data_name=None,
|
||
unit=None,
|
||
zero=False,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript area chart using "HighCharts".
|
||
@Params
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one.
|
||
title - The title displayed for the chart.
|
||
subtitle - The subtitle displayed for the chart.
|
||
data_name - The series name. Useful for multi-series charts.
|
||
If no data_name, will default to using "Series 1".
|
||
unit - The description label given to the chart's y-axis values.
|
||
zero - If True, the y-axis always starts at 0. (Default: False).
|
||
libs - The option to include Chart libraries (JS and CSS files).
|
||
Should be set to True (default) for the first time creating
|
||
a chart on a web page. If creating multiple charts on the
|
||
same web page, you won't need to re-import the libraries
|
||
when creating additional charts.
|
||
labels - If True, displays labels on the chart for data points.
|
||
legend - If True, displays the data point legend on the chart."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not data_name:
|
||
data_name = ""
|
||
style = "area"
|
||
self.__create_highchart(
|
||
chart_name=chart_name,
|
||
title=title,
|
||
subtitle=subtitle,
|
||
style=style,
|
||
data_name=data_name,
|
||
unit=unit,
|
||
zero=zero,
|
||
libs=libs,
|
||
labels=labels,
|
||
legend=legend,
|
||
)
|
||
|
||
def __create_highchart(
|
||
self,
|
||
chart_name=None,
|
||
title=None,
|
||
subtitle=None,
|
||
style=None,
|
||
data_name=None,
|
||
unit=None,
|
||
zero=False,
|
||
libs=True,
|
||
labels=True,
|
||
legend=True,
|
||
):
|
||
"""Creates a JavaScript chart using the "HighCharts" library."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not title:
|
||
title = ""
|
||
if not subtitle:
|
||
subtitle = ""
|
||
if not style:
|
||
style = "pie"
|
||
if not data_name:
|
||
data_name = "Series 1"
|
||
if not unit:
|
||
unit = "Values"
|
||
if labels:
|
||
labels = "true"
|
||
else:
|
||
labels = "false"
|
||
if legend:
|
||
legend = "true"
|
||
else:
|
||
legend = "false"
|
||
title = title.replace("'", "\\'")
|
||
subtitle = subtitle.replace("'", "\\'")
|
||
unit = unit.replace("'", "\\'")
|
||
self._chart_count += 1
|
||
# If chart_libs format is changed, also change: save_presentation()
|
||
chart_libs = """
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
<script src="%s"></script>
|
||
""" % (
|
||
constants.HighCharts.HC_JS,
|
||
constants.HighCharts.EXPORTING_JS,
|
||
constants.HighCharts.EXPORT_DATA_JS,
|
||
constants.HighCharts.ACCESSIBILITY_JS,
|
||
)
|
||
if not libs:
|
||
chart_libs = ""
|
||
chart_css = """
|
||
<style>
|
||
.highcharts-figure, .highcharts-data-table table {
|
||
min-width: 320px;
|
||
max-width: 660px;
|
||
margin: 1em auto;
|
||
}
|
||
.highcharts-data-table table {
|
||
font-family: Verdana, sans-serif;
|
||
border-collapse: collapse;
|
||
border: 1px solid #EBEBEB;
|
||
margin: 10px auto;
|
||
text-align: center;
|
||
width: 100%;
|
||
max-width: 500px;
|
||
}
|
||
.highcharts-data-table caption {
|
||
padding: 1em 0;
|
||
font-size: 1.2em;
|
||
color: #555;
|
||
}
|
||
.highcharts-data-table th {
|
||
font-weight: 600;
|
||
padding: 0.5em;
|
||
}
|
||
.highcharts-data-table td, .highcharts-data-table th,
|
||
.highcharts-data-table caption {
|
||
padding: 0.5em;
|
||
}
|
||
.highcharts-data-table thead tr,
|
||
.highcharts-data-table tr:nth-child(even) {
|
||
background: #f8f8f8;
|
||
}
|
||
.highcharts-data-table tr:hover {
|
||
background: #f1f7ff;
|
||
}
|
||
</style>
|
||
"""
|
||
if not libs:
|
||
chart_css = ""
|
||
chart_description = ""
|
||
chart_figure = """
|
||
<figure class="highcharts-figure">
|
||
<div id="chartcontainer_num_%s"></div>
|
||
<p class="highcharts-description">%s</p>
|
||
</figure>
|
||
""" % (
|
||
self._chart_count,
|
||
chart_description,
|
||
)
|
||
min_zero = ""
|
||
if zero:
|
||
min_zero = "min: 0,"
|
||
chart_init_1 = """
|
||
<script>
|
||
// Build the chart
|
||
Highcharts.chart('chartcontainer_num_%s', {
|
||
credits: {
|
||
enabled: false
|
||
},
|
||
title: {
|
||
text: '%s'
|
||
},
|
||
subtitle: {
|
||
text: '%s'
|
||
},
|
||
xAxis: { },
|
||
yAxis: {
|
||
%s
|
||
title: {
|
||
text: '%s',
|
||
style: {
|
||
fontSize: '14px'
|
||
}
|
||
},
|
||
labels: {
|
||
useHTML: true,
|
||
style: {
|
||
fontSize: '14px'
|
||
}
|
||
}
|
||
},
|
||
chart: {
|
||
renderTo: 'statusChart',
|
||
plotBackgroundColor: null,
|
||
plotBorderWidth: null,
|
||
plotShadow: false,
|
||
type: '%s'
|
||
},
|
||
""" % (
|
||
self._chart_count,
|
||
title,
|
||
subtitle,
|
||
min_zero,
|
||
unit,
|
||
style,
|
||
)
|
||
# "{series.name}:"
|
||
point_format = (
|
||
r"<b>{point.y}</b><br />" r"<b>{point.percentage:.1f}%</b>"
|
||
)
|
||
if style != "pie":
|
||
point_format = r"<b>{point.y}</b>"
|
||
chart_init_2 = (
|
||
"""
|
||
tooltip: {
|
||
enabled: true,
|
||
useHTML: true,
|
||
style: {
|
||
padding: '6px',
|
||
fontSize: '14px'
|
||
},
|
||
backgroundColor: {
|
||
linearGradient: {
|
||
x1: 0,
|
||
y1: 0,
|
||
x2: 0,
|
||
y2: 1
|
||
},
|
||
stops: [
|
||
[0, 'rgba(255, 255, 255, 0.78)'],
|
||
[0.5, 'rgba(235, 235, 235, 0.76)'],
|
||
[1, 'rgba(244, 252, 255, 0.74)']
|
||
]
|
||
},
|
||
hideDelay: 40,
|
||
pointFormat: '%s'
|
||
},
|
||
"""
|
||
% point_format
|
||
)
|
||
chart_init_3 = """
|
||
accessibility: {
|
||
point: {
|
||
valueSuffix: '%%'
|
||
}
|
||
},
|
||
plotOptions: {
|
||
series: {
|
||
states: {
|
||
inactive: {
|
||
opacity: 0.85
|
||
}
|
||
}
|
||
},
|
||
pie: {
|
||
size: "95%%",
|
||
allowPointSelect: true,
|
||
animation: false,
|
||
cursor: 'pointer',
|
||
dataLabels: {
|
||
enabled: %s,
|
||
formatter: function() {
|
||
if (this.y > 0) {
|
||
return this.point.name + ': ' + this.point.y
|
||
}
|
||
}
|
||
},
|
||
states: {
|
||
hover: {
|
||
enabled: true
|
||
}
|
||
},
|
||
showInLegend: %s
|
||
}
|
||
},
|
||
""" % (
|
||
labels,
|
||
legend,
|
||
)
|
||
if style != "pie":
|
||
chart_init_3 = """
|
||
allowPointSelect: true,
|
||
cursor: 'pointer',
|
||
legend: {
|
||
layout: 'vertical',
|
||
align: 'right',
|
||
verticalAlign: 'middle'
|
||
},
|
||
states: {
|
||
hover: {
|
||
enabled: true
|
||
}
|
||
},
|
||
plotOptions: {
|
||
series: {
|
||
dataLabels: {
|
||
enabled: %s
|
||
},
|
||
showInLegend: %s,
|
||
animation: false,
|
||
shadow: false,
|
||
lineWidth: 3,
|
||
fillOpacity: 0.5,
|
||
marker: {
|
||
enabled: true
|
||
}
|
||
}
|
||
},
|
||
""" % (
|
||
labels,
|
||
legend,
|
||
)
|
||
chart_init = chart_init_1 + chart_init_2 + chart_init_3
|
||
color_by_point = "true"
|
||
if style != "pie":
|
||
color_by_point = "false"
|
||
series = """
|
||
series: [{
|
||
name: '%s',
|
||
colorByPoint: %s,
|
||
data: [
|
||
""" % (
|
||
data_name,
|
||
color_by_point,
|
||
)
|
||
new_chart = chart_libs + chart_css + chart_figure + chart_init + series
|
||
new_chart = textwrap.dedent(new_chart)
|
||
self._chart_data[chart_name] = []
|
||
self._chart_label[chart_name] = []
|
||
self._chart_data[chart_name].append(new_chart)
|
||
self._chart_first_series[chart_name] = True
|
||
self._chart_series_count[chart_name] = 1
|
||
|
||
def add_series_to_chart(self, data_name=None, chart_name=None):
|
||
"""Add a new data series to an existing chart.
|
||
This allows charts to have multiple data sets.
|
||
@Params
|
||
data_name - Set the series name. Useful for multi-series charts.
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
self._chart_series_count[chart_name] += 1
|
||
if not data_name:
|
||
data_name = "Series %s" % self._chart_series_count[chart_name]
|
||
series = (
|
||
"""
|
||
]
|
||
},
|
||
{
|
||
name: '%s',
|
||
colorByPoint: false,
|
||
data: [
|
||
"""
|
||
% data_name
|
||
)
|
||
self._chart_data[chart_name].append(series)
|
||
self._chart_first_series[chart_name] = False
|
||
|
||
def add_data_point(self, label, value, color=None, chart_name=None):
|
||
"""Add a data point to a SeleniumBase-generated chart.
|
||
@Params
|
||
label - The label name for the data point.
|
||
value - The numeric value of the data point.
|
||
color - The HTML color of the data point.
|
||
Can be an RGB color. Eg: "#55ACDC".
|
||
Can also be a named color. Eg: "Teal".
|
||
chart_name - If creating multiple charts,
|
||
use this to select which one."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if chart_name not in self._chart_data:
|
||
# Create a chart if it doesn't already exist
|
||
self.create_pie_chart(chart_name=chart_name)
|
||
if not value:
|
||
value = 0
|
||
if not isinstance(value, (int, float)):
|
||
raise Exception('Expecting a numeric value for "value"!')
|
||
if not color:
|
||
color = ""
|
||
else:
|
||
color = color.replace("'", "\\'")
|
||
label = label.replace("'", "\\'")
|
||
if color:
|
||
data_point = """
|
||
{
|
||
name: '%s',
|
||
y: %s,
|
||
color: '%s'
|
||
},
|
||
""" % (
|
||
label,
|
||
value,
|
||
color,
|
||
)
|
||
else:
|
||
data_point = """
|
||
{
|
||
name: '%s',
|
||
y: %s,
|
||
},
|
||
""" % (
|
||
label,
|
||
value,
|
||
)
|
||
data_point = textwrap.dedent(data_point)
|
||
self._chart_data[chart_name].append(data_point)
|
||
if self._chart_first_series[chart_name]:
|
||
self._chart_label[chart_name].append(label)
|
||
|
||
def save_chart(self, chart_name=None, filename=None, folder=None):
|
||
"""Saves a SeleniumBase-generated chart to a file for later use.
|
||
@Params
|
||
chart_name - If creating multiple charts at the same time,
|
||
use this to select the one you wish to use.
|
||
filename - The name of the HTML file that you wish to
|
||
save the chart to. (filename must end in ".html")
|
||
folder - The name of the folder where you wish to
|
||
save the HTML file. (Default: "./saved_charts/")"""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not filename:
|
||
filename = "my_chart.html"
|
||
if chart_name not in self._chart_data:
|
||
raise Exception("Chart {%s} does not exist!" % chart_name)
|
||
if not filename.endswith(".html"):
|
||
raise Exception('Chart file must end in ".html"!')
|
||
the_html = '<meta charset="utf-8">\n'
|
||
the_html += '<meta http-equiv="Content-Type" content="text/html">\n'
|
||
the_html += '<meta name="viewport" content="shrink-to-fit=no">\n'
|
||
for chart_data_point in self._chart_data[chart_name]:
|
||
the_html += chart_data_point
|
||
the_html += """
|
||
]
|
||
}]
|
||
});
|
||
</script>
|
||
"""
|
||
axis = "xAxis: {\n"
|
||
axis += " labels: {\n"
|
||
axis += " useHTML: true,\n"
|
||
axis += " style: {\n"
|
||
axis += " fontSize: '14px',\n"
|
||
axis += " },\n"
|
||
axis += " },\n"
|
||
axis += "categories: ["
|
||
for label in self._chart_label[chart_name]:
|
||
axis += "'%s'," % label
|
||
axis += "], crosshair: false},"
|
||
the_html = the_html.replace("xAxis: { },", axis)
|
||
if not folder:
|
||
saved_charts_folder = constants.Charts.SAVED_FOLDER
|
||
else:
|
||
saved_charts_folder = folder
|
||
if saved_charts_folder.endswith("/"):
|
||
saved_charts_folder = saved_charts_folder[:-1]
|
||
if not os.path.exists(saved_charts_folder):
|
||
with suppress(Exception):
|
||
os.makedirs(saved_charts_folder)
|
||
file_path = os.path.join(saved_charts_folder, filename)
|
||
out_file = codecs.open(file_path, "w+", encoding="utf-8")
|
||
out_file.writelines(the_html)
|
||
out_file.close()
|
||
if self._output_file_saves:
|
||
print("\n>>> [%s] was saved!" % file_path)
|
||
return file_path
|
||
|
||
def display_chart(self, chart_name=None, filename=None, interval=0):
|
||
"""Displays a SeleniumBase-generated chart in the browser window.
|
||
@Params
|
||
chart_name - If creating multiple charts at the same time,
|
||
use this to select the one you wish to use.
|
||
filename - The name of the HTML file that you wish to
|
||
save the chart to. (filename must end in ".html")
|
||
interval - The delay time for auto-advancing charts. (in seconds)
|
||
If set to 0 (default), auto-advancing is disabled."""
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
interval = 1 # Race through chart if running in headless mode
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if not filename:
|
||
filename = "my_chart.html"
|
||
if not interval:
|
||
interval = 0
|
||
if interval == 0 and self.interval:
|
||
interval = float(self.interval)
|
||
if not isinstance(interval, (int, float)):
|
||
raise Exception('Expecting a numeric value for "interval"!')
|
||
if interval < 0:
|
||
raise Exception('The "interval" cannot be a negative number!')
|
||
if chart_name not in self._chart_data:
|
||
raise Exception("Chart {%s} does not exist!" % chart_name)
|
||
if not filename.endswith(".html"):
|
||
raise Exception('Chart file must end in ".html"!')
|
||
file_path = self.save_chart(chart_name=chart_name, filename=filename)
|
||
self.open_html_file(file_path)
|
||
chart_folder = constants.Charts.SAVED_FOLDER
|
||
if interval == 0:
|
||
with suppress(Exception):
|
||
print("\n*** Close the browser window to continue ***")
|
||
# Will also continue if manually navigating to a new page
|
||
while len(self.driver.window_handles) > 0 and (
|
||
chart_folder in self.get_current_url()
|
||
):
|
||
time.sleep(0.05)
|
||
else:
|
||
with suppress(Exception):
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (interval * 1000.0)
|
||
for x in range(int(interval * 10)):
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
if len(self.driver.window_handles) == 0:
|
||
break
|
||
if chart_folder not in self.get_current_url():
|
||
break
|
||
time.sleep(0.1)
|
||
|
||
def extract_chart(self, chart_name=None):
|
||
"""Extracts the HTML from a SeleniumBase-generated chart.
|
||
@Params
|
||
chart_name - If creating multiple charts at the same time,
|
||
use this to select the one you wish to use."""
|
||
if not chart_name:
|
||
chart_name = "default"
|
||
if chart_name not in self._chart_data:
|
||
raise Exception("Chart {%s} does not exist!" % chart_name)
|
||
the_html = ""
|
||
for chart_data_point in self._chart_data[chart_name]:
|
||
the_html += chart_data_point
|
||
the_html += """
|
||
]
|
||
}]
|
||
});
|
||
</script>
|
||
"""
|
||
axis = "xAxis: {\n"
|
||
axis += " labels: {\n"
|
||
axis += " useHTML: true,\n"
|
||
axis += " style: {\n"
|
||
axis += " fontSize: '14px',\n"
|
||
axis += " },\n"
|
||
axis += " },\n"
|
||
axis += "categories: ["
|
||
for label in self._chart_label[chart_name]:
|
||
axis += "'%s'," % label
|
||
axis += "], crosshair: false},"
|
||
the_html = the_html.replace("xAxis: { },", axis)
|
||
self._chart_xcount += 1
|
||
the_html = the_html.replace(
|
||
"chartcontainer_num_", "chartcontainer_%s_" % self._chart_xcount
|
||
)
|
||
return the_html
|
||
|
||
############
|
||
|
||
def create_tour(self, name=None, theme=None):
|
||
"""Creates a guided tour for any website.
|
||
The default theme is the IntroJS Library.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
theme - Sets the default theme for the website tour. Available themes:
|
||
"Bootstrap", "DriverJS", "Hopscotch", "IntroJS", "Shepherd".
|
||
The "Shepherd" library includes variations: "light"/"arrows",
|
||
"dark", "default", "square", and "square-dark"."""
|
||
if not name:
|
||
name = "default"
|
||
if theme:
|
||
if theme.lower() == "bootstrap":
|
||
self.create_bootstrap_tour(name)
|
||
elif theme.lower() == "hopscotch":
|
||
self.create_hopscotch_tour(name)
|
||
elif theme.lower() == "intro":
|
||
self.create_introjs_tour(name)
|
||
elif theme.lower() == "introjs":
|
||
self.create_introjs_tour(name)
|
||
elif theme.lower() == "driver":
|
||
self.create_driverjs_tour(name)
|
||
elif theme.lower() == "driverjs":
|
||
self.create_driverjs_tour(name)
|
||
elif theme.lower() == "shepherd":
|
||
self.create_shepherd_tour(name, theme="light")
|
||
elif theme.lower() == "light":
|
||
self.create_shepherd_tour(name, theme="light")
|
||
elif theme.lower() == "arrows":
|
||
self.create_shepherd_tour(name, theme="light")
|
||
elif theme.lower() == "dark":
|
||
self.create_shepherd_tour(name, theme="dark")
|
||
elif theme.lower() == "square":
|
||
self.create_shepherd_tour(name, theme="square")
|
||
elif theme.lower() == "square-dark":
|
||
self.create_shepherd_tour(name, theme="square-dark")
|
||
elif theme.lower() == "default":
|
||
self.create_shepherd_tour(name, theme="default")
|
||
else:
|
||
self.create_introjs_tour(name)
|
||
else:
|
||
self.create_introjs_tour(name)
|
||
|
||
def create_shepherd_tour(self, name=None, theme=None):
|
||
"""Creates a Shepherd JS website tour.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
theme - Sets the default theme for the tour.
|
||
Choose from "light"/"arrows", "dark", "default", "square",
|
||
and "square-dark". ("light" is used if None is selected.)"""
|
||
shepherd_theme = "shepherd-theme-arrows"
|
||
if theme:
|
||
if theme.lower() == "default":
|
||
shepherd_theme = "shepherd-theme-default"
|
||
elif theme.lower() == "dark":
|
||
shepherd_theme = "shepherd-theme-dark"
|
||
elif theme.lower() == "light":
|
||
shepherd_theme = "shepherd-theme-arrows"
|
||
elif theme.lower() == "arrows":
|
||
shepherd_theme = "shepherd-theme-arrows"
|
||
elif theme.lower() == "square":
|
||
shepherd_theme = "shepherd-theme-square"
|
||
elif theme.lower() == "square-dark":
|
||
shepherd_theme = "shepherd-theme-square-dark"
|
||
if not name:
|
||
name = "default"
|
||
new_tour = (
|
||
"""
|
||
// Shepherd Tour
|
||
var tour = new Shepherd.Tour({
|
||
defaults: {
|
||
classes: '%s',
|
||
scrollTo: true
|
||
}
|
||
});
|
||
var allButtons = {
|
||
skip: {
|
||
text: "Skip",
|
||
action: tour.cancel,
|
||
classes: 'shepherd-button-secondary tour-button-left'
|
||
},
|
||
back: {
|
||
text: "Back",
|
||
action: tour.back,
|
||
classes: 'shepherd-button-secondary'
|
||
},
|
||
next: {
|
||
text: "Next",
|
||
action: tour.next,
|
||
classes: 'shepherd-button-primary tour-button-right'
|
||
},
|
||
};
|
||
var firstStepButtons = [allButtons.skip, allButtons.next];
|
||
var midTourButtons = [allButtons.back, allButtons.next];
|
||
"""
|
||
% shepherd_theme
|
||
)
|
||
self._tour_steps[name] = []
|
||
self._tour_steps[name].append(new_tour)
|
||
|
||
def create_bootstrap_tour(self, name=None):
|
||
"""Creates a Bootstrap tour for a website.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to."""
|
||
if not name:
|
||
name = "default"
|
||
new_tour = """
|
||
// Bootstrap Tour
|
||
var tour = new Tour({
|
||
container: 'body',
|
||
animation: true,
|
||
keyboard: true,
|
||
orphan: true,
|
||
smartPlacement: true,
|
||
autoscroll: true,
|
||
backdrop: true,
|
||
backdropContainer: 'body',
|
||
backdropPadding: 3,
|
||
});
|
||
tour.addSteps([
|
||
"""
|
||
self._tour_steps[name] = []
|
||
self._tour_steps[name].append(new_tour)
|
||
|
||
def create_driverjs_tour(self, name=None):
|
||
"""Creates a DriverJS tour for a website.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to."""
|
||
if not name:
|
||
name = "default"
|
||
new_tour = """
|
||
// DriverJS Tour
|
||
var tour = new Driver({
|
||
opacity: 0.24, // Background opacity (0: no popover / overlay)
|
||
padding: 6, // Distance of element from around the edges
|
||
allowClose: false, // Whether clicking on overlay should close
|
||
overlayClickNext: false, // Move to next step on overlay click
|
||
doneBtnText: 'Done', // Text that appears on the Done button
|
||
closeBtnText: 'Close', // Text appearing on the Close button
|
||
nextBtnText: 'Next', // Text that appears on the Next button
|
||
prevBtnText: 'Previous', // Text appearing on Previous button
|
||
showButtons: true, // This shows control buttons in the footer
|
||
keyboardControl: true, // (escape to close, arrow keys to move)
|
||
animate: true, // Animate while changing highlighted element
|
||
});
|
||
tour.defineSteps([
|
||
"""
|
||
self._tour_steps[name] = []
|
||
self._tour_steps[name].append(new_tour)
|
||
|
||
def create_hopscotch_tour(self, name=None):
|
||
"""Creates a Hopscotch tour for a website.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to."""
|
||
if not name:
|
||
name = "default"
|
||
|
||
new_tour = """
|
||
// Hopscotch Tour
|
||
var tour = {
|
||
id: "hopscotch_tour",
|
||
steps: [
|
||
"""
|
||
|
||
self._tour_steps[name] = []
|
||
self._tour_steps[name].append(new_tour)
|
||
|
||
def create_introjs_tour(self, name=None):
|
||
"""Creates an IntroJS tour for a website.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to."""
|
||
if not hasattr(sb_config, "introjs_theme_color"):
|
||
sb_config.introjs_theme_color = constants.TourColor.theme_color
|
||
if not hasattr(sb_config, "introjs_hover_color"):
|
||
sb_config.introjs_hover_color = constants.TourColor.hover_color
|
||
if not name:
|
||
name = "default"
|
||
new_tour = """
|
||
// IntroJS Tour
|
||
function startIntro(){
|
||
var intro = introJs();
|
||
intro.setOptions({
|
||
steps: [
|
||
"""
|
||
self._tour_steps[name] = []
|
||
self._tour_steps[name].append(new_tour)
|
||
|
||
def set_introjs_colors(self, theme_color=None, hover_color=None):
|
||
"""Use this method to set the theme colors for IntroJS tours.
|
||
Args must be hex color values that start with a "#" sign.
|
||
If a color isn't specified, the color will reset to the default.
|
||
The border color of buttons is set to the hover color.
|
||
@Params
|
||
theme_color - The color of buttons.
|
||
hover_color - The color of buttons after hovering over them."""
|
||
if not hasattr(sb_config, "introjs_theme_color"):
|
||
sb_config.introjs_theme_color = constants.TourColor.theme_color
|
||
if not hasattr(sb_config, "introjs_hover_color"):
|
||
sb_config.introjs_hover_color = constants.TourColor.hover_color
|
||
if theme_color:
|
||
match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", theme_color)
|
||
if not match:
|
||
raise Exception(
|
||
'Expecting a hex value color that starts with "#"!'
|
||
)
|
||
sb_config.introjs_theme_color = theme_color
|
||
else:
|
||
sb_config.introjs_theme_color = constants.TourColor.theme_color
|
||
if hover_color:
|
||
match = re.search(r"^#(?:[0-9a-fA-F]{3}){1,2}$", hover_color)
|
||
if not match:
|
||
raise Exception(
|
||
'Expecting a hex value color that starts with "#"!'
|
||
)
|
||
sb_config.introjs_hover_color = hover_color
|
||
else:
|
||
sb_config.introjs_hover_color = constants.TourColor.hover_color
|
||
|
||
def add_tour_step(
|
||
self,
|
||
message,
|
||
selector=None,
|
||
name=None,
|
||
title=None,
|
||
theme=None,
|
||
alignment=None,
|
||
duration=None,
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
theme - (Shepherd Tours ONLY) The styling of the tour step.
|
||
Choose from "light"/"arrows", "dark", "default", "square",
|
||
and "square-dark". ("arrows" is used if None is selected.)
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("top" is default, except for Hopscotch and DriverJS).
|
||
duration - (Bootstrap Tours ONLY) The amount of time, in seconds,
|
||
before automatically advancing to the next tour step."""
|
||
if not selector:
|
||
selector = "html"
|
||
if page_utils.is_name_selector(selector):
|
||
name = page_utils.get_name_from_selector(selector)
|
||
selector = '[name="%s"]' % name
|
||
if page_utils.is_xpath_selector(selector):
|
||
selector = self.convert_to_css_selector(selector, By.XPATH)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
if not name:
|
||
name = "default"
|
||
if name not in self._tour_steps:
|
||
# By default, will create an IntroJS tour if no tours exist
|
||
self.create_tour(name=name, theme="introjs")
|
||
if not title:
|
||
title = ""
|
||
title = self.__escape_quotes_if_needed(title)
|
||
if message:
|
||
message = self.__escape_quotes_if_needed(message)
|
||
else:
|
||
message = ""
|
||
if not alignment or alignment not in [
|
||
"top",
|
||
"bottom",
|
||
"left",
|
||
"right",
|
||
]:
|
||
t_name = self._tour_steps[name][0]
|
||
if "Hopscotch" not in t_name and "DriverJS" not in t_name:
|
||
alignment = "top"
|
||
else:
|
||
alignment = "bottom"
|
||
if "Bootstrap" in self._tour_steps[name][0]:
|
||
self.__add_bootstrap_tour_step(
|
||
message,
|
||
selector=selector,
|
||
name=name,
|
||
title=title,
|
||
alignment=alignment,
|
||
duration=duration,
|
||
)
|
||
elif "DriverJS" in self._tour_steps[name][0]:
|
||
self.__add_driverjs_tour_step(
|
||
message,
|
||
selector=selector,
|
||
name=name,
|
||
title=title,
|
||
alignment=alignment,
|
||
)
|
||
elif "Hopscotch" in self._tour_steps[name][0]:
|
||
self.__add_hopscotch_tour_step(
|
||
message,
|
||
selector=selector,
|
||
name=name,
|
||
title=title,
|
||
alignment=alignment,
|
||
)
|
||
elif "IntroJS" in self._tour_steps[name][0]:
|
||
self.__add_introjs_tour_step(
|
||
message,
|
||
selector=selector,
|
||
name=name,
|
||
title=title,
|
||
alignment=alignment,
|
||
)
|
||
else:
|
||
self.__add_shepherd_tour_step(
|
||
message,
|
||
selector=selector,
|
||
name=name,
|
||
title=title,
|
||
theme=theme,
|
||
alignment=alignment,
|
||
)
|
||
|
||
def __add_shepherd_tour_step(
|
||
self,
|
||
message,
|
||
selector=None,
|
||
name=None,
|
||
title=None,
|
||
theme=None,
|
||
alignment=None,
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
theme - (Shepherd Tours ONLY) The styling of the tour step.
|
||
Choose from "light"/"arrows", "dark", "default", "square",
|
||
and "square-dark". ("arrows" is used if None is selected.)
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("top" is the default alignment)."""
|
||
if theme == "default":
|
||
shepherd_theme = "shepherd-theme-default"
|
||
elif theme == "dark":
|
||
shepherd_theme = "shepherd-theme-dark"
|
||
elif theme == "light":
|
||
shepherd_theme = "shepherd-theme-arrows"
|
||
elif theme == "arrows":
|
||
shepherd_theme = "shepherd-theme-arrows"
|
||
elif theme == "square":
|
||
shepherd_theme = "shepherd-theme-square"
|
||
elif theme == "square-dark":
|
||
shepherd_theme = "shepherd-theme-square-dark"
|
||
else:
|
||
shepherd_base_theme = re.search(
|
||
r"[\S\s]+classes: '([\S\s]+)',[\S\s]+",
|
||
self._tour_steps[name][0],
|
||
).group(1)
|
||
shepherd_theme = shepherd_base_theme
|
||
shepherd_classes = shepherd_theme
|
||
if selector == "html":
|
||
shepherd_classes += " shepherd-orphan"
|
||
buttons = "firstStepButtons"
|
||
if len(self._tour_steps[name]) > 1:
|
||
buttons = "midTourButtons"
|
||
step = """tour.addStep('%s', {
|
||
title: '%s',
|
||
classes: '%s',
|
||
text: '%s',
|
||
attachTo: {element: '%s', on: '%s'},
|
||
buttons: %s,
|
||
advanceOn: '.docs-link click'
|
||
});""" % (
|
||
name,
|
||
title,
|
||
shepherd_classes,
|
||
message,
|
||
selector,
|
||
alignment,
|
||
buttons,
|
||
)
|
||
self._tour_steps[name].append(step)
|
||
|
||
def __add_bootstrap_tour_step(
|
||
self,
|
||
message,
|
||
selector=None,
|
||
name=None,
|
||
title=None,
|
||
alignment=None,
|
||
duration=None,
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("top" is the default alignment).
|
||
duration - (Bootstrap Tours ONLY) The amount of time, in seconds,
|
||
before automatically advancing to the next tour step."""
|
||
if selector != "html":
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
element_row = "element: '%s'," % selector
|
||
else:
|
||
element_row = ""
|
||
if not duration:
|
||
duration = "0"
|
||
else:
|
||
duration = str(float(duration) * 1000.0)
|
||
bd = "backdrop: true,"
|
||
if selector == "html":
|
||
bd = "backdrop: false,"
|
||
step = """{
|
||
%s
|
||
title: '%s',
|
||
content: '%s',
|
||
orphan: true,
|
||
autoscroll: true,
|
||
%s
|
||
placement: '%s',
|
||
smartPlacement: true,
|
||
duration: %s,
|
||
},""" % (
|
||
element_row,
|
||
title,
|
||
message,
|
||
bd,
|
||
alignment,
|
||
duration,
|
||
)
|
||
self._tour_steps[name].append(step)
|
||
|
||
def __add_driverjs_tour_step(
|
||
self, message, selector=None, name=None, title=None, alignment=None
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("top" is the default alignment)."""
|
||
message = (
|
||
'<font size="3" color="#33477B"><b>' + message + "</b></font>"
|
||
)
|
||
title_row = ""
|
||
if not title:
|
||
title_row = "title: '%s'," % message
|
||
message = ""
|
||
else:
|
||
title_row = "title: '%s'," % title
|
||
align_row = "position: '%s'," % alignment
|
||
ani_row = "animate: true,"
|
||
if not selector or selector == "html" or selector == "body":
|
||
selector = "body"
|
||
ani_row = "animate: false,"
|
||
align_row = "position: '%s'," % "mid-center"
|
||
element_row = "element: '%s'," % selector
|
||
desc_row = "description: '%s'," % message
|
||
step = """{
|
||
%s
|
||
%s
|
||
popover: {
|
||
className: 'popover-class',
|
||
%s
|
||
%s
|
||
%s
|
||
}
|
||
},""" % (
|
||
element_row,
|
||
ani_row,
|
||
title_row,
|
||
desc_row,
|
||
align_row,
|
||
)
|
||
self._tour_steps[name].append(step)
|
||
|
||
def __add_hopscotch_tour_step(
|
||
self, message, selector=None, name=None, title=None, alignment=None
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("bottom" is the default alignment)."""
|
||
arrow_offset_row = None
|
||
if not selector or selector == "html":
|
||
selector = "head"
|
||
alignment = "bottom"
|
||
arrow_offset_row = "arrowOffset: '200',"
|
||
else:
|
||
arrow_offset_row = ""
|
||
step = """{
|
||
target: '%s',
|
||
title: '%s',
|
||
content: '%s',
|
||
%s
|
||
showPrevButton: 'true',
|
||
scrollDuration: '550',
|
||
placement: '%s'},
|
||
""" % (
|
||
selector,
|
||
title,
|
||
message,
|
||
arrow_offset_row,
|
||
alignment,
|
||
)
|
||
self._tour_steps[name].append(step)
|
||
|
||
def __add_introjs_tour_step(
|
||
self, message, selector=None, name=None, title=None, alignment=None
|
||
):
|
||
"""Allows the user to add tour steps for a website.
|
||
@Params
|
||
message - The message to display.
|
||
selector - The CSS Selector of the Element to attach to.
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
title - Additional header text that appears above the message.
|
||
alignment - Choose from "top", "bottom", "left", and "right".
|
||
("top" is the default alignment)."""
|
||
if selector != "html":
|
||
element_row = "element: '%s'," % selector
|
||
else:
|
||
element_row = ""
|
||
if title:
|
||
message = "<center><b>" + title + "</b></center><hr>" + message
|
||
message = '<font size="3" color="#33477B">' + message + "</font>"
|
||
step = """{%s
|
||
intro: '%s',
|
||
position: '%s'},""" % (
|
||
element_row,
|
||
message,
|
||
alignment,
|
||
)
|
||
self._tour_steps[name].append(step)
|
||
|
||
def play_tour(self, name=None, interval=0):
|
||
"""Plays a tour on the current website.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
interval - The delay time between autoplaying tour steps. (Seconds)
|
||
If set to 0 (default), the tour is fully manual control."""
|
||
from seleniumbase.core import tour_helper
|
||
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
return # Tours should not run in headless mode.
|
||
self.wait_for_ready_state_complete()
|
||
if not interval:
|
||
interval = 0
|
||
if interval == 0 and self.interval:
|
||
interval = float(self.interval)
|
||
if not name:
|
||
name = "default"
|
||
if name not in self._tour_steps:
|
||
raise Exception("Tour {%s} does not exist!" % name)
|
||
if "Bootstrap" in self._tour_steps[name][0]:
|
||
tour_helper.play_bootstrap_tour(
|
||
self.driver,
|
||
self._tour_steps,
|
||
self.browser,
|
||
self.message_duration,
|
||
name=name,
|
||
interval=interval,
|
||
)
|
||
elif "DriverJS" in self._tour_steps[name][0]:
|
||
tour_helper.play_driverjs_tour(
|
||
self.driver,
|
||
self._tour_steps,
|
||
self.browser,
|
||
self.message_duration,
|
||
name=name,
|
||
interval=interval,
|
||
)
|
||
elif "Hopscotch" in self._tour_steps[name][0]:
|
||
tour_helper.play_hopscotch_tour(
|
||
self.driver,
|
||
self._tour_steps,
|
||
self.browser,
|
||
self.message_duration,
|
||
name=name,
|
||
interval=interval,
|
||
)
|
||
elif "IntroJS" in self._tour_steps[name][0]:
|
||
tour_helper.play_introjs_tour(
|
||
self.driver,
|
||
self._tour_steps,
|
||
self.browser,
|
||
self.message_duration,
|
||
name=name,
|
||
interval=interval,
|
||
)
|
||
else:
|
||
tour_helper.play_shepherd_tour(
|
||
self.driver,
|
||
self._tour_steps,
|
||
self.message_duration,
|
||
name=name,
|
||
interval=interval,
|
||
)
|
||
|
||
def start_tour(self, name=None, interval=0):
|
||
"""Same as self.play_tour()"""
|
||
self.play_tour(name=name, interval=interval)
|
||
|
||
def export_tour(self, name=None, filename="my_tour.js", url=None):
|
||
"""Exports a tour as a JS file.
|
||
You can call self.export_tour() anywhere where you would
|
||
normally use self.play_tour() to play a website tour.
|
||
It will include necessary resources as well, such as jQuery.
|
||
You'll be able to copy the tour directly into the Console of
|
||
any web browser to play the tour outside of SeleniumBase runs.
|
||
@Params
|
||
name - If creating multiple tours at the same time,
|
||
use this to select the tour you wish to add steps to.
|
||
filename - The name of the JavaScript file that you wish to
|
||
save the tour to.
|
||
url - The URL where the tour starts. If not specified, the URL
|
||
of the current page will be used."""
|
||
from seleniumbase.core import tour_helper
|
||
|
||
if not url:
|
||
url = self.get_current_url()
|
||
tour_helper.export_tour(
|
||
self._tour_steps, name=name, filename=filename, url=url
|
||
)
|
||
|
||
############
|
||
|
||
def activate_jquery_confirm(self):
|
||
"""See https://craftpip.github.io/jquery-confirm/ for usage."""
|
||
self.__check_scope()
|
||
self._check_browser()
|
||
js_utils.activate_jquery_confirm(self.driver)
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def set_jqc_theme(self, theme, color=None, width=None):
|
||
"""Sets the default jquery-confirm theme and width (optional).
|
||
Available themes: "bootstrap", "modern", "material", "supervan",
|
||
"light", "dark", and "seamless".
|
||
Available colors: (This sets the BORDER color, NOT the button color.)
|
||
"blue", "default", "green", "red", "purple", "orange", "dark".
|
||
Width can be set using percent or pixels. Eg: "36.0%", "450px"."""
|
||
if not self.__changed_jqc_theme:
|
||
self.__jqc_default_theme = constants.JqueryConfirm.DEFAULT_THEME
|
||
self.__jqc_default_color = constants.JqueryConfirm.DEFAULT_COLOR
|
||
self.__jqc_default_width = constants.JqueryConfirm.DEFAULT_WIDTH
|
||
valid_themes = [
|
||
"bootstrap",
|
||
"modern",
|
||
"material",
|
||
"supervan",
|
||
"light",
|
||
"dark",
|
||
"seamless",
|
||
]
|
||
if theme.lower() not in valid_themes:
|
||
raise Exception(
|
||
"%s is not a valid jquery-confirm theme! "
|
||
"Select from %s" % (theme.lower(), valid_themes)
|
||
)
|
||
constants.JqueryConfirm.DEFAULT_THEME = theme.lower()
|
||
if color:
|
||
valid_colors = [
|
||
"blue",
|
||
"default",
|
||
"green",
|
||
"red",
|
||
"purple",
|
||
"orange",
|
||
"dark",
|
||
]
|
||
if color.lower() not in valid_colors:
|
||
raise Exception(
|
||
"%s is not a valid jquery-confirm border color! "
|
||
"Select from %s" % (color.lower(), valid_colors)
|
||
)
|
||
constants.JqueryConfirm.DEFAULT_COLOR = color.lower()
|
||
if width:
|
||
if isinstance(width, (int, float)):
|
||
# Convert to a string if a number is given
|
||
width = str(width)
|
||
if width.isnumeric():
|
||
if int(width) <= 0:
|
||
raise Exception("Width must be set to a positive number!")
|
||
elif int(width) <= 100:
|
||
width = str(width) + "%"
|
||
else:
|
||
width = str(width) + "px" # Use pixels if width is > 100
|
||
if not width.endswith("%") and not width.endswith("px"):
|
||
raise Exception(
|
||
"jqc width must end with %% for percent or px for pixels!"
|
||
)
|
||
value = None
|
||
if width.endswith("%"):
|
||
value = width[:-1]
|
||
if width.endswith("px"):
|
||
value = width[:-2]
|
||
try:
|
||
value = float(value)
|
||
except Exception:
|
||
raise Exception("%s is not a numeric value!" % value)
|
||
if value <= 0:
|
||
raise Exception("%s is not a positive number!" % value)
|
||
constants.JqueryConfirm.DEFAULT_WIDTH = width
|
||
|
||
def reset_jqc_theme(self):
|
||
"""Resets the jqc theme settings to factory defaults."""
|
||
if self.__changed_jqc_theme:
|
||
constants.JqueryConfirm.DEFAULT_THEME = self.__jqc_default_theme
|
||
constants.JqueryConfirm.DEFAULT_COLOR = self.__jqc_default_color
|
||
constants.JqueryConfirm.DEFAULT_WIDTH = self.__jqc_default_width
|
||
self.__changed_jqc_theme = False
|
||
|
||
def get_jqc_button_input(self, message, buttons, options=None):
|
||
"""
|
||
Pop up a jquery-confirm box and return the text of the button clicked.
|
||
If running in headless mode, the last button text is returned.
|
||
@Params
|
||
message: The message to display in the jquery-confirm dialog.
|
||
buttons: A list of tuples for text and color.
|
||
Example: [("Yes!", "green"), ("No!", "red")]
|
||
Available colors: blue, green, red, orange, purple, default, dark.
|
||
A simple text string also works: "My Button". (Uses default color.)
|
||
options: A list of tuples for options to set.
|
||
Example: [("theme", "bootstrap"), ("width", "450px")]
|
||
Available theme options: bootstrap, modern, material, supervan,
|
||
light, dark, and seamless.
|
||
Available colors: (For the BORDER color, NOT the button color.)
|
||
"blue", "default", "green", "red", "purple", "orange", "dark".
|
||
Example option for changing the border color: ("color", "default")
|
||
Width can be set using percent or pixels. Eg: "36.0%", "450px"."""
|
||
from seleniumbase.core import jqc_helper
|
||
|
||
if message and not isinstance(message, str):
|
||
raise Exception('Expecting a string for arg: "message"!')
|
||
if not isinstance(buttons, (list, tuple)):
|
||
raise Exception('Expecting a list or tuple for arg: "button"!')
|
||
if len(buttons) < 1:
|
||
raise Exception('List "buttons" requires at least one button!')
|
||
new_buttons = []
|
||
for button in buttons:
|
||
if isinstance(button, (list, tuple)) and (len(button) == 1):
|
||
new_buttons.append(button[0])
|
||
elif isinstance(button, (list, tuple)) and (len(button) > 1):
|
||
new_buttons.append((button[0], str(button[1]).lower()))
|
||
else:
|
||
new_buttons.append((str(button), ""))
|
||
buttons = new_buttons
|
||
if options:
|
||
for option in options:
|
||
if not isinstance(option, (list, tuple)):
|
||
raise Exception('"options" should be a list of tuples!')
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
return buttons[-1][0]
|
||
jqc_helper.jquery_confirm_button_dialog(
|
||
self.driver, message, buttons, options
|
||
)
|
||
time.sleep(0.02)
|
||
jf = "document.querySelector('.jconfirm-box').focus();"
|
||
with suppress(Exception):
|
||
self.execute_script(jf)
|
||
waiting_for_response = True
|
||
while waiting_for_response:
|
||
time.sleep(0.05)
|
||
jqc_open = self.execute_script("return jconfirm.instances.length;")
|
||
if str(jqc_open) == "0":
|
||
break
|
||
time.sleep(0.1)
|
||
status = None
|
||
try:
|
||
status = self.execute_script("return $jqc_status;")
|
||
except Exception:
|
||
status = self.execute_script("return jconfirm.lastButtonText;")
|
||
return status
|
||
|
||
def get_jqc_text_input(self, message, button=None, options=None):
|
||
"""
|
||
Pop up a jquery-confirm box and return the text submitted by the input.
|
||
If running in headless mode, the text returned is "" by default.
|
||
@Params
|
||
message: The message to display in the jquery-confirm dialog.
|
||
button: A 2-item list or tuple for text and color. Or just the text.
|
||
Example: ["Submit", "blue"] -> (default button if not specified)
|
||
Available colors: blue, green, red, orange, purple, default, dark.
|
||
A simple text string also works: "My Button". (Uses default color.)
|
||
options: A list of tuples for options to set.
|
||
Example: [("theme", "bootstrap"), ("width", "450px")]
|
||
Available theme options: bootstrap, modern, material, supervan,
|
||
light, dark, and seamless.
|
||
Available colors: (For the BORDER color, NOT the button color.)
|
||
"blue", "default", "green", "red", "purple", "orange", "dark".
|
||
Example option for changing the border color: ("color", "default")
|
||
Width can be set using percent or pixels. Eg: "36.0%", "450px"."""
|
||
from seleniumbase.core import jqc_helper
|
||
|
||
if message and not isinstance(message, str):
|
||
raise Exception('Expecting a string for arg: "message"!')
|
||
if button:
|
||
if isinstance(button, (list, tuple)) and (len(button) == 1):
|
||
button = (str(button[0]), "")
|
||
elif isinstance(button, (list, tuple)) and (len(button) > 1):
|
||
valid_colors = [
|
||
"blue",
|
||
"default",
|
||
"green",
|
||
"red",
|
||
"purple",
|
||
"orange",
|
||
"dark",
|
||
]
|
||
detected_color = str(button[1]).lower()
|
||
if str(button[1]).lower() not in valid_colors:
|
||
raise Exception(
|
||
"%s is an invalid jquery-confirm button color!\n"
|
||
"Select from %s" % (detected_color, valid_colors)
|
||
)
|
||
button = (str(button[0]), str(button[1]).lower())
|
||
else:
|
||
button = (str(button), "")
|
||
else:
|
||
button = ("Submit", "blue")
|
||
if options:
|
||
for option in options:
|
||
if not isinstance(option, (list, tuple)):
|
||
raise Exception('"options" should be a list of tuples!')
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
return ""
|
||
jqc_helper.jquery_confirm_text_dialog(
|
||
self.driver, message, button, options
|
||
)
|
||
time.sleep(0.02)
|
||
jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();"
|
||
with suppress(Exception):
|
||
self.execute_script(jf)
|
||
waiting_for_response = True
|
||
while waiting_for_response:
|
||
time.sleep(0.05)
|
||
jqc_open = self.execute_script("return jconfirm.instances.length;")
|
||
if str(jqc_open) == "0":
|
||
break
|
||
time.sleep(0.1)
|
||
status = None
|
||
try:
|
||
status = self.execute_script("return $jqc_input;")
|
||
except Exception:
|
||
status = self.execute_script("return jconfirm.lastInputText;")
|
||
return status
|
||
|
||
def get_jqc_form_inputs(self, message, buttons, options=None):
|
||
"""
|
||
Pop up a jquery-confirm box and return the input/button texts as tuple.
|
||
If running in headless mode, returns the ("", buttons[-1][0]) tuple.
|
||
@Params
|
||
message: The message to display in the jquery-confirm dialog.
|
||
buttons: A list of tuples for text and color.
|
||
Example: [("Yes!", "green"), ("No!", "red")]
|
||
Available colors: blue, green, red, orange, purple, default, dark.
|
||
A simple text string also works: "My Button". (Uses default color.)
|
||
options: A list of tuples for options to set.
|
||
Example: [("theme", "bootstrap"), ("width", "450px")]
|
||
Available theme options: bootstrap, modern, material, supervan,
|
||
light, dark, and seamless.
|
||
Available colors: (For the BORDER color, NOT the button color.)
|
||
"blue", "default", "green", "red", "purple", "orange", "dark".
|
||
Example option for changing the border color: ("color", "default")
|
||
Width can be set using percent or pixels. Eg: "36.0%", "450px"."""
|
||
from seleniumbase.core import jqc_helper
|
||
|
||
if message and not isinstance(message, str):
|
||
raise Exception('Expecting a string for arg: "message"!')
|
||
if not isinstance(buttons, (list, tuple)):
|
||
raise Exception('Expecting a list or tuple for arg: "button"!')
|
||
if len(buttons) < 1:
|
||
raise Exception('List "buttons" requires at least one button!')
|
||
new_buttons = []
|
||
for button in buttons:
|
||
if isinstance(button, (list, tuple)) and (len(button) == 1):
|
||
new_buttons.append(button[0])
|
||
elif isinstance(button, (list, tuple)) and (len(button) > 1):
|
||
new_buttons.append((button[0], str(button[1]).lower()))
|
||
else:
|
||
new_buttons.append((str(button), ""))
|
||
buttons = new_buttons
|
||
if options:
|
||
for option in options:
|
||
if not isinstance(option, (list, tuple)):
|
||
raise Exception('"options" should be a list of tuples!')
|
||
if self.headless or self.headless2 or self.xvfb:
|
||
return ("", buttons[-1][0])
|
||
jqc_helper.jquery_confirm_full_dialog(
|
||
self.driver, message, buttons, options
|
||
)
|
||
time.sleep(0.02)
|
||
jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();"
|
||
with suppress(Exception):
|
||
self.execute_script(jf)
|
||
waiting_for_response = True
|
||
while waiting_for_response:
|
||
time.sleep(0.05)
|
||
jqc_open = self.execute_script("return jconfirm.instances.length;")
|
||
if str(jqc_open) == "0":
|
||
break
|
||
time.sleep(0.1)
|
||
text_status = None
|
||
button_status = None
|
||
try:
|
||
text_status = self.execute_script("return $jqc_input;")
|
||
button_status = self.execute_script("return $jqc_status;")
|
||
except Exception:
|
||
text_status = self.execute_script("return jconfirm.lastInputText;")
|
||
button_status = self.execute_script(
|
||
"return jconfirm.lastButtonText;"
|
||
)
|
||
return (text_status, button_status)
|
||
|
||
############
|
||
|
||
def __are_quotes_escaped(self, string):
|
||
return js_utils.are_quotes_escaped(string)
|
||
|
||
def __escape_quotes_if_needed(self, string):
|
||
return js_utils.escape_quotes_if_needed(string)
|
||
|
||
def __is_in_frame(self):
|
||
return js_utils.is_in_frame(self.driver)
|
||
|
||
############
|
||
|
||
def __js_click(self, selector, by="css selector"):
|
||
"""Clicks an element using pure JS. Does not use jQuery."""
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
is_visible = self.is_element_visible(selector, by=by)
|
||
current_url = self.get_current_url()
|
||
script = (
|
||
"""var simulateClick = function (elem) {
|
||
var evt = new MouseEvent('click', {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
view: window
|
||
});
|
||
var canceled = !elem.dispatchEvent(evt);
|
||
};
|
||
var someLink = document.querySelector('%s');
|
||
simulateClick(someLink);"""
|
||
% css_selector
|
||
)
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
self.save_recorded_actions()
|
||
try:
|
||
self.execute_script(script)
|
||
except Exception as e:
|
||
# If element was visible but no longer, or on a different page now,
|
||
# assume that the click actually worked and continue with the test.
|
||
if (
|
||
(is_visible and not self.is_element_visible(selector, by=by))
|
||
or current_url != self.get_current_url()
|
||
):
|
||
return # The click worked, but threw an Exception. Keep going.
|
||
# It appears the first click didn't work. Make another attempt.
|
||
self.wait_for_ready_state_complete()
|
||
if "Cannot read properties of null" in e.msg:
|
||
page_actions.wait_for_element_present(
|
||
self.driver, selector, by, timeout=5
|
||
)
|
||
if not page_actions.is_element_clickable(
|
||
self.driver, selector, by
|
||
):
|
||
with suppress(Exception):
|
||
self.wait_for_element_clickable(
|
||
selector, by, timeout=1.8
|
||
)
|
||
# If the regular mouse-simulated click fails, do a basic JS click
|
||
script = (
|
||
"""document.querySelector('%s').click();"""
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def __js_click_element(self, element):
|
||
"""Clicks an element using pure JS. Does not use jQuery."""
|
||
is_visible = element.is_displayed()
|
||
current_url = self.get_current_url()
|
||
script = (
|
||
"""var simulateClick = function (elem) {
|
||
var evt = new MouseEvent('click', {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
view: window
|
||
});
|
||
var canceled = !elem.dispatchEvent(evt);
|
||
};
|
||
var someLink = arguments[0];
|
||
simulateClick(someLink);"""
|
||
)
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
self.save_recorded_actions()
|
||
try:
|
||
self.execute_script(script, element)
|
||
except Exception:
|
||
# If element was visible but no longer, or on a different page now,
|
||
# assume that the click actually worked and continue with the test.
|
||
if (
|
||
(is_visible and not element.is_displayed())
|
||
or current_url != self.get_current_url()
|
||
):
|
||
return # The click worked, but threw an Exception. Keep going.
|
||
# It appears the first click didn't work. Make another attempt.
|
||
self.wait_for_ready_state_complete()
|
||
# If the regular mouse-simulated click fails, do a basic JS click
|
||
script = ("""arguments[0].click();""")
|
||
self.execute_script(script, element)
|
||
|
||
def __js_click_all(self, selector, by="css selector"):
|
||
"""Clicks all matching elements using pure JS. (No jQuery)"""
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
css_selector = self.convert_to_css_selector(selector, by=by)
|
||
css_selector = re.escape(css_selector) # Add "\\" to special chars
|
||
css_selector = self.__escape_quotes_if_needed(css_selector)
|
||
script = (
|
||
"""var simulateClick = function (elem) {
|
||
var evt = new MouseEvent('click', {
|
||
bubbles: true,
|
||
cancelable: true,
|
||
view: window
|
||
});
|
||
var canceled = !elem.dispatchEvent(evt);
|
||
};
|
||
var $elements = document.querySelectorAll('%s');
|
||
var index = 0, length = $elements.length;
|
||
for(; index < length; index++){
|
||
simulateClick($elements[index]);}"""
|
||
% css_selector
|
||
)
|
||
self.execute_script(script)
|
||
|
||
def __click_with_offset(
|
||
self,
|
||
selector,
|
||
x,
|
||
y,
|
||
by="css selector",
|
||
double=False,
|
||
mark=None,
|
||
timeout=None,
|
||
center=None,
|
||
):
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.14)
|
||
if not timeout:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver, selector, by, timeout
|
||
)
|
||
if self.demo_mode:
|
||
self.__highlight(selector, by=by, loops=1)
|
||
elif self.slow_mode:
|
||
self.__slow_scroll_to_element(element)
|
||
else:
|
||
self.__scroll_to_element(element, selector, by)
|
||
self.wait_for_ready_state_complete()
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.03)
|
||
if self.demo_mode and mark is None:
|
||
mark = True
|
||
if mark:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
m_x = x
|
||
m_y = y
|
||
if center:
|
||
element_rect = element.rect
|
||
left_offset = element_rect["width"] / 2
|
||
top_offset = element_rect["height"] / 2
|
||
m_x = left_offset + (m_x or 0)
|
||
m_y = top_offset + (m_y or 0)
|
||
px = m_x - 3
|
||
py = m_y - 3
|
||
script = (
|
||
"var canvas = document.querySelector('%s');"
|
||
"var ctx = canvas.getContext('2d');"
|
||
"ctx.fillStyle = '#F8F808';"
|
||
"ctx.fillRect(%s, %s, 7, 7);"
|
||
"ctx.fillStyle = '#F80808';"
|
||
"ctx.fillRect(%s+1, %s+1, 5, 5);" % (selector, px, py, px, py)
|
||
)
|
||
self.execute_script(script)
|
||
try:
|
||
element_location = element.location["y"]
|
||
element_location = element_location - constants.Scroll.Y_OFFSET + y
|
||
if element_location < 0:
|
||
element_location = 0
|
||
scroll_script = "window.scrollTo(0, %s);" % element_location
|
||
self.execute_script(scroll_script)
|
||
time.sleep(0.12)
|
||
except Exception:
|
||
time.sleep(0.05)
|
||
if self.__needs_minimum_wait():
|
||
time.sleep(0.05)
|
||
try:
|
||
if not center:
|
||
element_rect = element.rect
|
||
left_offset = element_rect["width"] / 2
|
||
top_offset = element_rect["height"] / 2
|
||
x = -left_offset + (math.ceil(float(x)) or 0)
|
||
y = -top_offset + (math.ceil(float(y)) or 0)
|
||
action_chains = ActionChains(self.driver)
|
||
action_chains.move_to_element_with_offset(element, x, y)
|
||
if not double:
|
||
action_chains.click().perform()
|
||
else:
|
||
action_chains.double_click().perform()
|
||
except MoveTargetOutOfBoundsException:
|
||
message = (
|
||
"Target coordinates for click are out-of-bounds!\n"
|
||
"The offset must stay inside the target element!"
|
||
)
|
||
raise Exception(message)
|
||
except InvalidArgumentException:
|
||
if not self.browser == "chrome":
|
||
raise
|
||
chrome_version = self.driver.capabilities["browserVersion"]
|
||
major_chrome_version = chrome_version.split(".")[0]
|
||
chrome_dict = self.driver.capabilities["chrome"]
|
||
chromedriver_version = chrome_dict["chromedriverVersion"]
|
||
chromedriver_version = chromedriver_version.split(" ")[0]
|
||
major_chromedriver_version = chromedriver_version.split(".")[0]
|
||
if (
|
||
int(major_chromedriver_version) >= 76
|
||
and int(major_chrome_version) >= 76
|
||
):
|
||
raise
|
||
install_sb = (
|
||
"seleniumbase get chromedriver %s" % major_chrome_version
|
||
)
|
||
if int(major_chromedriver_version) < int(major_chrome_version):
|
||
# Upgrading the driver is needed for performing canvas actions
|
||
message = (
|
||
"You need to upgrade to a newer\n"
|
||
"version of chromedriver to perform canvas actions!\n"
|
||
"Reason: github.com/SeleniumHQ/selenium/issues/7000"
|
||
"\nYour version of chromedriver is: %s\n"
|
||
"And your version of Chrome is: %s\n"
|
||
"You can fix this issue by running:\n>>> %s\n"
|
||
% (chromedriver_version, chrome_version, install_sb)
|
||
)
|
||
raise Exception(message)
|
||
else:
|
||
raise
|
||
if self.demo_mode:
|
||
self.__demo_mode_pause_if_active()
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def __jquery_slow_scroll_to(self, selector, by="css selector"):
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = self.wait_for_element_present(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
dist = js_utils.get_scroll_distance_to_element(self.driver, element)
|
||
time_offset = 0
|
||
try:
|
||
if dist and abs(dist) > constants.Values.SSMD:
|
||
time_offset = int(
|
||
float(abs(dist) - constants.Values.SSMD) / 12.5
|
||
)
|
||
if time_offset > 950:
|
||
time_offset = 950
|
||
except Exception:
|
||
time_offset = 0
|
||
scroll_time_ms = 550 + time_offset
|
||
sleep_time = 0.625 + (float(time_offset) / 1000.0)
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
scroll_script = (
|
||
"""jQuery([document.documentElement, document.body]).animate({"""
|
||
"""scrollTop: jQuery('%s').offset().top - %s}, %s);"""
|
||
% (selector, constants.Scroll.Y_OFFSET, scroll_time_ms)
|
||
)
|
||
if js_utils.is_jquery_activated(self.driver):
|
||
self.execute_script(scroll_script)
|
||
else:
|
||
self.__slow_scroll_to_element(element)
|
||
time.sleep(sleep_time)
|
||
|
||
def __jquery_click(self, selector, by="css selector"):
|
||
"""Clicks an element using jQuery. Different from using pure JS."""
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
self.wait_for_element_present(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
click_script = """jQuery('%s')[0].click();""" % selector
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
self.save_recorded_actions()
|
||
self.safe_execute_script(click_script)
|
||
|
||
def __get_major_browser_version(self):
|
||
version = self.driver.__dict__["caps"]["browserVersion"]
|
||
return version.split(".")[0]
|
||
|
||
def __get_href_from_link_text(self, link_text, hard_fail=True):
|
||
href = self.get_link_attribute(link_text, "href", hard_fail)
|
||
if not href:
|
||
return None
|
||
if href.startswith("//"):
|
||
link = "http:" + href
|
||
elif href.startswith("/"):
|
||
url = self.driver.current_url
|
||
domain_url = self.get_domain_url(url)
|
||
link = domain_url + href
|
||
else:
|
||
link = href
|
||
return link
|
||
|
||
def __click_dropdown_link_text(self, link_text, link_css):
|
||
"""When a link may be hidden under a dropdown menu, use this."""
|
||
soup = self.get_beautiful_soup()
|
||
drop_down_list = []
|
||
for item in soup.select("li[class]"):
|
||
drop_down_list.append(item)
|
||
csstype = link_css.split("[")[1].split("=")[0]
|
||
for item in drop_down_list:
|
||
item_text_list = item.text.split("\n")
|
||
if link_text in item_text_list and csstype in item.decode():
|
||
dropdown_css = ""
|
||
try:
|
||
for css_class in item["class"]:
|
||
dropdown_css += "."
|
||
dropdown_css += css_class
|
||
except Exception:
|
||
continue
|
||
dropdown_css = item.name + dropdown_css
|
||
matching_dropdowns = self.find_visible_elements(dropdown_css)
|
||
for dropdown in matching_dropdowns:
|
||
# The same class names might be used for multiple dropdowns
|
||
if dropdown.is_displayed():
|
||
with suppress(Exception):
|
||
try:
|
||
page_actions.hover_element(
|
||
self.driver,
|
||
dropdown,
|
||
)
|
||
except Exception:
|
||
# If hovering fails, driver is likely outdated
|
||
# Time to go directly to the hidden link text
|
||
self.open(
|
||
self.__get_href_from_link_text(link_text)
|
||
)
|
||
return True
|
||
page_actions.hover_element_and_click(
|
||
self.driver,
|
||
dropdown,
|
||
link_text,
|
||
click_by="link text",
|
||
timeout=0.12,
|
||
)
|
||
return True
|
||
return False
|
||
|
||
def __get_href_from_partial_link_text(self, link_text, hard_fail=True):
|
||
href = self.get_partial_link_text_attribute(
|
||
link_text, "href", hard_fail
|
||
)
|
||
if not href:
|
||
return None
|
||
if href.startswith("//"):
|
||
link = "http:" + href
|
||
elif href.startswith("/"):
|
||
url = self.driver.current_url
|
||
domain_url = self.get_domain_url(url)
|
||
link = domain_url + href
|
||
else:
|
||
link = href
|
||
return link
|
||
|
||
def __click_dropdown_partial_link_text(self, link_text, link_css):
|
||
"""When a partial link may be hidden under a dropdown, use this."""
|
||
soup = self.get_beautiful_soup()
|
||
drop_down_list = []
|
||
for item in soup.select("li[class]"):
|
||
drop_down_list.append(item)
|
||
csstype = link_css.split("[")[1].split("=")[0]
|
||
for item in drop_down_list:
|
||
item_text_list = item.text.split("\n")
|
||
if link_text in item_text_list and csstype in item.decode():
|
||
dropdown_css = ""
|
||
try:
|
||
for css_class in item["class"]:
|
||
dropdown_css += "."
|
||
dropdown_css += css_class
|
||
except Exception:
|
||
continue
|
||
dropdown_css = item.name + dropdown_css
|
||
matching_dropdowns = self.find_visible_elements(dropdown_css)
|
||
for dropdown in matching_dropdowns:
|
||
# The same class names might be used for multiple dropdowns
|
||
if dropdown.is_displayed():
|
||
with suppress(Exception):
|
||
try:
|
||
page_actions.hover_element(
|
||
self.driver, dropdown
|
||
)
|
||
except Exception:
|
||
# If hovering fails, driver is likely outdated
|
||
# Time to go directly to the hidden link text
|
||
self.open(
|
||
self.__get_href_from_partial_link_text(
|
||
link_text
|
||
)
|
||
)
|
||
return True
|
||
page_actions.hover_element_and_click(
|
||
self.driver,
|
||
dropdown,
|
||
link_text,
|
||
click_by="link text",
|
||
timeout=0.12,
|
||
)
|
||
return True
|
||
return False
|
||
|
||
def __recalculate_selector(self, selector, by, xp_ok=True):
|
||
"""Use autodetection to return the correct selector with "by" updated.
|
||
If "xp_ok" is False, don't call convert_css_to_xpath(), which is
|
||
used to make the ":contains()" selector valid outside of JS calls.
|
||
Returns a (selector, by) tuple."""
|
||
return page_utils.recalculate_selector(selector, by, xp_ok=xp_ok)
|
||
|
||
def __looks_like_a_page_url(self, url):
|
||
return page_utils.looks_like_a_page_url(url)
|
||
|
||
def __make_css_match_first_element_only(self, selector):
|
||
# Only get the first match
|
||
return page_utils.make_css_match_first_element_only(selector)
|
||
|
||
def __switch_to_newest_window_if_not_blank(self):
|
||
current_window = self.driver.current_window_handle
|
||
try:
|
||
self.switch_to_window(-1)
|
||
if self.get_current_url() == "about:blank":
|
||
self.switch_to_window(current_window)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.switch_to_window(current_window)
|
||
|
||
def __needs_minimum_wait(self):
|
||
if (
|
||
self.page_load_strategy == "none"
|
||
and hasattr(settings, "SKIP_JS_WAITS")
|
||
and settings.SKIP_JS_WAITS
|
||
):
|
||
return True
|
||
else:
|
||
return False
|
||
|
||
def __demo_mode_pause_if_active(self, tiny=False):
|
||
if self.demo_mode:
|
||
wait_time = settings.DEFAULT_DEMO_MODE_TIMEOUT
|
||
if self.demo_sleep:
|
||
wait_time = float(self.demo_sleep)
|
||
if not tiny:
|
||
time.sleep(wait_time)
|
||
else:
|
||
time.sleep(wait_time / 3.4)
|
||
elif self.slow_mode:
|
||
self.__slow_mode_pause_if_active()
|
||
|
||
def __slow_mode_pause_if_active(self):
|
||
if self.slow_mode:
|
||
wait_time = settings.DEFAULT_DEMO_MODE_TIMEOUT
|
||
if self.demo_sleep:
|
||
wait_time = float(self.demo_sleep)
|
||
time.sleep(wait_time)
|
||
|
||
def __demo_mode_scroll_if_active(self, selector, by):
|
||
if self.demo_mode:
|
||
self.slow_scroll_to(selector, by=by)
|
||
|
||
def __demo_mode_highlight_if_active(self, selector, by):
|
||
self.__skip_if_esc()
|
||
if self.demo_mode:
|
||
# Includes self.slow_scroll_to(selector, by=by) by default
|
||
self.__highlight(selector, by=by)
|
||
elif self.slow_mode:
|
||
# Just do the slow scroll part of the highlight() method
|
||
time.sleep(0.08)
|
||
selector, by = self.__recalculate_selector(selector, by)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
try:
|
||
if self.browser != "safari":
|
||
scroll_distance = js_utils.get_scroll_distance_to_element(
|
||
self.driver, element
|
||
)
|
||
if abs(scroll_distance) > constants.Values.SSMD:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
else:
|
||
self.__slow_scroll_to_element(element)
|
||
else:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
except (Stale_Exception, ENI_Exception):
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
self.__slow_scroll_to_element(element)
|
||
time.sleep(0.12)
|
||
|
||
def __scroll_to_element(self, element, selector=None, by="css selector"):
|
||
success = js_utils.scroll_to_element(self.driver, element)
|
||
if not success and selector:
|
||
self.wait_for_ready_state_complete()
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver, selector, by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
self.__demo_mode_pause_if_active(tiny=True)
|
||
|
||
def __slow_scroll_to_element(self, element):
|
||
try:
|
||
js_utils.slow_scroll_to_element(self.driver, element)
|
||
except Exception:
|
||
# Scroll to the element instantly if the slow scroll fails
|
||
js_utils.scroll_to_element(self.driver, element)
|
||
|
||
def __highlight_with_js(self, selector, loops, o_bs):
|
||
if not self.__is_cdp_swap_needed():
|
||
self.wait_for_ready_state_complete()
|
||
js_utils.highlight_with_js(self.driver, selector, loops, o_bs)
|
||
|
||
def __highlight_element_with_js(self, element, loops, o_bs):
|
||
self.wait_for_ready_state_complete()
|
||
js_utils.highlight_element_with_js(self.driver, element, loops, o_bs)
|
||
|
||
def __highlight_with_jquery(self, selector, loops, o_bs):
|
||
if not self.__is_cdp_swap_needed():
|
||
self.wait_for_ready_state_complete()
|
||
js_utils.highlight_with_jquery(self.driver, selector, loops, o_bs)
|
||
|
||
def __highlight_with_js_2(self, message, selector, o_bs):
|
||
duration = self.message_duration
|
||
if not duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
js_utils.highlight_with_js_2(
|
||
self.driver, message, selector, o_bs, duration
|
||
)
|
||
|
||
def __highlight_element_with_js_2(self, message, element, o_bs):
|
||
duration = self.message_duration
|
||
if not duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
js_utils.highlight_element_with_js_2(
|
||
self.driver, message, element, o_bs, duration
|
||
)
|
||
|
||
def __highlight_with_jquery_2(self, message, selector, o_bs):
|
||
duration = self.message_duration
|
||
if not duration:
|
||
duration = settings.DEFAULT_MESSAGE_DURATION
|
||
if (
|
||
(self.headless or self.headless2 or self.xvfb)
|
||
and float(duration) > 0.75
|
||
):
|
||
duration = 0.75
|
||
js_utils.highlight_with_jquery_2(
|
||
self.driver, message, selector, o_bs, duration
|
||
)
|
||
|
||
def __highlight_with_assert_success(
|
||
self, message, selector, by="css selector"
|
||
):
|
||
selector, by = self.__recalculate_selector(selector, by, xp_ok=False)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
try:
|
||
if self.browser != "safari":
|
||
scroll_distance = js_utils.get_scroll_distance_to_element(
|
||
self.driver, element
|
||
)
|
||
if abs(scroll_distance) > constants.Values.SSMD:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
else:
|
||
self.__slow_scroll_to_element(element)
|
||
else:
|
||
self.__jquery_slow_scroll_to(selector, by)
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by=by, timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
self.__slow_scroll_to_element(element)
|
||
use_element_directly = False
|
||
try:
|
||
selector = self.convert_to_css_selector(selector, by=by)
|
||
except Exception:
|
||
# If can't convert to CSS_Selector for JS, use element directly
|
||
use_element_directly = True
|
||
|
||
o_bs = "" # original_box_shadow
|
||
try:
|
||
style = element.get_attribute("style")
|
||
except Exception:
|
||
self.wait_for_ready_state_complete()
|
||
time.sleep(0.12)
|
||
element = self.wait_for_element_visible(
|
||
selector, by="css selector", timeout=settings.SMALL_TIMEOUT
|
||
)
|
||
style = element.get_attribute("style")
|
||
if style:
|
||
if "box-shadow: " in style:
|
||
box_start = style.find("box-shadow: ")
|
||
box_end = style.find(";", box_start) + 1
|
||
original_box_shadow = style[box_start:box_end]
|
||
o_bs = original_box_shadow
|
||
|
||
if use_element_directly:
|
||
self.__highlight_element_with_js_2(message, element, o_bs)
|
||
elif ":contains" not in selector and ":first" not in selector:
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
self.__highlight_with_js_2(message, selector, o_bs)
|
||
else:
|
||
selector = self.__make_css_match_first_element_only(selector)
|
||
selector = re.escape(selector)
|
||
selector = self.__escape_quotes_if_needed(selector)
|
||
try:
|
||
self.__highlight_with_jquery_2(message, selector, o_bs)
|
||
except Exception:
|
||
pass # JQuery probably couldn't load. Skip highlighting.
|
||
time.sleep(0.065)
|
||
|
||
def __activate_standard_virtual_display(self):
|
||
from sbvirtualdisplay import Display
|
||
width = settings.HEADLESS_START_WIDTH
|
||
height = settings.HEADLESS_START_HEIGHT
|
||
with suppress(Exception):
|
||
self._xvfb_display = Display(
|
||
visible=0, size=(width, height)
|
||
)
|
||
self._xvfb_display.start()
|
||
self.headless_active = True
|
||
if not self.undetectable:
|
||
sb_config._virtual_display = self._xvfb_display
|
||
sb_config.headless_active = True
|
||
if self._reuse_session and hasattr(sb_config, "_vd_list"):
|
||
if isinstance(sb_config._vd_list, list):
|
||
sb_config._vd_list.append(self._xvfb_display)
|
||
|
||
def __activate_virtual_display(self):
|
||
if self.undetectable and not (self.headless or self.headless2):
|
||
from sbvirtualdisplay import Display
|
||
import Xlib.display
|
||
try:
|
||
if not self._xvfb_width:
|
||
self._xvfb_width = 1366
|
||
if not self._xvfb_height:
|
||
self._xvfb_height = 768
|
||
self._xvfb_display = Display(
|
||
visible=True,
|
||
size=(self._xvfb_width, self._xvfb_height),
|
||
backend="xvfb",
|
||
use_xauth=True,
|
||
)
|
||
self._xvfb_display.start()
|
||
if "DISPLAY" not in os.environ.keys():
|
||
print(
|
||
"\nX11 display failed! Will use regular xvfb!"
|
||
)
|
||
self.__activate_standard_virtual_display()
|
||
else:
|
||
self.headless_active = True
|
||
if self._reuse_session and hasattr(sb_config, "_vd_list"):
|
||
if isinstance(sb_config._vd_list, list):
|
||
sb_config._vd_list.append(self._xvfb_display)
|
||
except Exception as e:
|
||
if hasattr(e, "msg"):
|
||
print("\n" + str(e.msg))
|
||
else:
|
||
print(e)
|
||
print("\nX11 display failed! Will use regular xvfb!")
|
||
self.__activate_standard_virtual_display()
|
||
return
|
||
pyautogui_is_installed = False
|
||
try:
|
||
import pyautogui
|
||
with suppress(Exception):
|
||
use_pyautogui_ver = constants.PyAutoGUI.VER
|
||
if pyautogui.__version__ != use_pyautogui_ver:
|
||
del pyautogui # To get newer ver
|
||
shared_utils.pip_install(
|
||
"pyautogui", version=use_pyautogui_ver
|
||
)
|
||
import pyautogui
|
||
pyautogui_is_installed = True
|
||
except Exception:
|
||
message = (
|
||
"PyAutoGUI is required for UC Mode on Linux! "
|
||
"Installing now..."
|
||
)
|
||
print("\n" + message)
|
||
shared_utils.pip_install(
|
||
"pyautogui", version=constants.PyAutoGUI.VER
|
||
)
|
||
import pyautogui
|
||
pyautogui_is_installed = True
|
||
if (
|
||
pyautogui_is_installed
|
||
and hasattr(pyautogui, "_pyautogui_x11")
|
||
):
|
||
try:
|
||
pyautogui._pyautogui_x11._display = (
|
||
Xlib.display.Display(os.environ['DISPLAY'])
|
||
)
|
||
sb_config._pyautogui_x11_display = (
|
||
pyautogui._pyautogui_x11._display
|
||
)
|
||
except Exception as e:
|
||
if hasattr(e, "msg"):
|
||
print("\n" + str(e.msg))
|
||
else:
|
||
print(e)
|
||
else:
|
||
self.__activate_standard_virtual_display()
|
||
|
||
def __activate_virtual_display_as_needed(self):
|
||
"""This is only needed on Linux.
|
||
The "--xvfb" arg is still useful, as it prevents headless mode,
|
||
which is the default mode on Linux unless using another arg."""
|
||
if is_linux and (not self.headed or self.xvfb):
|
||
pip_find_lock = fasteners.InterProcessLock(
|
||
constants.PipInstall.FINDLOCK
|
||
)
|
||
try:
|
||
with pip_find_lock:
|
||
pass
|
||
except Exception:
|
||
# Since missing permissions, skip the locks
|
||
self.__activate_virtual_display()
|
||
return
|
||
with pip_find_lock: # Prevent issues with multiple processes
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(constants.PipInstall.FINDLOCK)
|
||
self.__activate_virtual_display()
|
||
|
||
def __ad_block_as_needed(self):
|
||
"""This is an internal method for handling ad-blocking.
|
||
Use "pytest --ad-block" to enable this during tests.
|
||
When not Chromium or in headless mode, use the hack."""
|
||
if self.ad_block_on and (self.headless or not self.is_chromium()):
|
||
# (Chromium browsers in headed mode use the extension instead)
|
||
current_url = self.get_current_url()
|
||
if not current_url == self.__last_page_load_url:
|
||
if page_actions.is_element_present(
|
||
self.driver, "iframe", By.CSS_SELECTOR
|
||
):
|
||
self.ad_block()
|
||
self.__last_page_load_url = current_url
|
||
|
||
def __disable_beforeunload_as_needed(self):
|
||
"""Disables beforeunload as needed. Also resets frame_switch state."""
|
||
if (
|
||
hasattr(self, "_disable_beforeunload")
|
||
and self._disable_beforeunload
|
||
):
|
||
self.disable_beforeunload()
|
||
if self.recorder_mode:
|
||
try:
|
||
current_url = self.get_current_url
|
||
except Exception:
|
||
current_url = None
|
||
self.__last_saved_url = None
|
||
if current_url != self.__last_saved_url:
|
||
self.__frame_switch_layer = 0
|
||
self.__frame_switch_multi = False
|
||
self.__last_saved_url = current_url
|
||
|
||
############
|
||
|
||
@decorators.deprecated("You should use re.escape() instead.")
|
||
def jq_format(self, code):
|
||
# DEPRECATED - re.escape() already performs this action.
|
||
return js_utils._jq_format(code)
|
||
|
||
############
|
||
|
||
# Shadow DOM / Shadow-root methods
|
||
|
||
def __get_shadow_element(
|
||
self, selector, timeout=None, must_be_visible=False
|
||
):
|
||
self.wait_for_ready_state_complete()
|
||
if timeout is None:
|
||
timeout = settings.SMALL_TIMEOUT
|
||
elif timeout == 0:
|
||
timeout = 0.1 # Use for: is_shadow_element_* (* = present/visible)
|
||
if self.timeout_multiplier and timeout == settings.SMALL_TIMEOUT:
|
||
timeout = self.__get_new_timeout(timeout)
|
||
self.__fail_if_invalid_shadow_selector_usage(selector)
|
||
if "::shadow " not in selector:
|
||
raise Exception(
|
||
'A Shadow DOM selector must contain at least one "::shadow "!'
|
||
)
|
||
selectors = selector.split("::shadow ")
|
||
element = self.get_element(selectors[0])
|
||
selector_chain = selectors[0]
|
||
is_present = False
|
||
for selector_part in selectors[1:]:
|
||
shadow_root = None
|
||
if (
|
||
(self.is_chromium() or self.browser == "firefox")
|
||
and int(self.__get_major_browser_version()) >= 96
|
||
):
|
||
try:
|
||
shadow_root = element.shadow_root
|
||
except Exception:
|
||
if self.browser == "chrome":
|
||
chrome_dict = self.driver.capabilities["chrome"]
|
||
chrome_dr_version = chrome_dict["chromedriverVersion"]
|
||
chromedriver_version = chrome_dr_version.split(" ")[0]
|
||
major_c_dr_version = chromedriver_version.split(".")[0]
|
||
if int(major_c_dr_version) < 96:
|
||
upgrade_to = "latest"
|
||
major_browser_version = (
|
||
self.__get_major_browser_version()
|
||
)
|
||
if int(major_browser_version) >= 96:
|
||
upgrade_to = str(major_browser_version)
|
||
message = (
|
||
"You need to upgrade to a newer\n"
|
||
"version of chromedriver to interact\n"
|
||
"with Shadow root elements!\n"
|
||
"(Current driver version is: %s)"
|
||
"\n(Minimum driver version is: 96.*)"
|
||
"\nTo upgrade, run this:"
|
||
'\n"seleniumbase get chromedriver %s"'
|
||
% (chromedriver_version, upgrade_to)
|
||
)
|
||
raise Exception(message)
|
||
if timeout != 0.1: # Skip wait for special 0.1 (See above)
|
||
time.sleep(2)
|
||
try:
|
||
shadow_root = element.shadow_root
|
||
except Exception:
|
||
raise Exception(
|
||
"Element {%s} has no shadow root!" % selector_chain
|
||
)
|
||
else: # This part won't work on Chrome 96 or newer.
|
||
# If using Chrome 96 or newer (and on an old Python version),
|
||
# you'll need to upgrade in order to access Shadow roots.
|
||
# Firefox users will likely hit:
|
||
# https://github.com/mozilla/geckodriver/issues/1711
|
||
# When Firefox adds support, switch to element.shadow_root
|
||
try:
|
||
shadow_root = self.execute_script(
|
||
"return arguments[0].shadowRoot;", element
|
||
)
|
||
except Exception:
|
||
time.sleep(2)
|
||
shadow_root = self.execute_script(
|
||
"return arguments[0].shadowRoot;", element
|
||
)
|
||
if timeout == 0.1 and not shadow_root:
|
||
raise Exception(
|
||
"Element {%s} has no shadow root!" % selector_chain
|
||
)
|
||
elif not shadow_root:
|
||
time.sleep(2) # Wait two seconds for the shadow root to appear
|
||
shadow_root = self.execute_script(
|
||
"return arguments[0].shadowRoot;", element
|
||
)
|
||
if not shadow_root:
|
||
raise Exception(
|
||
"Element {%s} has no shadow root!" % selector_chain
|
||
)
|
||
selector_chain += "::shadow "
|
||
selector_chain += selector_part
|
||
try:
|
||
if (
|
||
(self.is_chromium() or self.browser == "firefox")
|
||
and int(self.__get_major_browser_version()) >= 96
|
||
):
|
||
if timeout == 0.1:
|
||
element = shadow_root.find_element(
|
||
By.CSS_SELECTOR, value=selector_part
|
||
)
|
||
else:
|
||
found = False
|
||
for i in range(int(timeout) * 4):
|
||
try:
|
||
element = shadow_root.find_element(
|
||
By.CSS_SELECTOR, value=selector_part
|
||
)
|
||
is_present = True
|
||
if (
|
||
selector_part == selectors[-1]
|
||
and must_be_visible
|
||
):
|
||
if not element.is_displayed():
|
||
raise Exception(
|
||
"Shadow Root element not visible!"
|
||
)
|
||
found = True
|
||
break
|
||
except Exception:
|
||
time.sleep(0.2)
|
||
continue
|
||
if not found:
|
||
element = shadow_root.find_element(
|
||
By.CSS_SELECTOR, value=selector_part
|
||
)
|
||
is_present = True
|
||
if (
|
||
selector_part == selectors[-1]
|
||
and must_be_visible
|
||
and not element.is_displayed()
|
||
):
|
||
raise Exception(
|
||
"Shadow Root element not visible!"
|
||
)
|
||
else:
|
||
element = page_actions.wait_for_element_present(
|
||
shadow_root,
|
||
selector_part,
|
||
by="css selector",
|
||
timeout=timeout,
|
||
)
|
||
except Exception:
|
||
error = "not present"
|
||
the_exception = "NoSuchElementException"
|
||
if must_be_visible and is_present:
|
||
error = "not visible"
|
||
the_exception = "ElementNotVisibleException"
|
||
msg = (
|
||
"Shadow DOM Element {%s} was %s after %s seconds!"
|
||
% (selector_chain, error, timeout)
|
||
)
|
||
page_actions.timeout_exception(the_exception, msg)
|
||
return element
|
||
|
||
def __fail_if_invalid_shadow_selector_usage(self, selector):
|
||
if selector.strip().endswith("::shadow"):
|
||
msg = (
|
||
"A Shadow DOM selector cannot end on a shadow root element!"
|
||
" End the selector with an element inside the shadow root!"
|
||
)
|
||
raise Exception(msg)
|
||
|
||
def __is_shadow_selector(self, selector):
|
||
self.__fail_if_invalid_shadow_selector_usage(selector)
|
||
if "::shadow " in selector:
|
||
return True
|
||
return False
|
||
|
||
def __shadow_click(self, selector, timeout):
|
||
element = self.__get_shadow_element(
|
||
selector, timeout=timeout, must_be_visible=True
|
||
)
|
||
element.click()
|
||
|
||
def __shadow_type(self, selector, text, timeout, clear_first=True):
|
||
element = self.__get_shadow_element(
|
||
selector, timeout=timeout, must_be_visible=True
|
||
)
|
||
if clear_first:
|
||
with suppress(Exception):
|
||
element.clear()
|
||
backspaces = Keys.BACK_SPACE * 42 # Autofill Defense
|
||
element.send_keys(backspaces)
|
||
text = self.__get_type_checked_text(text)
|
||
if not text.endswith("\n"):
|
||
element.send_keys(text)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
else:
|
||
element.send_keys(text[:-1])
|
||
element.send_keys(Keys.RETURN)
|
||
if settings.WAIT_FOR_RSC_ON_PAGE_LOADS:
|
||
self.wait_for_ready_state_complete()
|
||
|
||
def __shadow_clear(self, selector, timeout):
|
||
element = self.__get_shadow_element(
|
||
selector, timeout=timeout, must_be_visible=True
|
||
)
|
||
with suppress(Exception):
|
||
element.clear()
|
||
backspaces = Keys.BACK_SPACE * 42 # Autofill Defense
|
||
element.send_keys(backspaces)
|
||
|
||
def __get_shadow_text(self, selector, timeout):
|
||
element = self.__get_shadow_element(
|
||
selector, timeout=timeout, must_be_visible=True
|
||
)
|
||
element_text = element.text
|
||
if self.browser == "safari":
|
||
element_text = element.get_attribute("innerText")
|
||
return element_text
|
||
|
||
def __get_shadow_attribute(self, selector, attribute, timeout):
|
||
element = self.__get_shadow_element(selector, timeout=timeout)
|
||
return element.get_attribute(attribute)
|
||
|
||
def __wait_for_shadow_text_visible(self, text, selector, timeout):
|
||
text = str(text)
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0)
|
||
for x in range(int(settings.SMALL_TIMEOUT * 10)):
|
||
try:
|
||
actual_text = self.__get_shadow_text(
|
||
selector, timeout=1
|
||
).strip()
|
||
text = text.strip()
|
||
if text not in actual_text:
|
||
msg = (
|
||
"Expected text {%s} in element {%s} was not visible!"
|
||
% (text, selector)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"ElementNotVisibleException", msg
|
||
)
|
||
return True
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.1)
|
||
actual_text = self.__get_shadow_text(selector, timeout=1).strip()
|
||
text = text.strip()
|
||
if text not in actual_text:
|
||
msg = "Expected text {%s} in element {%s} was not visible!" % (
|
||
text,
|
||
selector,
|
||
)
|
||
page_actions.timeout_exception("TextNotVisibleException", msg)
|
||
return True
|
||
|
||
def __wait_for_exact_shadow_text_visible(self, text, selector, timeout):
|
||
text = str(text)
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0)
|
||
for x in range(int(settings.SMALL_TIMEOUT * 10)):
|
||
try:
|
||
actual_text = self.__get_shadow_text(
|
||
selector, timeout=1
|
||
).strip()
|
||
text = text.strip()
|
||
if text != actual_text:
|
||
msg = (
|
||
"Expected exact text {%s} in element {%s} not visible!"
|
||
"" % (text, selector)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"TextNotVisibleException", msg
|
||
)
|
||
return True
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.1)
|
||
actual_text = self.__get_shadow_text(selector, timeout=1).strip()
|
||
text = text.strip()
|
||
if text != actual_text:
|
||
msg = (
|
||
"Expected exact text {%s} in element {%s} was not visible!"
|
||
% (text, selector)
|
||
)
|
||
page_actions.timeout_exception("TextNotVisibleException", msg)
|
||
return True
|
||
|
||
def __wait_for_non_empty_shadow_text_visible(self, selector, timeout):
|
||
start_ms = time.time() * 1000.0
|
||
stop_ms = start_ms + (settings.SMALL_TIMEOUT * 1000.0)
|
||
for x in range(int(settings.SMALL_TIMEOUT * 10)):
|
||
try:
|
||
actual_text = self.__get_shadow_text(selector, timeout=1)
|
||
actual_text = actual_text.strip()
|
||
if len(actual_text) == 0:
|
||
msg = "Element {%s} has no visible text!" % selector
|
||
page_actions.timeout_exception(
|
||
"TextNotVisibleException", msg
|
||
)
|
||
return True
|
||
except Exception:
|
||
now_ms = time.time() * 1000.0
|
||
if now_ms >= stop_ms:
|
||
break
|
||
time.sleep(0.1)
|
||
actual_text = self.__get_shadow_text(selector, timeout=1)
|
||
actual_text = actual_text.strip()
|
||
if len(actual_text) == 0:
|
||
msg = "Element {%s} has no visible text!" % selector
|
||
page_actions.timeout_exception("TextNotVisibleException", msg)
|
||
return True
|
||
|
||
def __assert_shadow_text_visible(self, text, selector, timeout):
|
||
self.__wait_for_shadow_text_visible(text, selector, timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT TEXT"
|
||
i_n = "in"
|
||
by = By.CSS_SELECTOR
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b>: {%s} %s %s: %s" % (
|
||
a_t, text, i_n, by.upper(), selector
|
||
)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def __assert_exact_shadow_text_visible(self, text, selector, timeout):
|
||
self.__wait_for_exact_shadow_text_visible(text, selector, timeout)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT EXACT TEXT"
|
||
i_n = "in"
|
||
by = By.CSS_SELECTOR
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_exact_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b>: {%s} %s %s: %s" % (
|
||
a_t, text, i_n, by.upper(), selector
|
||
)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def __assert_non_empty_shadow_text_visible(self, selector, timeout, strip):
|
||
self.__wait_for_non_empty_shadow_text_visible(selector, timeout, strip)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT NON-EMPTY TEXT"
|
||
i_n = "in"
|
||
by = By.CSS_SELECTOR
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert_non_empty_text(self._language)
|
||
i_n = SD.translate_in(self._language)
|
||
messenger_post = "<b>%s</b> %s %s: %s" % (
|
||
a_t, i_n, by.upper(), selector
|
||
)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def __is_shadow_element_present(self, selector):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
return element is not None
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_element_visible(self, selector):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
return element.is_displayed()
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_element_clickable(self, selector):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
if element.is_displayed() and element.is_enabled():
|
||
return True
|
||
return False
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_element_enabled(self, selector):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
return element.is_enabled()
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_text_visible(self, text, selector):
|
||
text = str(text)
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
if self.browser == "safari":
|
||
return (
|
||
element.is_displayed()
|
||
and text in element.get_attribute("innerText")
|
||
)
|
||
return element.is_displayed() and text in element.text
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_exact_text_visible(self, text, selector):
|
||
text = str(text)
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
if self.browser == "safari":
|
||
return (
|
||
element.is_displayed()
|
||
and text in element.get_attribute("innerText")
|
||
)
|
||
return (
|
||
element.is_displayed()
|
||
and text.strip() == element.text.strip()
|
||
)
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_non_empty_text_visible(self, selector):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
if self.browser == "safari":
|
||
return (
|
||
element.is_displayed()
|
||
and len(element.get_attribute("innerText").strip() > 0)
|
||
)
|
||
return element.is_displayed() and len(element.text.strip()) > 0
|
||
except Exception:
|
||
return False
|
||
|
||
def __is_shadow_attribute_present(self, selector, attribute, value=None):
|
||
try:
|
||
element = self.__get_shadow_element(selector, timeout=0.1)
|
||
found_value = element.get_attribute(attribute)
|
||
if found_value is None:
|
||
return False
|
||
if value is not None:
|
||
if found_value == value:
|
||
return True
|
||
else:
|
||
return False
|
||
else:
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def __wait_for_shadow_element_present(self, selector, timeout):
|
||
element = self.__get_shadow_element(selector, timeout=timeout)
|
||
return element
|
||
|
||
def __wait_for_shadow_element_visible(self, selector, timeout):
|
||
element = self.__get_shadow_element(
|
||
selector, timeout=timeout, must_be_visible=True
|
||
)
|
||
return element
|
||
|
||
def __wait_for_shadow_attribute_present(
|
||
self, selector, attribute, value=None, timeout=None
|
||
):
|
||
element = self.__get_shadow_element(selector, timeout=timeout)
|
||
actual_value = element.get_attribute(attribute)
|
||
plural = "s"
|
||
if timeout == 1:
|
||
plural = ""
|
||
if value is None:
|
||
# The element attribute only needs to exist
|
||
if actual_value is not None:
|
||
return element
|
||
else:
|
||
# The element does not have the attribute
|
||
message = (
|
||
"Expected attribute {%s} of element {%s} "
|
||
"was not present after %s second%s!"
|
||
% (attribute, selector, timeout, plural)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"NoSuchAttributeException", message
|
||
)
|
||
else:
|
||
if actual_value == value:
|
||
return element
|
||
else:
|
||
message = (
|
||
"Expected value {%s} for attribute {%s} of element "
|
||
"{%s} was not present after %s second%s! "
|
||
"(Actual value was {%s})"
|
||
% (
|
||
value,
|
||
attribute,
|
||
selector,
|
||
timeout,
|
||
plural,
|
||
actual_value,
|
||
)
|
||
)
|
||
page_actions.timeout_exception(
|
||
"NoSuchAttributeException", message
|
||
)
|
||
|
||
def __assert_shadow_element_present(self, selector):
|
||
self.__get_shadow_element(selector)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT"
|
||
by = By.CSS_SELECTOR
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert(self._language)
|
||
messenger_post = "<b>%s %s</b>: %s" % (a_t, by.upper(), selector)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
def __assert_shadow_element_visible(self, selector):
|
||
element = self.__get_shadow_element(selector)
|
||
if not element.is_displayed():
|
||
msg = "Shadow DOM Element {%s} was not visible!" % selector
|
||
page_actions.timeout_exception("NoSuchElementException", msg)
|
||
if self.demo_mode:
|
||
a_t = "ASSERT"
|
||
by = By.CSS_SELECTOR
|
||
if self._language != "English":
|
||
from seleniumbase.fixtures.words import SD
|
||
|
||
a_t = SD.translate_assert(self._language)
|
||
messenger_post = "<b>%s %s</b>: %s" % (a_t, by.upper(), selector)
|
||
with suppress(Exception):
|
||
js_utils.activate_jquery(self.driver)
|
||
js_utils.post_messenger_success_message(
|
||
self.driver, messenger_post, self.message_duration
|
||
)
|
||
|
||
############
|
||
|
||
def setUp(self, masterqa_mode=False):
|
||
"""This method runs before every test begins.
|
||
Be careful if a subclass of BaseCase overrides setUp().
|
||
If so, add the following line to the subclass setUp() method:
|
||
super().setUp() """
|
||
if not hasattr(self, "_using_sb_fixture") and self.__called_setup:
|
||
return # This test already called setUp()
|
||
self.__called_setup = True
|
||
self.__called_teardown = False
|
||
self.is_pytest = None
|
||
self.log_path = constants.Logs.LATEST
|
||
self.masterqa_mode = masterqa_mode
|
||
try:
|
||
# This raises an exception if the test is not coming from pytest
|
||
self.is_pytest = sb_config.is_pytest
|
||
except Exception:
|
||
# Not using pytest (could be pynose, behave, or raw python)
|
||
self.is_pytest = False
|
||
if self.is_pytest:
|
||
# pytest-specific code
|
||
test_id = self.__get_test_id()
|
||
self.test_id = test_id
|
||
self.is_behave = False
|
||
if hasattr(self, "_using_sb_fixture"):
|
||
self.test_id = sb_config._test_id
|
||
if hasattr(sb_config, "_sb_pdb_driver"):
|
||
sb_config._sb_pdb_driver = None
|
||
self.browser = sb_config.browser
|
||
self.account = sb_config.account
|
||
self.data = sb_config.data
|
||
self.var1 = sb_config.var1
|
||
self.var2 = sb_config.var2
|
||
self.var3 = sb_config.var3
|
||
variables = sb_config.variables
|
||
if variables and isinstance(variables, str) and len(variables) > 0:
|
||
import ast
|
||
|
||
bad_input = False
|
||
if (
|
||
not variables.startswith("{")
|
||
or not variables.endswith("}")
|
||
):
|
||
bad_input = True
|
||
else:
|
||
try:
|
||
variables = ast.literal_eval(variables)
|
||
if not isinstance(variables, dict):
|
||
bad_input = True
|
||
except Exception:
|
||
bad_input = True
|
||
if bad_input:
|
||
raise Exception(
|
||
'\nExpecting a Python dictionary for "variables"!'
|
||
"\nEg. --variables=\"{'KEY1':'VALUE', 'KEY2':123}\""
|
||
)
|
||
elif isinstance(variables, dict):
|
||
pass # Already processed
|
||
else:
|
||
variables = {}
|
||
sb_config.variables = variables
|
||
self.variables = sb_config.variables
|
||
self.slow_mode = sb_config.slow_mode
|
||
self.demo_mode = sb_config.demo_mode
|
||
self.demo_sleep = sb_config.demo_sleep
|
||
self.highlights = sb_config.highlights
|
||
self.time_limit = sb_config._time_limit
|
||
sb_config.time_limit = sb_config._time_limit # Reset between tests
|
||
self.environment = sb_config.environment
|
||
self.env = self.environment # Add a shortened version
|
||
self.with_selenium = sb_config.with_selenium # Should be True
|
||
self.headless = sb_config.headless
|
||
self.headless1 = sb_config.headless1
|
||
if self.headless1:
|
||
self.headless = True
|
||
self.headless2 = sb_config.headless2
|
||
self.headless_active = False
|
||
sb_config.headless_active = False
|
||
self.headed = sb_config.headed
|
||
self.xvfb = sb_config.xvfb
|
||
self.xvfb_metrics = sb_config.xvfb_metrics
|
||
self.locale_code = sb_config.locale_code
|
||
self.interval = sb_config.interval
|
||
self.start_page = sb_config.start_page
|
||
self.log_path = sb_config.log_path
|
||
self.with_testing_base = sb_config.with_testing_base
|
||
self.with_basic_test_info = sb_config.with_basic_test_info
|
||
self.with_screen_shots = sb_config.with_screen_shots
|
||
self.with_page_source = sb_config.with_page_source
|
||
self.with_db_reporting = sb_config.with_db_reporting
|
||
self.with_s3_logging = sb_config.with_s3_logging
|
||
self.protocol = sb_config.protocol
|
||
self.servername = sb_config.servername
|
||
self.port = sb_config.port
|
||
self.proxy_string = sb_config.proxy_string
|
||
self.proxy_bypass_list = sb_config.proxy_bypass_list
|
||
self.proxy_pac_url = sb_config.proxy_pac_url
|
||
self.multi_proxy = sb_config.multi_proxy
|
||
self.user_agent = sb_config.user_agent
|
||
self.mobile_emulator = sb_config.mobile_emulator
|
||
self.device_metrics = sb_config.device_metrics
|
||
self.cap_file = sb_config.cap_file
|
||
self.cap_string = sb_config.cap_string
|
||
self.settings_file = sb_config.settings_file
|
||
self.database_env = sb_config.database_env
|
||
self.message_duration = sb_config.message_duration
|
||
self.js_checking_on = sb_config.js_checking_on
|
||
self.ad_block_on = sb_config.ad_block_on
|
||
self.host_resolver_rules = sb_config.host_resolver_rules
|
||
self.block_images = sb_config.block_images
|
||
self.do_not_track = sb_config.do_not_track
|
||
self.chromium_arg = sb_config.chromium_arg
|
||
self.firefox_arg = sb_config.firefox_arg
|
||
self.firefox_pref = sb_config.firefox_pref
|
||
self.verify_delay = sb_config.verify_delay
|
||
self.esc_end = sb_config.esc_end
|
||
self.recorder_mode = sb_config.recorder_mode
|
||
self.recorder_ext = sb_config.recorder_mode
|
||
self.rec_print = sb_config.rec_print
|
||
self.rec_behave = sb_config.rec_behave
|
||
self.record_sleep = sb_config.record_sleep
|
||
if self.rec_print and not self.recorder_mode:
|
||
self.recorder_mode = True
|
||
self.recorder_ext = True
|
||
elif self.rec_behave and not self.recorder_mode:
|
||
self.recorder_mode = True
|
||
self.recorder_ext = True
|
||
elif self.record_sleep and not self.recorder_mode:
|
||
self.recorder_mode = True
|
||
self.recorder_ext = True
|
||
self.disable_cookies = sb_config.disable_cookies
|
||
self.disable_js = sb_config.disable_js
|
||
self.disable_csp = sb_config.disable_csp
|
||
self.disable_ws = sb_config.disable_ws
|
||
self.enable_ws = sb_config.enable_ws
|
||
if not self.disable_ws:
|
||
self.enable_ws = True
|
||
self.enable_sync = sb_config.enable_sync
|
||
self.use_auto_ext = sb_config.use_auto_ext
|
||
self.undetectable = sb_config.undetectable
|
||
self.uc_cdp_events = sb_config.uc_cdp_events
|
||
self.uc_subprocess = sb_config.uc_subprocess
|
||
self.log_cdp_events = sb_config.log_cdp_events
|
||
self.no_sandbox = sb_config.no_sandbox
|
||
self.disable_gpu = sb_config.disable_gpu
|
||
self.incognito = sb_config.incognito
|
||
self.guest_mode = sb_config.guest_mode
|
||
self.dark_mode = sb_config.dark_mode
|
||
self.devtools = sb_config.devtools
|
||
self.remote_debug = sb_config.remote_debug
|
||
self._multithreaded = sb_config._multithreaded
|
||
self._reuse_session = sb_config.reuse_session
|
||
self._crumbs = sb_config.crumbs
|
||
self._disable_beforeunload = sb_config._disable_beforeunload
|
||
self.dashboard = sb_config.dashboard
|
||
self._dash_initialized = sb_config._dashboard_initialized
|
||
if self.dashboard and self._multithreaded:
|
||
self.dash_lock = fasteners.InterProcessLock(
|
||
constants.Dashboard.LOCKFILE
|
||
)
|
||
self.enable_3d_apis = sb_config.enable_3d_apis
|
||
self._swiftshader = sb_config.swiftshader
|
||
self.user_data_dir = sb_config.user_data_dir
|
||
self.extension_zip = sb_config.extension_zip
|
||
self.extension_dir = sb_config.extension_dir
|
||
self.disable_features = sb_config.disable_features
|
||
self.binary_location = sb_config.binary_location
|
||
self.driver_version = sb_config.driver_version
|
||
self.page_load_strategy = sb_config.page_load_strategy
|
||
self.use_wire = sb_config.use_wire
|
||
self.external_pdf = sb_config.external_pdf
|
||
self._final_debug = sb_config.final_debug
|
||
self.window_position = sb_config.window_position
|
||
self.window_size = sb_config.window_size
|
||
self.maximize_option = sb_config.maximize_option
|
||
self.save_screenshot_after_test = sb_config.save_screenshot
|
||
self.no_screenshot_after_test = sb_config.no_screenshot
|
||
self.visual_baseline = sb_config.visual_baseline
|
||
self.timeout_multiplier = sb_config.timeout_multiplier
|
||
self.pytest_html_report = sb_config.pytest_html_report
|
||
self.report_on = False
|
||
if self.pytest_html_report:
|
||
self.report_on = True
|
||
self.use_grid = False
|
||
if self.servername != "localhost":
|
||
# Use Selenium Grid (Use --server="127.0.0.1" for a local Grid)
|
||
self.use_grid = True
|
||
if self.with_db_reporting:
|
||
import getpass
|
||
import uuid
|
||
from seleniumbase.core.application_manager import (
|
||
ApplicationManager,
|
||
)
|
||
from seleniumbase.core.testcase_manager import (
|
||
ExecutionQueryPayload,
|
||
TestcaseDataPayload,
|
||
TestcaseManager,
|
||
)
|
||
|
||
self.execution_guid = str(uuid.uuid4())
|
||
self.testcase_guid = None
|
||
self.execution_start_time = 0
|
||
self.case_start_time = 0
|
||
self.testcase_manager = None
|
||
self.testcase_manager = TestcaseManager(self.database_env)
|
||
#
|
||
exec_payload = ExecutionQueryPayload()
|
||
exec_payload.execution_start_time = int(time.time() * 1000.0)
|
||
self.execution_start_time = exec_payload.execution_start_time
|
||
exec_payload.guid = self.execution_guid
|
||
exec_payload.username = getpass.getuser()
|
||
self.testcase_manager.insert_execution_data(exec_payload)
|
||
#
|
||
data_payload = TestcaseDataPayload()
|
||
self.testcase_guid = str(uuid.uuid4())
|
||
data_payload.guid = self.testcase_guid
|
||
data_payload.execution_guid = self.execution_guid
|
||
if self.with_selenium:
|
||
data_payload.browser = self.browser
|
||
else:
|
||
data_payload.browser = "N/A"
|
||
data_payload.test_address = test_id
|
||
application = ApplicationManager.generate_application_string(
|
||
self
|
||
)
|
||
data_payload.env = application.split(".")[0]
|
||
data_payload.start_time = application.split(".")[1]
|
||
data_payload.state = constants.State.UNTESTED
|
||
self.__skip_reason = None
|
||
self.testcase_manager.insert_testcase_data(data_payload)
|
||
self.case_start_time = int(time.time() * 1000.0)
|
||
elif hasattr(self, "is_behave") and self.is_behave:
|
||
self.__initialize_variables()
|
||
elif hasattr(self, "is_nosetest") and self.is_nosetest:
|
||
pass # Setup performed in plugins for pynose
|
||
else:
|
||
# Pure Python run. (Eg. SB() and Driver() Managers)
|
||
pass # Variables initialized in respective plugins
|
||
|
||
# Verify SeleniumBase is installed successfully, and used correctly
|
||
if not hasattr(self, "browser"):
|
||
raise Exception(
|
||
"SeleniumBase plugins DID NOT load!\n"
|
||
'Either "seleniumbase" is not installed on your system,\n'
|
||
'or you called "setUp()" directly without initialization!\n'
|
||
"*** To install SeleniumBase in Develop Mode from a clone:\n"
|
||
' >>> "pip install -e ." (Run in DIR with setup.py)\n'
|
||
"*** To install the latest SeleniumBase version from PyPI:\n"
|
||
' >>> "pip install -U seleniumbase" (Run in any DIR)\n'
|
||
'NOTE: "setUp()" can ONLY be called after vars are set.\n'
|
||
' If using "pytest", then "pytest_plugin.py" sets them.\n'
|
||
' The SeleniumBase "SB()" Manager can also set them.\n'
|
||
"See SeleniumBase/help_docs/syntax_formats.md for details!"
|
||
)
|
||
|
||
if not hasattr(sb_config, "_is_timeout_changed"):
|
||
# Should only be reachable from pure Python runs
|
||
sb_config._is_timeout_changed = False
|
||
sb_config._SMALL_TIMEOUT = settings.SMALL_TIMEOUT
|
||
sb_config._LARGE_TIMEOUT = settings.LARGE_TIMEOUT
|
||
|
||
if sb_config._is_timeout_changed:
|
||
if sb_config._SMALL_TIMEOUT and sb_config._LARGE_TIMEOUT:
|
||
settings.SMALL_TIMEOUT = sb_config._SMALL_TIMEOUT
|
||
settings.LARGE_TIMEOUT = sb_config._LARGE_TIMEOUT
|
||
|
||
if not hasattr(self, "_swiftshader"):
|
||
# Not swiftshader: options.add_argument("--disable-gpu")
|
||
self._swiftshader = False
|
||
|
||
if not hasattr(sb_config, "_recorded_actions"):
|
||
# Only filled when Recorder Mode is enabled
|
||
sb_config._recorded_actions = {}
|
||
sb_config._behave_recorded_actions = {}
|
||
|
||
if not hasattr(settings, "SWITCH_TO_NEW_TABS_ON_CLICK"):
|
||
# If using an older settings file, set the new definitions manually
|
||
settings.SWITCH_TO_NEW_TABS_ON_CLICK = True
|
||
|
||
# Parse the settings file
|
||
if self.settings_file:
|
||
from seleniumbase.core import settings_parser
|
||
|
||
settings_parser.set_settings(self.settings_file)
|
||
|
||
# Set variables that may be useful to developers
|
||
self.log_abspath = os.path.abspath(self.log_path)
|
||
self.data_path = os.path.join(self.log_path, self.__get_test_id())
|
||
self.data_abspath = os.path.abspath(self.data_path)
|
||
|
||
# Add _test_logpath value to sb_config
|
||
test_id = self.__get_test_id()
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
sb_config._test_logpath = test_logpath
|
||
|
||
# Add _process_dashboard_entry method to sb_config
|
||
sb_config._process_dashboard_entry = self._process_dashboard_entry
|
||
|
||
# Add _add_pytest_html_extra method to sb_config
|
||
sb_config._add_pytest_html_extra = self._add_pytest_html_extra
|
||
|
||
# Add _process_visual_baseline_logs method to sb_config
|
||
sb_config._process_v_baseline_logs = self._process_visual_baseline_logs
|
||
|
||
# Add _log_fail_data method to sb_config
|
||
sb_config._log_fail_data = self._log_fail_data
|
||
|
||
# Reset the last_page_screenshot variables
|
||
sb_config._last_page_screenshot = None
|
||
sb_config._last_page_screenshot_png = None
|
||
|
||
# Indictate to pytest reports that SeleniumBase is being used
|
||
sb_config._sbase_detected = True
|
||
sb_config._only_unittest = False
|
||
|
||
# Mobile Emulator device metrics: CSS Width, CSS Height, & Pixel-Ratio
|
||
if self.device_metrics:
|
||
metrics_string = self.device_metrics
|
||
metrics_string = metrics_string.replace(" ", "")
|
||
metrics_list = metrics_string.split(",")
|
||
exception_string = (
|
||
"Invalid input for Mobile Emulator device metrics!\n"
|
||
"Expecting a comma-separated string with integer values\n"
|
||
"for Width/Height, and an int or float for Pixel-Ratio.\n"
|
||
'Example: --metrics="411,731,3" '
|
||
)
|
||
if len(metrics_list) != 3:
|
||
raise Exception(exception_string)
|
||
try:
|
||
self.__device_width = int(metrics_list[0])
|
||
self.__device_height = int(metrics_list[1])
|
||
self.__device_pixel_ratio = float(metrics_list[2])
|
||
self.mobile_emulator = True
|
||
except Exception:
|
||
raise Exception(exception_string)
|
||
|
||
window_position = self.window_position
|
||
if window_position:
|
||
if window_position.count(",") != 1:
|
||
message = (
|
||
'\n\n window_position expects an "x,y" string!'
|
||
'\n (Your input was: "%s")\n' % window_position
|
||
)
|
||
raise Exception(message)
|
||
window_position = window_position.replace(" ", "")
|
||
win_x = None
|
||
win_y = None
|
||
try:
|
||
win_x = int(window_position.split(",")[0])
|
||
win_y = int(window_position.split(",")[1])
|
||
except Exception:
|
||
message = (
|
||
'\n\n Expecting integer values for "x,y"!'
|
||
'\n (window_position input was: "%s")\n'
|
||
% window_position
|
||
)
|
||
raise Exception(message)
|
||
settings.WINDOW_START_X = win_x
|
||
settings.WINDOW_START_Y = win_y
|
||
|
||
window_size = self.window_size
|
||
if window_size:
|
||
if window_size.count(",") != 1:
|
||
message = (
|
||
'\n\n window_size expects a "width,height" string!'
|
||
'\n (Your input was: "%s")\n' % window_size
|
||
)
|
||
raise Exception(message)
|
||
window_size = window_size.replace(" ", "")
|
||
width = None
|
||
height = None
|
||
try:
|
||
width = int(window_size.split(",")[0])
|
||
height = int(window_size.split(",")[1])
|
||
except Exception:
|
||
message = (
|
||
'\n\n Expecting integer values for "width,height"!'
|
||
'\n (window_size input was: "%s")\n' % window_size
|
||
)
|
||
raise Exception(message)
|
||
settings.CHROME_START_WIDTH = width
|
||
settings.CHROME_START_HEIGHT = height
|
||
settings.HEADLESS_START_WIDTH = width
|
||
settings.HEADLESS_START_HEIGHT = height
|
||
|
||
if self.xvfb_metrics:
|
||
metrics_string = self.xvfb_metrics
|
||
metrics_string = metrics_string.replace(" ", "")
|
||
metrics_list = metrics_string.split(",")[0:2]
|
||
exception_string = (
|
||
"Invalid input for xvfb_metrics!\n"
|
||
"Expecting a comma-separated string\n"
|
||
"with integer values for Width/Height.\n"
|
||
'Eg. --xvfb-metrics="1920,1080".\n'
|
||
"(Minimum: 1024,768) (Default: 1366,768)"
|
||
)
|
||
if len(metrics_list) != 2:
|
||
raise Exception(exception_string)
|
||
try:
|
||
self._xvfb_width = int(metrics_list[0])
|
||
self._xvfb_height = int(metrics_list[1])
|
||
# The minimum width,height is: 1024,768
|
||
if self._xvfb_width < 1024:
|
||
self._xvfb_width = 1024
|
||
sb_config._xvfb_width = self._xvfb_width
|
||
if self._xvfb_height < 768:
|
||
self._xvfb_height = 768
|
||
sb_config._xvfb_height = self._xvfb_height
|
||
self.xvfb = True
|
||
except Exception:
|
||
raise Exception(exception_string)
|
||
|
||
if self.mobile_emulator and not self.user_agent:
|
||
# Use a Pixel user agent by default if not specified
|
||
self.user_agent = constants.Mobile.AGENT
|
||
|
||
if self.browser in ["firefox", "ie", "safari"]:
|
||
# The Recorder Mode browser extension is only for Chrome/Edge.
|
||
if self.recorder_mode:
|
||
message = (
|
||
"Recorder Mode ONLY supports Chrome and Edge!\n"
|
||
'(Your browser choice was: "%s")' % self.browser
|
||
)
|
||
raise Exception(message)
|
||
|
||
if not hasattr(self, "is_nosetest") or not self.is_nosetest:
|
||
# Xvfb Virtual Display activation for Linux
|
||
self.__activate_virtual_display_as_needed()
|
||
|
||
# Dashboard pre-processing:
|
||
if self.dashboard:
|
||
if self._multithreaded:
|
||
with self.dash_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(
|
||
constants.Dashboard.LOCKFILE
|
||
)
|
||
if not self._dash_initialized:
|
||
sb_config._dashboard_initialized = True
|
||
self._dash_initialized = True
|
||
self.__process_dashboard(False, init=True)
|
||
else:
|
||
if not self._dash_initialized:
|
||
sb_config._dashboard_initialized = True
|
||
self._dash_initialized = True
|
||
self.__process_dashboard(False, init=True)
|
||
|
||
# Set the JS start time for Recorder Mode.
|
||
# Use this to skip saving recorded actions from previous tests.
|
||
if self.recorder_mode:
|
||
self.__js_start_time = int(time.time() * 1000.0)
|
||
|
||
has_url = False
|
||
if self._reuse_session:
|
||
if not hasattr(sb_config, "shared_driver"):
|
||
sb_config.shared_driver = None
|
||
if sb_config.shared_driver:
|
||
with suppress(Exception):
|
||
self._default_driver = sb_config.shared_driver
|
||
self.driver = sb_config.shared_driver
|
||
self._drivers_list = [sb_config.shared_driver]
|
||
url = self.get_current_url()
|
||
if url is not None:
|
||
has_url = True
|
||
if len(self.driver.window_handles) > 1:
|
||
while len(self.driver.window_handles) > 1:
|
||
self.switch_to_window(-1)
|
||
self.driver.close()
|
||
self.switch_to_window(0)
|
||
if self._crumbs:
|
||
if self.binary_location == "chs":
|
||
self.delete_session_storage()
|
||
else:
|
||
self.wait_for_ready_state_complete()
|
||
with suppress(Exception):
|
||
self.driver.delete_all_cookies()
|
||
if self._reuse_session and sb_config.shared_driver and has_url:
|
||
good_start_page = False
|
||
if self.recorder_ext:
|
||
self.__js_start_time = int(time.time() * 1000.0)
|
||
if self.start_page and len(self.start_page) >= 4:
|
||
if page_utils.is_valid_url(self.start_page):
|
||
good_start_page = True
|
||
self.__new_window_on_rec_open = False
|
||
self.open(self.start_page)
|
||
self.__new_window_on_rec_open = True
|
||
else:
|
||
new_start_page = "https://" + self.start_page
|
||
if page_utils.is_valid_url(new_start_page):
|
||
good_start_page = True
|
||
self.__dont_record_open = True
|
||
self.open(new_start_page)
|
||
self.__dont_record_open = False
|
||
if self.recorder_ext or (self._crumbs and not good_start_page):
|
||
if self.get_current_url() != "about:blank":
|
||
self.__new_window_on_rec_open = False
|
||
self.open("about:blank")
|
||
self.__new_window_on_rec_open = True
|
||
if self.recorder_ext:
|
||
self.__js_start_time = int(time.time() * 1000.0)
|
||
else:
|
||
# Launch WebDriver for both pytest and pynose
|
||
self.driver = self.get_new_driver(
|
||
browser=self.browser,
|
||
headless=self.headless,
|
||
locale_code=self.locale_code,
|
||
protocol=self.protocol,
|
||
servername=self.servername,
|
||
port=self.port,
|
||
proxy=self.proxy_string,
|
||
proxy_bypass_list=self.proxy_bypass_list,
|
||
proxy_pac_url=self.proxy_pac_url,
|
||
multi_proxy=self.multi_proxy,
|
||
agent=self.user_agent,
|
||
switch_to=True,
|
||
cap_file=self.cap_file,
|
||
cap_string=self.cap_string,
|
||
recorder_ext=self.recorder_ext,
|
||
disable_cookies=self.disable_cookies,
|
||
disable_js=self.disable_js,
|
||
disable_csp=self.disable_csp,
|
||
enable_ws=self.enable_ws,
|
||
enable_sync=self.enable_sync,
|
||
use_auto_ext=self.use_auto_ext,
|
||
undetectable=self.undetectable,
|
||
uc_cdp_events=self.uc_cdp_events,
|
||
uc_subprocess=self.uc_subprocess,
|
||
log_cdp_events=self.log_cdp_events,
|
||
no_sandbox=self.no_sandbox,
|
||
disable_gpu=self.disable_gpu,
|
||
headless1=self.headless1,
|
||
headless2=self.headless2,
|
||
incognito=self.incognito,
|
||
guest_mode=self.guest_mode,
|
||
dark_mode=self.dark_mode,
|
||
devtools=self.devtools,
|
||
remote_debug=self.remote_debug,
|
||
enable_3d_apis=self.enable_3d_apis,
|
||
swiftshader=self._swiftshader,
|
||
ad_block_on=self.ad_block_on,
|
||
host_resolver_rules=self.host_resolver_rules,
|
||
block_images=self.block_images,
|
||
do_not_track=self.do_not_track,
|
||
chromium_arg=self.chromium_arg,
|
||
firefox_arg=self.firefox_arg,
|
||
firefox_pref=self.firefox_pref,
|
||
user_data_dir=self.user_data_dir,
|
||
extension_zip=self.extension_zip,
|
||
extension_dir=self.extension_dir,
|
||
disable_features=self.disable_features,
|
||
binary_location=self.binary_location,
|
||
driver_version=self.driver_version,
|
||
page_load_strategy=self.page_load_strategy,
|
||
use_wire=self.use_wire,
|
||
external_pdf=self.external_pdf,
|
||
is_mobile=self.mobile_emulator,
|
||
d_width=self.__device_width,
|
||
d_height=self.__device_height,
|
||
d_p_r=self.__device_pixel_ratio,
|
||
)
|
||
try:
|
||
if self.driver.timeouts.implicit_wait > 0:
|
||
self.driver.implicitly_wait(0)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.driver.implicitly_wait(0)
|
||
self._default_driver = self.driver
|
||
if self._reuse_session:
|
||
sb_config.shared_driver = self.driver
|
||
if len(self._drivers_list) == 0:
|
||
# The user is overriding self.get_new_driver()
|
||
# (Otherwise this code shouldn't be reachable)
|
||
self._drivers_list.append(self.driver)
|
||
self._drivers_browser_map[self.driver] = self.browser
|
||
|
||
if self.browser in ["firefox", "ie", "safari"]:
|
||
# Only Chrome and Edge browsers have the mobile emulator.
|
||
# Some actions such as hover-clicking are different on mobile.
|
||
self.mobile_emulator = False
|
||
|
||
# Configure the test time limit (if used).
|
||
self.set_time_limit(self.time_limit)
|
||
|
||
# Configure the page load timeout
|
||
with suppress(Exception):
|
||
if hasattr(settings, "PAGE_LOAD_TIMEOUT"):
|
||
self.driver.set_page_load_timeout(settings.PAGE_LOAD_TIMEOUT)
|
||
else:
|
||
self.driver.set_page_load_timeout(120) # Selenium uses 300
|
||
|
||
# Set the start time for the test (in ms).
|
||
# Although the pytest clock starts before setUp() begins,
|
||
# the time-limit clock starts at the end of the setUp() method.
|
||
sb_config.start_time_ms = int(time.time() * 1000.0)
|
||
self.__start_time_ms = sb_config.start_time_ms
|
||
|
||
def __set_last_page_screenshot(self):
|
||
"""self.__last_page_screenshot is only for pytest html report logs.
|
||
self.__last_page_screenshot_png is for all screenshot log files."""
|
||
SCREENSHOT_SKIPPED = constants.Warnings.SCREENSHOT_SKIPPED
|
||
SCREENSHOT_UNDEFINED = constants.Warnings.SCREENSHOT_UNDEFINED
|
||
if (
|
||
hasattr(self, "no_screenshot_after_test")
|
||
and self.no_screenshot_after_test
|
||
):
|
||
from seleniumbase.core import encoded_images
|
||
|
||
NO_SCREENSHOT = encoded_images.get_no_screenshot_png()
|
||
self.__last_page_screenshot = NO_SCREENSHOT
|
||
self.__last_page_screenshot_png = SCREENSHOT_SKIPPED
|
||
sb_config._last_page_screenshot_png = NO_SCREENSHOT
|
||
return
|
||
element = None
|
||
if (
|
||
not self.__last_page_screenshot
|
||
and not self.__last_page_screenshot_png
|
||
):
|
||
with suppress(Exception):
|
||
try:
|
||
element = page_actions.wait_for_element_visible(
|
||
self.driver,
|
||
"body",
|
||
"css selector",
|
||
timeout=0.1,
|
||
ignore_test_time_limit=True,
|
||
)
|
||
except Exception:
|
||
element = page_actions.wait_for_element_present(
|
||
self.driver,
|
||
"body",
|
||
"css selector",
|
||
timeout=0.1,
|
||
ignore_test_time_limit=True,
|
||
)
|
||
try:
|
||
if (
|
||
hasattr(settings, "SCREENSHOT_WITH_BACKGROUND")
|
||
and settings.SCREENSHOT_WITH_BACKGROUND
|
||
):
|
||
self.__last_page_screenshot = (
|
||
self.driver.get_screenshot_as_base64()
|
||
)
|
||
else:
|
||
self.__last_page_screenshot = (
|
||
element.screenshot_as_base64
|
||
)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.__last_page_screenshot = (
|
||
self.driver.get_screenshot_as_base64()
|
||
)
|
||
if not self.__last_page_screenshot:
|
||
self.__last_page_screenshot = SCREENSHOT_UNDEFINED
|
||
self.__last_page_screenshot_png = SCREENSHOT_UNDEFINED
|
||
if element:
|
||
try:
|
||
self.__last_page_screenshot_png = (
|
||
element.screenshot_as_png
|
||
)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.__last_page_screenshot_png = (
|
||
self.driver.get_screenshot_as_png()
|
||
)
|
||
else:
|
||
import base64
|
||
|
||
try:
|
||
self.__last_page_screenshot_png = (
|
||
base64.b64decode(self.__last_page_screenshot)
|
||
)
|
||
except Exception:
|
||
if element:
|
||
try:
|
||
self.__last_page_screenshot_png = (
|
||
element.screenshot_as_png
|
||
)
|
||
except Exception:
|
||
with suppress(Exception):
|
||
self.__last_page_screenshot_png = (
|
||
self.driver.get_screenshot_as_png()
|
||
)
|
||
sb_config._last_page_screenshot_png = self.__last_page_screenshot_png
|
||
|
||
def __set_last_page_url(self):
|
||
if not self.__last_page_url:
|
||
try:
|
||
self.__last_page_url = log_helper.get_last_page(self.driver)
|
||
except Exception:
|
||
self.__last_page_url = None
|
||
|
||
def __set_last_page_source(self):
|
||
if not self.__last_page_source:
|
||
try:
|
||
self.__last_page_source = (
|
||
log_helper.get_html_source_with_base_href(
|
||
self.driver, self.driver.page_source
|
||
)
|
||
)
|
||
except Exception:
|
||
self.__last_page_source = (
|
||
constants.Warnings.PAGE_SOURCE_UNDEFINED
|
||
)
|
||
sb_config._last_page_source = self.__last_page_source
|
||
|
||
def __get_exception_info(self):
|
||
exc_message = None
|
||
if (
|
||
hasattr(self, "_outcome")
|
||
and hasattr(self._outcome, "errors")
|
||
and self._outcome.errors
|
||
):
|
||
try:
|
||
exc_message = self._outcome.errors[0][1][1]
|
||
except Exception:
|
||
exc_message = "(Unknown Exception)"
|
||
else:
|
||
try:
|
||
exc_message = sys.last_value
|
||
except Exception:
|
||
exc_message = "(Unknown Exception)"
|
||
return str(exc_message)
|
||
|
||
def __insert_test_result(self, state, err):
|
||
from seleniumbase.core.testcase_manager import TestcaseDataPayload
|
||
|
||
data_payload = TestcaseDataPayload()
|
||
data_payload.runtime = int(time.time() * 1000.0) - self.case_start_time
|
||
data_payload.guid = self.testcase_guid
|
||
data_payload.execution_guid = self.execution_guid
|
||
data_payload.state = state
|
||
if err:
|
||
import traceback
|
||
tb_string = traceback.format_exc()
|
||
if "Message: " in tb_string:
|
||
data_payload.message = (
|
||
"Message: " + tb_string.split("Message: ")[-1]
|
||
)
|
||
elif "Exception: " in tb_string:
|
||
data_payload.message = tb_string.split("Exception: ")[-1]
|
||
elif "Error: " in tb_string:
|
||
data_payload.message = tb_string.split("Error: ")[-1]
|
||
else:
|
||
data_payload.message = self.__get_exception_info()
|
||
else:
|
||
test_id = self.__get_test_id_2()
|
||
if (
|
||
self.is_pytest
|
||
and test_id in sb_config._results.keys()
|
||
and (sb_config._results[test_id] == "Skipped")
|
||
):
|
||
if self.__skip_reason:
|
||
data_payload.message = "Skipped: " + self.__skip_reason
|
||
else:
|
||
data_payload.message = "Skipped: (no reason given)"
|
||
self.testcase_manager.update_testcase_data(data_payload)
|
||
|
||
def _add_pytest_html_extra(self):
|
||
if not (
|
||
(python3_11_or_newer and py311_patch2)
|
||
or "--pdb" in sys.argv
|
||
):
|
||
return
|
||
self.__add_pytest_html_extra()
|
||
|
||
def __add_pytest_html_extra(self):
|
||
if not self.__added_pytest_html_extra:
|
||
with suppress(Exception):
|
||
if self.with_selenium:
|
||
if not self.__last_page_screenshot:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
if self.report_on:
|
||
extra_url = {}
|
||
extra_url["name"] = "URL"
|
||
extra_url["format"] = "url"
|
||
extra_url["format_type"] = "url"
|
||
extra_url["content"] = self.__last_page_url
|
||
extra_url["mime_type"] = None
|
||
extra_url["extension"] = None
|
||
extra_image = {}
|
||
extra_image["name"] = "Screenshot"
|
||
extra_image["format"] = "image"
|
||
extra_image["format_type"] = "image"
|
||
extra_image["content"] = self.__last_page_screenshot
|
||
extra_image["mime_type"] = "image/png"
|
||
extra_image["extension"] = "png"
|
||
self.__added_pytest_html_extra = True
|
||
if self.__last_page_screenshot != (
|
||
constants.Warnings.SCREENSHOT_UNDEFINED
|
||
):
|
||
self._html_report_extra.append(extra_url)
|
||
self._html_report_extra.append(extra_image)
|
||
|
||
def __delay_driver_quit(self):
|
||
delay_driver_quit = False
|
||
if (
|
||
hasattr(self, "_using_sb_fixture")
|
||
and self._using_sb_fixture
|
||
and "--pdb" in sys.argv
|
||
and self.__has_exception()
|
||
and len(self._drivers_list) == 1
|
||
and self.driver == self._default_driver
|
||
):
|
||
# Special case: Using sb fixture, --pdb, and has error.
|
||
# Keep the driver open for debugging and quit it later.
|
||
delay_driver_quit = True
|
||
return delay_driver_quit
|
||
|
||
def __quit_all_drivers(self):
|
||
if self._reuse_session and sb_config.shared_driver:
|
||
if len(self._drivers_list) > 0:
|
||
if self._drivers_list[0] != sb_config.shared_driver:
|
||
if sb_config.shared_driver in self._drivers_list:
|
||
self._drivers_list.remove(sb_config.shared_driver)
|
||
self._drivers_list.insert(0, sb_config.shared_driver)
|
||
self._default_driver = self._drivers_list[0]
|
||
self.switch_to_default_driver()
|
||
if len(self._drivers_list) > 1:
|
||
self._drivers_list = self._drivers_list[1:]
|
||
else:
|
||
self._drivers_list = []
|
||
# Close all open browser windows
|
||
delay_driver_quit = self.__delay_driver_quit()
|
||
self._drivers_list.reverse() # Last In, First Out
|
||
for driver in self._drivers_list:
|
||
try:
|
||
if (
|
||
not is_windows
|
||
or self.browser == "ie"
|
||
or self.servername != "localhost"
|
||
or (
|
||
hasattr(driver, "service")
|
||
and driver.service.process
|
||
)
|
||
):
|
||
if not delay_driver_quit:
|
||
driver.quit()
|
||
else:
|
||
# Save it for later to quit it later.
|
||
sb_config._sb_pdb_driver = driver
|
||
except AttributeError:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
if not delay_driver_quit:
|
||
self.driver = None
|
||
self._default_driver = None
|
||
self._drivers_list = []
|
||
|
||
def __has_exception(self):
|
||
has_exception = False
|
||
if hasattr(sys, "last_traceback") and sys.last_traceback is not None:
|
||
has_exception = True
|
||
elif hasattr(self, "is_context_manager") and self.is_context_manager:
|
||
if self.with_testing_base and self._has_failure:
|
||
return True
|
||
else:
|
||
return False
|
||
elif hasattr(self, "_outcome") and hasattr(self._outcome, "errors"):
|
||
if self._outcome.errors:
|
||
has_exception = True
|
||
else:
|
||
has_exception = sys.exc_info()[1] is not None
|
||
if self.__will_be_skipped and hasattr(self, "_using_sb_fixture"):
|
||
has_exception = False
|
||
return has_exception
|
||
|
||
def __get_test_id(self):
|
||
"""The id used in various places such as the test log path."""
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
file_name = sb_config.behave_scenario.filename
|
||
file_name = file_name.replace("/", ".").replace("\\", ".")
|
||
scenario_name = sb_config.behave_scenario.name
|
||
if " -- @" in scenario_name:
|
||
scenario_name = scenario_name.split(" # ")[0].rstrip()
|
||
scenario_name = re.sub(r"[^\w" + r"_ " + r"]", "", scenario_name)
|
||
scenario_name = scenario_name.replace(" ", "_")
|
||
test_id = "%s.%s" % (file_name, scenario_name)
|
||
return test_id
|
||
elif hasattr(self, "is_context_manager") and self.is_context_manager:
|
||
filename = self.__class__.__module__.split(".")[-1] + ".py"
|
||
methodname = self._testMethodName
|
||
context_id = None
|
||
if filename == "base_case.py" or methodname == "runTest":
|
||
import traceback
|
||
stack_base = traceback.format_stack()[0].split(", in ")[0]
|
||
test_base = stack_base.split(", in ")[0].split(os.sep)[-1]
|
||
if hasattr(self, "cm_filename") and self.cm_filename:
|
||
filename = self.cm_filename
|
||
else:
|
||
filename = test_base.split('"')[0]
|
||
methodname = ".line_" + test_base.split(", line ")[-1]
|
||
context_id = filename.split(".")[0] + methodname
|
||
return context_id
|
||
test_id = "%s.%s.%s" % (
|
||
self.__class__.__module__,
|
||
self.__class__.__name__,
|
||
self._testMethodName,
|
||
)
|
||
if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6:
|
||
test_id = self._sb_test_identifier
|
||
elif hasattr(self, "_using_sb_fixture") and self._using_sb_fixture:
|
||
test_id = sb_config._latest_display_id
|
||
test_id = test_id.replace(".py::", ".").replace("::", ".")
|
||
test_id = test_id.replace("/", ".").replace("\\", ".")
|
||
test_id = test_id.replace(" ", "_")
|
||
# Linux filename length limit for `codecs.open(filename)` = 255
|
||
# 255 - len("latest_logs/") - len("/basic_test_info.txt") = 223
|
||
if len(test_id) <= 223:
|
||
return test_id
|
||
else:
|
||
# 223 - len("__TRUNCATED__") = 210
|
||
# 210 / 2 = 105
|
||
return test_id[:105] + "__TRUNCATED__" + test_id[-105:]
|
||
|
||
def __get_test_id_2(self):
|
||
"""The id for SeleniumBase Dashboard entries."""
|
||
if "PYTEST_CURRENT_TEST" in os.environ:
|
||
full_name = os.environ["PYTEST_CURRENT_TEST"]
|
||
if "] " in full_name:
|
||
return full_name.split("] ")[0] + "]"
|
||
else:
|
||
return full_name.split(" ")[0]
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
return self.__get_test_id()
|
||
test_id = "%s.%s.%s" % (
|
||
self.__class__.__module__.split(".")[-1],
|
||
self.__class__.__name__,
|
||
self._testMethodName,
|
||
)
|
||
if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6:
|
||
test_id = self._sb_test_identifier
|
||
if test_id.count(".") > 1:
|
||
test_id = ".".join(test_id.split(".")[1:])
|
||
return test_id
|
||
|
||
def __get_display_id(self):
|
||
"""The id for running a test from pytest. (Displayed on Dashboard)"""
|
||
if "PYTEST_CURRENT_TEST" in os.environ:
|
||
full_name = os.environ["PYTEST_CURRENT_TEST"]
|
||
if "] " in full_name:
|
||
return full_name.split("] ")[0] + "]"
|
||
else:
|
||
return full_name.split(" ")[0]
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
file_name = sb_config.behave_scenario.filename
|
||
line_num = sb_config.behave_line_num
|
||
scenario_name = sb_config.behave_scenario.name
|
||
if " -- @" in scenario_name:
|
||
scenario_name = scenario_name.split(" # ")[0].rstrip()
|
||
test_id = "%s:%s => %s" % (file_name, line_num, scenario_name)
|
||
return test_id
|
||
test_id = "%s.py::%s::%s" % (
|
||
self.__class__.__module__.replace(".", "/"),
|
||
self.__class__.__name__,
|
||
self._testMethodName,
|
||
)
|
||
if self._sb_test_identifier and len(str(self._sb_test_identifier)) > 6:
|
||
test_id = self._sb_test_identifier
|
||
if hasattr(self, "_using_sb_fixture_class"):
|
||
if test_id.count(".") >= 2:
|
||
parts = test_id.split(".")
|
||
full = parts[-3] + ".py::" + parts[-2] + "::" + parts[-1]
|
||
test_id = full
|
||
elif hasattr(self, "_using_sb_fixture_no_class"):
|
||
if test_id.count(".") >= 1:
|
||
parts = test_id.split(".")
|
||
full = parts[-2] + ".py::" + parts[-1]
|
||
test_id = full
|
||
return test_id
|
||
|
||
def __get_filename(self):
|
||
"""The filename of the current SeleniumBase test. (NOT Path)"""
|
||
filename = None
|
||
if "PYTEST_CURRENT_TEST" in os.environ:
|
||
test_id = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
|
||
filename = test_id.split("::")[0].split("/")[-1]
|
||
elif hasattr(self, "is_behave") and self.is_behave:
|
||
filename = sb_config.behave_scenario.filename
|
||
filename = filename.split("/")[-1].split("\\")[-1]
|
||
else:
|
||
filename = self.__class__.__module__.split(".")[-1] + ".py"
|
||
return filename
|
||
|
||
def __create_log_path_as_needed(self, test_logpath):
|
||
if not os.path.exists(test_logpath):
|
||
try:
|
||
os.makedirs(test_logpath)
|
||
except Exception:
|
||
pass # Only reachable during multi-threaded runs
|
||
|
||
def _process_dashboard_entry(self, has_exception, init=False):
|
||
if self._multithreaded:
|
||
self.dash_lock = fasteners.InterProcessLock(
|
||
constants.Dashboard.LOCKFILE
|
||
)
|
||
with self.dash_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(constants.Dashboard.LOCKFILE)
|
||
self.__process_dashboard(has_exception, init)
|
||
else:
|
||
self.__process_dashboard(has_exception, init)
|
||
|
||
def __process_dashboard(self, has_exception, init=False):
|
||
"""SeleniumBase Dashboard Processing"""
|
||
if (
|
||
self.is_pytest
|
||
and "--pdb" in sys.argv
|
||
and has_exception
|
||
):
|
||
sb_config._pdb_failure = True
|
||
elif (
|
||
self.is_pytest
|
||
and hasattr(sb_config, "_pdb_failure")
|
||
and sb_config._pdb_failure
|
||
and not has_exception
|
||
):
|
||
return # Handle case where "pytest --pdb" marks failures as Passed
|
||
if self._multithreaded:
|
||
existing_res = sb_config._results # For recording "Skipped" tests
|
||
abs_path = os.path.abspath(".")
|
||
dash_json_loc = constants.Dashboard.DASH_JSON
|
||
dash_jsonpath = os.path.join(abs_path, dash_json_loc)
|
||
if not init and os.path.exists(dash_jsonpath):
|
||
with open(dash_jsonpath, "r") as f:
|
||
dash_json = f.read().strip()
|
||
dash_data, d_id, dash_rt, tlp, d_stats = json.loads(dash_json)
|
||
num_passed, num_failed, num_skipped, num_untested = d_stats
|
||
sb_config._results = dash_data
|
||
sb_config._display_id = d_id
|
||
sb_config._duration = dash_rt # Dashboard Run Time
|
||
sb_config._d_t_log_path = tlp # Test Log Path
|
||
sb_config.item_count_passed = num_passed
|
||
sb_config.item_count_failed = num_failed
|
||
sb_config.item_count_skipped = num_skipped
|
||
sb_config.item_count_untested = num_untested
|
||
if len(sb_config._extra_dash_entries) > 0:
|
||
# First take care of existing entries from non-SeleniumBase tests
|
||
for test_id in sb_config._extra_dash_entries:
|
||
if test_id in sb_config._results.keys():
|
||
if sb_config._results[test_id] == "Skipped":
|
||
sb_config.item_count_skipped += 1
|
||
sb_config.item_count_untested -= 1
|
||
elif sb_config._results[test_id] == "Failed":
|
||
sb_config.item_count_failed += 1
|
||
sb_config.item_count_untested -= 1
|
||
elif sb_config._results[test_id] == "Passed":
|
||
sb_config.item_count_passed += 1
|
||
sb_config.item_count_untested -= 1
|
||
else: # Mark "Skipped" if unknown
|
||
sb_config.item_count_skipped += 1
|
||
sb_config.item_count_untested -= 1
|
||
sb_config._extra_dash_entries = [] # Reset the list to empty
|
||
# Process new entries
|
||
log_dir = self.log_path
|
||
ft_id = self.__get_test_id() # Full test id with path to log files
|
||
test_id = self.__get_test_id_2() # The test id used by the DashBoard
|
||
dud = "seleniumbase/plugins/pytest_plugin.py::BaseClass::base_method"
|
||
dud2 = "pytest_plugin.BaseClass.base_method"
|
||
if hasattr(self, "_using_sb_fixture") and self.__will_be_skipped:
|
||
test_id = sb_config._test_id
|
||
if not init:
|
||
duration_ms = int(time.time() * 1000.0) - self.__start_time_ms
|
||
duration = float(duration_ms) / 1000.0
|
||
duration = "{:.2f}".format(duration)
|
||
sb_config._duration[test_id] = duration
|
||
if (
|
||
has_exception
|
||
or self.save_screenshot_after_test
|
||
or self.__screenshot_count > 0
|
||
or self.__logs_data_count > 0
|
||
or self.__level_0_visual_f
|
||
or self.__will_be_skipped
|
||
):
|
||
sb_config._d_t_log_path[test_id] = os.path.join(log_dir, ft_id)
|
||
else:
|
||
sb_config._d_t_log_path[test_id] = None
|
||
if test_id not in sb_config._display_id.keys():
|
||
sb_config._display_id[test_id] = self.__get_display_id()
|
||
if sb_config._display_id[test_id] == dud:
|
||
return
|
||
if (
|
||
hasattr(self, "_using_sb_fixture")
|
||
and test_id not in sb_config._results.keys()
|
||
):
|
||
if test_id.count(".") > 1:
|
||
alt_test_id = ".".join(test_id.split(".")[1:])
|
||
if alt_test_id in sb_config._results.keys():
|
||
sb_config._results.pop(alt_test_id)
|
||
elif test_id.count(".") == 1:
|
||
alt_test_id = sb_config._display_id[test_id]
|
||
alt_test_id = alt_test_id.replace(".py::", ".")
|
||
alt_test_id = alt_test_id.replace("::", ".")
|
||
alt_test_id = alt_test_id.replace(" ", "_")
|
||
if alt_test_id in sb_config._results.keys():
|
||
sb_config._results.pop(alt_test_id)
|
||
if test_id in sb_config._results.keys() and (
|
||
sb_config._results[test_id] == "Skipped"
|
||
):
|
||
if self.__passed_then_skipped:
|
||
# Multiple calls of setUp() and tearDown() in the same test
|
||
sb_config.item_count_passed -= 1
|
||
sb_config.item_count_untested += 1
|
||
self.__passed_then_skipped = False
|
||
sb_config._results[test_id] = "Skipped"
|
||
sb_config.item_count_skipped += 1
|
||
sb_config.item_count_untested -= 1
|
||
elif (
|
||
self._multithreaded
|
||
and test_id in existing_res.keys()
|
||
and existing_res[test_id] == "Skipped"
|
||
):
|
||
sb_config._results[test_id] = "Skipped"
|
||
sb_config.item_count_skipped += 1
|
||
sb_config.item_count_untested -= 1
|
||
elif has_exception:
|
||
if test_id not in sb_config._results.keys():
|
||
sb_config._results[test_id] = "Failed"
|
||
sb_config.item_count_failed += 1
|
||
sb_config.item_count_untested -= 1
|
||
elif not sb_config._results[test_id] == "Failed":
|
||
# tearDown() was called more than once in the test
|
||
if sb_config._results[test_id] == "Passed":
|
||
# Passed earlier, but last run failed
|
||
sb_config._results[test_id] = "Failed"
|
||
sb_config.item_count_failed += 1
|
||
sb_config.item_count_passed -= 1
|
||
else:
|
||
sb_config._results[test_id] = "Failed"
|
||
sb_config.item_count_failed += 1
|
||
sb_config.item_count_untested -= 1
|
||
else:
|
||
# pytest-rerunfailures caused a duplicate failure
|
||
sb_config._results[test_id] = "Failed"
|
||
else:
|
||
if (
|
||
test_id in sb_config._results.keys()
|
||
and sb_config._results[test_id] == "Failed"
|
||
):
|
||
# pytest-rerunfailures reran a test that failed
|
||
sb_config._d_t_log_path[test_id] = os.path.join(
|
||
log_dir, ft_id
|
||
)
|
||
sb_config.item_count_failed -= 1
|
||
sb_config.item_count_untested += 1
|
||
elif (
|
||
test_id in sb_config._results.keys()
|
||
and sb_config._results[test_id] == "Passed"
|
||
):
|
||
# tearDown() was called more than once in the test
|
||
sb_config.item_count_passed -= 1
|
||
sb_config.item_count_untested += 1
|
||
sb_config._results[test_id] = "Passed"
|
||
sb_config.item_count_passed += 1
|
||
sb_config.item_count_untested -= 1
|
||
else:
|
||
pass # Only initialize the Dashboard on the first processing
|
||
num_passed = sb_config.item_count_passed
|
||
num_failed = sb_config.item_count_failed
|
||
num_skipped = sb_config.item_count_skipped
|
||
num_untested = sb_config.item_count_untested
|
||
self.create_pie_chart(title=constants.Dashboard.TITLE)
|
||
self.add_data_point("Passed", num_passed, color="#84d474")
|
||
self.add_data_point("Untested", num_untested, color="#eaeaea")
|
||
self.add_data_point("Skipped", num_skipped, color="#efd8b4")
|
||
self.add_data_point("Failed", num_failed, color="#f17476")
|
||
style = (
|
||
'<link rel="stylesheet" charset="utf-8" '
|
||
'href="%s">' % constants.Dashboard.STYLE_CSS
|
||
)
|
||
auto_refresh_html = ""
|
||
if num_untested > 0:
|
||
# Refresh every X seconds when waiting for more test results
|
||
auto_refresh_html = constants.Dashboard.META_REFRESH_HTML
|
||
else:
|
||
# The tests are complete
|
||
if sb_config._using_html_report:
|
||
# Add the pie chart to the pytest html report
|
||
sb_config._saved_dashboard_pie = self.extract_chart()
|
||
if self._multithreaded:
|
||
abs_path = os.path.abspath(".")
|
||
dash_pie = json.dumps(sb_config._saved_dashboard_pie)
|
||
dash_pie_loc = constants.Dashboard.DASH_PIE
|
||
pie_path = os.path.join(abs_path, dash_pie_loc)
|
||
pie_file = codecs.open(pie_path, "w+", encoding="utf-8")
|
||
pie_file.writelines(dash_pie)
|
||
pie_file.close()
|
||
DASH_PIE_PNG_1 = constants.Dashboard.get_dash_pie_1()
|
||
head = (
|
||
'<head><meta charset="utf-8">'
|
||
'<meta name="viewport" content="shrink-to-fit=no">'
|
||
'<link rel="shortcut icon" href="%s">'
|
||
"%s"
|
||
"<title>Dashboard</title>"
|
||
"%s</head>"
|
||
% (DASH_PIE_PNG_1, auto_refresh_html, style)
|
||
)
|
||
table_html = (
|
||
"<div></div>"
|
||
'<table border="1px solid #e6e6e6;" width="100%;" padding: 5px;'
|
||
' font-size="12px;" text-align="left;" id="results-table">'
|
||
'<thead id="results-table-head">'
|
||
'<tr style="background-color: #F7F7FD;">'
|
||
'<th col="result">Result</th><th col="name">Test</th>'
|
||
'<th col="duration">Duration</th><th col="links">Links</th>'
|
||
"</tr></thead>"
|
||
)
|
||
the_failed = []
|
||
the_skipped = []
|
||
the_passed_hl = [] # Passed and has logs
|
||
the_passed_nl = [] # Passed and no logs
|
||
the_untested = []
|
||
if dud2 in sb_config._results.keys():
|
||
sb_config._results.pop(dud2)
|
||
for key in sb_config._results.keys():
|
||
t_res = sb_config._results[key]
|
||
t_dur = sb_config._duration[key]
|
||
t_d_id = sb_config._display_id[key]
|
||
t_l_path = sb_config._d_t_log_path[key]
|
||
res_low = t_res.lower()
|
||
if sb_config._results[key] == "Failed":
|
||
if not sb_config._d_t_log_path[key]:
|
||
sb_config._d_t_log_path[key] = os.path.join(log_dir, ft_id)
|
||
the_failed.append([res_low, t_res, t_d_id, t_dur, t_l_path])
|
||
elif sb_config._results[key] == "Skipped":
|
||
the_skipped.append([res_low, t_res, t_d_id, t_dur, t_l_path])
|
||
elif sb_config._results[key] == "Passed" and t_l_path:
|
||
the_passed_hl.append([res_low, t_res, t_d_id, t_dur, t_l_path])
|
||
elif sb_config._results[key] == "Passed" and not t_l_path:
|
||
the_passed_nl.append([res_low, t_res, t_d_id, t_dur, t_l_path])
|
||
elif sb_config._results[key] == "Untested":
|
||
the_untested.append([res_low, t_res, t_d_id, t_dur, t_l_path])
|
||
for row in the_failed:
|
||
row = (
|
||
'<tbody class="%s results-table-row">'
|
||
'<tr style="background-color: #FFF8F8;">'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
'<td><a href="%s">Logs</a> / <a href="%s/">Data</a>'
|
||
"</td></tr></tbody>"
|
||
"" % (row[0], row[1], row[2], row[3], log_dir, row[4])
|
||
)
|
||
table_html += row
|
||
for row in the_skipped:
|
||
if not row[4]:
|
||
row = (
|
||
'<tbody class="%s results-table-row">'
|
||
'<tr style="background-color: #FEFEF9;">'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
"<td>-</td></tr></tbody>"
|
||
% (row[0], row[1], row[2], row[3])
|
||
)
|
||
else:
|
||
row = (
|
||
'<tbody class="%s results-table-row">'
|
||
'<tr style="background-color: #FEFEF9;">'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
'<td><a href="%s">Logs</a> / <a href="%s/">Data</a>'
|
||
"</td></tr></tbody>"
|
||
"" % (row[0], row[1], row[2], row[3], log_dir, row[4])
|
||
)
|
||
table_html += row
|
||
for row in the_passed_hl:
|
||
# Passed and has logs
|
||
row = (
|
||
'<tbody class="%s results-table-row">'
|
||
'<tr style="background-color: #F8FFF8;">'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
'<td><a href="%s">Logs</a> / <a href="%s/">Data</a>'
|
||
"</td></tr></tbody>"
|
||
"" % (row[0], row[1], row[2], row[3], log_dir, row[4])
|
||
)
|
||
table_html += row
|
||
for row in the_passed_nl:
|
||
# Passed and no logs
|
||
row = (
|
||
'<tbody class="%s results-table-row">'
|
||
'<tr style="background-color: #F8FFF8;">'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
"<td>-</td></tr></tbody>" % (row[0], row[1], row[2], row[3])
|
||
)
|
||
table_html += row
|
||
for row in the_untested:
|
||
row = (
|
||
'<tbody class="%s results-table-row"><tr>'
|
||
'<td class="col-result">%s</td><td>%s</td><td>%s</td>'
|
||
"<td>-</td></tr></tbody>" % (row[0], row[1], row[2], row[3])
|
||
)
|
||
table_html += row
|
||
table_html += "</table>"
|
||
add_more = "<br /><b>Last updated:</b> "
|
||
timestamp, the_date, the_time = log_helper.get_master_time()
|
||
last_updated = "%s at %s" % (the_date, the_time)
|
||
add_more = add_more + "%s" % last_updated
|
||
status = "<p></p><div><b>Status:</b> Awaiting results..."
|
||
status += " (Refresh the page for updates)"
|
||
if num_untested == 0:
|
||
status = "<p></p><div><b>Status:</b> Test Run Complete:"
|
||
if num_failed == 0:
|
||
if num_passed > 0:
|
||
if num_skipped == 0:
|
||
status += " <b>Success!</b> (All tests passed)"
|
||
else:
|
||
status += " <b>Success!</b> (No failing tests)"
|
||
else:
|
||
status += " All tests were skipped!"
|
||
else:
|
||
latest_logs_dir = constants.Logs.LATEST + "/"
|
||
log_msg = "See latest logs for details"
|
||
if num_failed == 1:
|
||
status += (
|
||
" <b>1 test failed!</b> --- "
|
||
'(<b><a href="%s">%s</a></b>)'
|
||
"" % (latest_logs_dir, log_msg)
|
||
)
|
||
else:
|
||
status += (
|
||
" <b>%s tests failed!</b> --- "
|
||
'(<b><a href="%s">%s</a></b>)'
|
||
"" % (num_failed, latest_logs_dir, log_msg)
|
||
)
|
||
status += "</div><p></p>"
|
||
add_more = add_more + status
|
||
gen_by = (
|
||
'<p><div>Generated by: <b><a href="https://seleniumbase.io/">'
|
||
"SeleniumBase</a></b></div></p><p></p>"
|
||
)
|
||
add_more = add_more + gen_by
|
||
# Have dashboard auto-refresh on updates when using an http server
|
||
refresh_line = (
|
||
'<script type="text/javascript" src="%s">'
|
||
"</script>" % constants.Dashboard.LIVE_JS
|
||
)
|
||
if num_untested == 0 and sb_config._using_html_report:
|
||
sb_config._dash_final_summary = status
|
||
add_more = add_more + refresh_line
|
||
the_html = (
|
||
'<html lang="en">'
|
||
+ head
|
||
+ self.extract_chart()
|
||
+ table_html
|
||
+ add_more
|
||
)
|
||
abs_path = os.path.abspath(".")
|
||
file_path = os.path.join(abs_path, "dashboard.html")
|
||
out_file = codecs.open(file_path, "w+", encoding="utf-8")
|
||
out_file.writelines(the_html)
|
||
out_file.close()
|
||
sb_config._dash_html = the_html
|
||
if self._multithreaded:
|
||
d_stats = (num_passed, num_failed, num_skipped, num_untested)
|
||
_results = sb_config._results
|
||
_display_id = sb_config._display_id
|
||
_rt = sb_config._duration # Run Time (RT)
|
||
_tlp = sb_config._d_t_log_path # Test Log Path (TLP)
|
||
dash_json = json.dumps((_results, _display_id, _rt, _tlp, d_stats))
|
||
dash_json_loc = constants.Dashboard.DASH_JSON
|
||
dash_jsonpath = os.path.join(abs_path, dash_json_loc)
|
||
dash_json_file = codecs.open(dash_jsonpath, "w+", encoding="utf-8")
|
||
dash_json_file.writelines(dash_json)
|
||
dash_json_file.close()
|
||
|
||
def __activate_behave_post_mortem_debug_mode(self):
|
||
"""Activate Post Mortem Debug Mode for failing tests that use Behave"""
|
||
import pdb
|
||
|
||
pdb.post_mortem(sb_config.behave_step.exc_traceback)
|
||
# Post Mortem Debug Mode ("behave -D pdb")
|
||
|
||
def __activate_sb_mgr_post_mortem_debug_mode(self):
|
||
"""Activate Post Mortem Debug Mode for failing tests that use SB Mgr"""
|
||
import pdb
|
||
|
||
pdb.post_mortem()
|
||
# Post Mortem Debug Mode ("python --pdb")
|
||
|
||
def __activate_debug_mode_in_teardown(self):
|
||
"""Activate Final Trace / Debug Mode"""
|
||
import pdb
|
||
|
||
pdb.set_trace()
|
||
# Final Trace ("--ftrace")
|
||
|
||
def has_exception(self):
|
||
"""(This method should ONLY be used in custom tearDown() methods.)
|
||
This method returns True if the test failed or raised an exception.
|
||
This is useful for performing additional steps in your tearDown()
|
||
method (based on whether or not the test passed or failed).
|
||
Example use cases:
|
||
* Performing cleanup steps if a test didn't complete.
|
||
* Sending test data and/or results to a dashboard service.
|
||
"""
|
||
return self.__has_exception()
|
||
|
||
def save_teardown_screenshot(self):
|
||
"""(Should ONLY be used at the start of custom tearDown() methods.)
|
||
This method takes a screenshot of the active page for FAILING tests
|
||
(or when using "--screenshot" / "--save-screenshot" / "--ss").
|
||
That way your tearDown() method can navigate away from the last
|
||
page where the test failed, and still get the correct screenshot
|
||
before performing tearDown() steps on other pages. If this method
|
||
is not included in your custom tearDown() method, a screenshot
|
||
will still be taken after calling "super().tearDown()" there.
|
||
This method also saves recorded actions when using Recorder Mode.
|
||
"""
|
||
try:
|
||
self.__check_scope()
|
||
except Exception:
|
||
return
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
# In case tearDown() leaves the origin, save actions first.
|
||
self.save_recorded_actions()
|
||
if (
|
||
self.__has_exception()
|
||
or self.save_screenshot_after_test
|
||
or (python3_11_or_newer and py311_patch2)
|
||
or "--pdb" in sys.argv
|
||
):
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
if self.__has_exception() or self.save_screenshot_after_test:
|
||
if self.is_pytest:
|
||
self.__add_pytest_html_extra()
|
||
|
||
def _log_fail_data(self):
|
||
if not (
|
||
(python3_11_or_newer and py311_patch2)
|
||
or "--pdb" in sys.argv
|
||
):
|
||
return
|
||
test_id = self.__get_test_id()
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
log_helper.log_test_failure_data(
|
||
self,
|
||
test_logpath,
|
||
self.driver,
|
||
self.browser,
|
||
self.__last_page_url,
|
||
)
|
||
|
||
def _get_browser_version(self):
|
||
driver_capabilities = None
|
||
if hasattr(self, "driver") and hasattr(self.driver, "capabilities"):
|
||
driver_capabilities = self.driver.capabilities
|
||
elif hasattr(sb_config, "_browser_version"):
|
||
return sb_config._browser_version
|
||
else:
|
||
return "(Unknown Version)"
|
||
if "browserVersion" in driver_capabilities:
|
||
browser_version = driver_capabilities["browserVersion"]
|
||
else:
|
||
browser_version = "(Unknown Version)"
|
||
return browser_version
|
||
|
||
def _get_driver_name_and_version(self):
|
||
if not hasattr(self.driver, "capabilities"):
|
||
if hasattr(sb_config, "_driver_name_version"):
|
||
return sb_config._driver_name_version
|
||
else:
|
||
return None
|
||
driver = self.driver
|
||
if "chrome" in driver.capabilities:
|
||
cap_dict = driver.capabilities["chrome"]
|
||
return (
|
||
"chromedriver", cap_dict["chromedriverVersion"].split(" ")[0]
|
||
)
|
||
elif "msedge" in driver.capabilities:
|
||
cap_dict = driver.capabilities["msedge"]
|
||
return (
|
||
"msedgedriver", cap_dict["msedgedriverVersion"].split(" ")[0]
|
||
)
|
||
elif driver.capabilities["browserName"].lower() == "firefox":
|
||
return (
|
||
"geckodriver", driver.capabilities["moz:geckodriverVersion"]
|
||
)
|
||
elif driver.capabilities["browserName"].lower() == "safari":
|
||
return ("safaridriver", self._get_browser_version())
|
||
elif driver.capabilities["browserName"].lower() == "internet explorer":
|
||
return ("iedriver", self._get_browser_version())
|
||
else:
|
||
return None
|
||
|
||
def _get_num_handles(self):
|
||
return len(self.driver.window_handles)
|
||
|
||
def _get_rec_shift_esc_script(self):
|
||
return (
|
||
"""document.onkeydown = function(evt) {
|
||
evt = evt || window.event;
|
||
var isEscape = false;
|
||
if ("key" in evt) {
|
||
isEscape = (evt.key === "Escape" || evt.key === "Esc");
|
||
last_key = evt.key;
|
||
} else {
|
||
isEscape = (evt.keyCode === 27);
|
||
last_key = evt.keyCode;
|
||
if (last_key === 16) {
|
||
last_key = "Shift";
|
||
}
|
||
}
|
||
if (isEscape && document.sb_last_key === "Shift") {
|
||
document.sb_esc_end = "yes";
|
||
}
|
||
document.sb_last_key = last_key;
|
||
};"""
|
||
)
|
||
|
||
def _addSkip(self, result, test_case, reason):
|
||
"""This method should NOT be called directly from tests."""
|
||
addSkip = getattr(result, 'addSkip', None)
|
||
if addSkip is not None:
|
||
addSkip(test_case, reason)
|
||
else:
|
||
import warnings
|
||
warnings.warn(
|
||
"TestResult has no addSkip method! Skips not reported!",
|
||
RuntimeWarning, 2
|
||
)
|
||
result.addSuccess(test_case)
|
||
|
||
def _callTestMethod(self, method):
|
||
"""This method should NOT be called directly from tests."""
|
||
method()
|
||
|
||
def run(self, result=None):
|
||
"""Overwrite the unittest run() method for Python 3.11 or newer.
|
||
This method should NOT be called directly from tests."""
|
||
if not python3_11_or_newer:
|
||
return super().run(result=result)
|
||
if result is None:
|
||
result = self.defaultTestResult()
|
||
startTestRun = getattr(result, 'startTestRun', None)
|
||
stopTestRun = getattr(result, 'stopTestRun', None)
|
||
if startTestRun is not None:
|
||
startTestRun()
|
||
else:
|
||
stopTestRun = None
|
||
result.startTest(self)
|
||
try:
|
||
testMethod = getattr(self, self._testMethodName)
|
||
if (
|
||
getattr(self.__class__, "__unittest_skip__", False)
|
||
or getattr(testMethod, "__unittest_skip__", False)
|
||
):
|
||
skip_why = (
|
||
getattr(self.__class__, '__unittest_skip_why__', '')
|
||
or getattr(testMethod, '__unittest_skip_why__', '')
|
||
)
|
||
self._addSkip(result, self, skip_why)
|
||
return result
|
||
expecting_failure = (
|
||
getattr(self, "__unittest_expecting_failure__", False)
|
||
or getattr(testMethod, "__unittest_expecting_failure__", False)
|
||
)
|
||
outcome = unittest_helper._Outcome(result)
|
||
try:
|
||
self._outcome = outcome
|
||
with outcome.testPartExecutor(self):
|
||
self._callSetUp()
|
||
if outcome.success:
|
||
outcome.expecting_failure = expecting_failure
|
||
with outcome.testPartExecutor(self, isTest=True):
|
||
self._callTestMethod(testMethod)
|
||
outcome.expecting_failure = False
|
||
with outcome.testPartExecutor(self):
|
||
self._callTearDown()
|
||
self.doCleanups()
|
||
for test, reason in outcome.skipped:
|
||
self._addSkip(result, test, reason)
|
||
for test, exc_info in outcome.errors:
|
||
if exc_info is not None:
|
||
if issubclass(exc_info[0], self.failureException):
|
||
result.addFailure(test, exc_info)
|
||
else:
|
||
result.addError(test, exc_info)
|
||
if outcome.success:
|
||
if expecting_failure:
|
||
if outcome.expectedFailure:
|
||
self._addExpectedFailure(
|
||
result, outcome.expectedFailure
|
||
)
|
||
else:
|
||
self._addUnexpectedSuccess(result)
|
||
else:
|
||
result.addSuccess(self)
|
||
return result
|
||
finally:
|
||
outcome.errors.clear()
|
||
outcome.expectedFailure = None
|
||
self._outcome = None
|
||
finally:
|
||
result.stopTest(self)
|
||
if stopTestRun is not None:
|
||
stopTestRun()
|
||
|
||
def tearDown(self):
|
||
"""This method runs after every test completes.
|
||
Be careful if a subclass of BaseCase overrides setUp().
|
||
If so, add the following line to the subclass's tearDown() method:
|
||
super().tearDown() """
|
||
if not hasattr(self, "_using_sb_fixture") and self.__called_teardown:
|
||
# This test already called tearDown()
|
||
return
|
||
if hasattr(self, "recorder_mode") and self.recorder_mode:
|
||
page_actions._reconnect_if_disconnected(self.driver)
|
||
try:
|
||
self.__process_recorded_actions()
|
||
except Exception as e:
|
||
print("\n (Recorder) Code-generation exception:")
|
||
if hasattr(e, "msg"):
|
||
print("\n" + str(e.msg))
|
||
else:
|
||
print(e)
|
||
self.__called_teardown = True
|
||
self.__called_setup = False
|
||
try:
|
||
is_pytest = self.is_pytest # This fails if overriding setUp()
|
||
if is_pytest:
|
||
with_selenium = self.with_selenium
|
||
except Exception:
|
||
sub_class_name = (
|
||
str(self.__class__.__bases__[0]).split(".")[-1].split("'")[0]
|
||
)
|
||
sub_file_name = str(self.__class__.__bases__[0]).split(".")[-2]
|
||
sub_file_name = sub_file_name + ".py"
|
||
class_name = str(self.__class__).split(".")[-1].split("'")[0]
|
||
file_name = str(self.__class__).split(".")[-2] + ".py"
|
||
class_name_used = sub_class_name
|
||
file_name_used = sub_file_name
|
||
if sub_class_name == "BaseCase":
|
||
class_name_used = class_name
|
||
file_name_used = file_name
|
||
message = (
|
||
"You're overriding SeleniumBase's BaseCase setUp() "
|
||
"method with your own setUp() method, which breaks "
|
||
"SeleniumBase. You can fix this by going to your "
|
||
"%s class located in %s, and adding the "
|
||
"following line AT THE BEGINNING of your "
|
||
"setUp() method:\nsuper().setUp()\n\n"
|
||
"Also make sure the following line is AT THE END "
|
||
"of your tearDown() method:\nsuper().tearDown()\n"
|
||
% (class_name_used, file_name_used)
|
||
)
|
||
raise Exception(message)
|
||
# *** Start tearDown() officially ***
|
||
page_actions._reconnect_if_disconnected(self.driver)
|
||
self.__slow_mode_pause_if_active()
|
||
has_exception = self.__has_exception()
|
||
sb_config._has_exception = has_exception
|
||
sb_config._browser_version = self._get_browser_version()
|
||
sb_config._driver_name_version = self._get_driver_name_and_version()
|
||
|
||
if self.__overrided_default_timeouts:
|
||
# Reset default timeouts in case there are more tests
|
||
# These were changed in set_default_timeout()
|
||
if sb_config._SMALL_TIMEOUT and sb_config._LARGE_TIMEOUT:
|
||
settings.SMALL_TIMEOUT = sb_config._SMALL_TIMEOUT
|
||
settings.LARGE_TIMEOUT = sb_config._LARGE_TIMEOUT
|
||
sb_config._is_timeout_changed = False
|
||
self.__overrided_default_timeouts = False
|
||
deferred_exception = None
|
||
if self.__deferred_assert_failures:
|
||
print(
|
||
"\nWhen using self.deferred_assert_*() methods in your tests, "
|
||
"remember to call self.process_deferred_asserts() afterwards. "
|
||
"Now calling in tearDown()...\nFailures Detected:"
|
||
)
|
||
if not has_exception:
|
||
try:
|
||
self.process_deferred_asserts()
|
||
except Exception as e:
|
||
deferred_exception = e
|
||
else:
|
||
self.process_deferred_asserts(print_only=True)
|
||
if self.is_pytest:
|
||
# pytest-specific code
|
||
test_id = self.__get_test_id()
|
||
if with_selenium:
|
||
# Save a screenshot if logging is on when an exception occurs
|
||
if has_exception:
|
||
self.__add_pytest_html_extra()
|
||
sb_config._has_exception = True
|
||
sb_config._has_logs = True
|
||
if (
|
||
self.with_testing_base
|
||
and not has_exception
|
||
and self.save_screenshot_after_test
|
||
):
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if not self.__last_page_screenshot_png:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
log_helper.log_screenshot(
|
||
test_logpath,
|
||
self.driver,
|
||
self.__last_page_screenshot_png,
|
||
)
|
||
self.__add_pytest_html_extra()
|
||
sb_config._has_logs = True
|
||
elif (
|
||
(
|
||
(python3_11_or_newer and py311_patch2)
|
||
or "--pdb" in sys.argv
|
||
)
|
||
and not has_exception
|
||
):
|
||
# Handle a bug where exceptions aren't seen
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
if self.with_testing_base and has_exception:
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if (
|
||
not self.with_screen_shots
|
||
and not self.with_basic_test_info
|
||
and not self.with_page_source
|
||
):
|
||
# Log everything if nothing specified (if testing_base)
|
||
if not self.__last_page_screenshot_png:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
log_helper.log_screenshot(
|
||
test_logpath,
|
||
self.driver,
|
||
self.__last_page_screenshot_png,
|
||
)
|
||
log_helper.log_test_failure_data(
|
||
self,
|
||
test_logpath,
|
||
self.driver,
|
||
self.browser,
|
||
self.__last_page_url,
|
||
)
|
||
log_helper.log_page_source(
|
||
test_logpath, self.driver, self.__last_page_source
|
||
)
|
||
else:
|
||
if self.with_screen_shots:
|
||
if not self.__last_page_screenshot_png:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
log_helper.log_screenshot(
|
||
test_logpath,
|
||
self.driver,
|
||
self.__last_page_screenshot_png,
|
||
)
|
||
if self.with_basic_test_info:
|
||
log_helper.log_test_failure_data(
|
||
self,
|
||
test_logpath,
|
||
self.driver,
|
||
self.browser,
|
||
self.__last_page_url,
|
||
)
|
||
if self.with_page_source:
|
||
log_helper.log_page_source(
|
||
test_logpath,
|
||
self.driver,
|
||
self.__last_page_source,
|
||
)
|
||
if self.dashboard:
|
||
if self._multithreaded:
|
||
with self.dash_lock:
|
||
with suppress(Exception):
|
||
shared_utils.make_writable(
|
||
constants.Dashboard.LOCKFILE
|
||
)
|
||
self.__process_dashboard(has_exception)
|
||
else:
|
||
self.__process_dashboard(has_exception)
|
||
if self._final_debug:
|
||
self.__activate_debug_mode_in_teardown()
|
||
# (Pytest) Finally close all open browser windows
|
||
self.__quit_all_drivers()
|
||
if self.with_db_reporting:
|
||
if has_exception:
|
||
self.__insert_test_result(constants.State.FAILED, True)
|
||
else:
|
||
test_id = self.__get_test_id_2()
|
||
if test_id in sb_config._results.keys() and (
|
||
sb_config._results[test_id] == "Skipped"
|
||
):
|
||
self.__insert_test_result(
|
||
constants.State.SKIPPED, False
|
||
)
|
||
else:
|
||
self.__insert_test_result(
|
||
constants.State.PASSED, False
|
||
)
|
||
runtime = int(time.time() * 1000.0) - self.execution_start_time
|
||
self.testcase_manager.update_execution_data(
|
||
self.execution_guid, runtime
|
||
)
|
||
if self.with_s3_logging and has_exception:
|
||
"""If enabled, upload logs to S3 during test exceptions."""
|
||
import uuid
|
||
from seleniumbase.core.s3_manager import S3LoggingBucket
|
||
|
||
s3_bucket = S3LoggingBucket()
|
||
guid = str(uuid.uuid4().hex)
|
||
path = os.path.join(self.log_path, test_id)
|
||
uploaded_files = []
|
||
for logfile in os.listdir(path):
|
||
logfile_name = "%s/%s/%s" % (
|
||
guid,
|
||
test_id,
|
||
logfile.split(path)[-1],
|
||
)
|
||
s3_bucket.upload_file(
|
||
logfile_name, "%s" % os.path.join(path, logfile)
|
||
)
|
||
uploaded_files.append(logfile_name)
|
||
s3_bucket.save_uploaded_file_names(uploaded_files)
|
||
index_file = s3_bucket.upload_index_file(
|
||
test_id, guid, self.data_path, self.save_data_to_logs
|
||
)
|
||
print("\n*** Log files uploaded: ***\n%s\n" % index_file)
|
||
logging.info(
|
||
"\n*** Log files uploaded: ***\n%s\n" % index_file
|
||
)
|
||
if self.with_db_reporting:
|
||
from seleniumbase.core.testcase_manager import (
|
||
TestcaseDataPayload,
|
||
TestcaseManager,
|
||
)
|
||
|
||
self.testcase_manager = TestcaseManager(self.database_env)
|
||
data_payload = TestcaseDataPayload()
|
||
data_payload.guid = self.testcase_guid
|
||
data_payload.logURL = index_file
|
||
self.testcase_manager.update_testcase_log_url(data_payload)
|
||
else:
|
||
# (Pynose / Behave / Pure Python)
|
||
if hasattr(self, "is_behave") and self.is_behave:
|
||
if sb_config.behave_scenario.status.name == "failed":
|
||
has_exception = True
|
||
sb_config._has_exception = True
|
||
msg = " ❌ Scenario Failed! (Skipping remaining steps:)"
|
||
if is_windows:
|
||
c1 = colorama.Fore.RED + colorama.Back.LIGHTRED_EX
|
||
cr = colorama.Style.RESET_ALL
|
||
msg = msg.replace("❌", c1 + "><" + cr)
|
||
print(msg)
|
||
else:
|
||
msg = " ✅ Scenario Passed!"
|
||
if is_windows:
|
||
c2 = colorama.Fore.GREEN + colorama.Back.LIGHTGREEN_EX
|
||
cr = colorama.Style.RESET_ALL
|
||
msg = msg.replace("✅", c2 + "<>" + cr)
|
||
print(msg)
|
||
if self.dashboard:
|
||
self.__process_dashboard(has_exception)
|
||
if has_exception:
|
||
test_id = self.__get_test_id()
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
log_helper.log_test_failure_data(
|
||
self,
|
||
test_logpath,
|
||
self.driver,
|
||
self.browser,
|
||
self.__last_page_url,
|
||
)
|
||
if len(self._drivers_list) > 0:
|
||
if not self.__last_page_screenshot_png:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
log_helper.log_screenshot(
|
||
test_logpath,
|
||
self.driver,
|
||
self.__last_page_screenshot_png,
|
||
)
|
||
log_helper.log_page_source(
|
||
test_logpath, self.driver, self.__last_page_source
|
||
)
|
||
elif self.save_screenshot_after_test:
|
||
test_id = self.__get_test_id()
|
||
test_logpath = os.path.join(self.log_path, test_id)
|
||
self.__create_log_path_as_needed(test_logpath)
|
||
if not self.__last_page_screenshot_png:
|
||
self.__set_last_page_screenshot()
|
||
self.__set_last_page_url()
|
||
self.__set_last_page_source()
|
||
log_helper.log_screenshot(
|
||
test_logpath, self.driver, self.__last_page_screenshot_png
|
||
)
|
||
if self.report_on:
|
||
self._last_page_screenshot = self.__last_page_screenshot_png
|
||
try:
|
||
self._last_page_url = self.get_current_url()
|
||
except Exception:
|
||
self._last_page_url = "(Error: Unknown URL)"
|
||
if hasattr(self, "is_behave") and self.is_behave and has_exception:
|
||
if hasattr(sb_config, "pdb_option") and sb_config.pdb_option:
|
||
self.__activate_behave_post_mortem_debug_mode()
|
||
if self._final_debug:
|
||
self.__activate_debug_mode_in_teardown()
|
||
elif (
|
||
hasattr(sb_config, "_do_sb_post_mortem")
|
||
and sb_config._do_sb_post_mortem
|
||
):
|
||
self.__activate_sb_mgr_post_mortem_debug_mode()
|
||
elif (
|
||
hasattr(sb_config, "_do_sb_final_trace")
|
||
and sb_config._do_sb_final_trace
|
||
):
|
||
self.__activate_debug_mode_in_teardown()
|
||
# (Pynose / Behave / Pure Python) Close all open browser windows
|
||
self.__quit_all_drivers()
|
||
# Resume tearDown() for all test runners, (Pytest / Pynose / Behave)
|
||
if (
|
||
hasattr(self, "_xvfb_display")
|
||
and self._xvfb_display
|
||
and not self._reuse_session
|
||
):
|
||
# Stop the Xvfb virtual display launched from BaseCase
|
||
try:
|
||
if hasattr(self._xvfb_display, "stop"):
|
||
self._xvfb_display.stop()
|
||
self._xvfb_display = None
|
||
self.headless_active = False
|
||
except AttributeError:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
if (
|
||
hasattr(sb_config, "_virtual_display")
|
||
and sb_config._virtual_display
|
||
and hasattr(sb_config._virtual_display, "stop")
|
||
and (
|
||
not hasattr(sb_config, "reuse_session")
|
||
or (
|
||
hasattr(sb_config, "reuse_session")
|
||
and not sb_config.reuse_session
|
||
)
|
||
)
|
||
):
|
||
# CDP Mode may launch a 2nd Xvfb virtual display
|
||
try:
|
||
sb_config._virtual_display.stop()
|
||
sb_config._virtual_display = None
|
||
sb_config.headless_active = False
|
||
except AttributeError:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
if self.__visual_baseline_copies:
|
||
sb_config._visual_baseline_copies = True
|
||
if has_exception:
|
||
self.__process_visual_baseline_logs()
|
||
if deferred_exception:
|
||
# User forgot to call "self.process_deferred_asserts()" in test
|
||
raise deferred_exception
|
||
|
||
@classmethod
|
||
def setUpClass(self):
|
||
# Only used when: "--rcs" / "--reuse-class-session"
|
||
# Close existing sessions before the class starts.
|
||
session_helper.end_reused_class_session_as_needed()
|
||
|
||
@classmethod
|
||
def tearDownClass(self):
|
||
# Only used when: "--rcs" / "--reuse-class-session"
|
||
# Close existing sessions after the class finishes.
|
||
session_helper.end_reused_class_session_as_needed()
|
||
|
||
|
||
r"""----------------------------------------------------------------->
|
||
| ______ __ _ ____ |
|
||
| / ____/__ / /__ ____ (_)_ ______ ___ / _ \____ ________ |
|
||
| \__ \/ _ \/ / _ \/ __ \/ / / / / __ `__ \ / /_) / __ \/ ___/ _ \ |
|
||
| ___/ / __/ / __/ / / / / /_/ / / / / / // /_) / (_/ /__ / __/ |
|
||
| /____/\___/_/\___/_/ /_/_/\__,_/_/ /_/ /_//_____/\__,_/____/\___/ |
|
||
| |
|
||
------------------------------------------------------------------>"""
|