From e0d106233ff21d2dac472158bb5024ca56875587 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 21:04:45 -0400 Subject: [PATCH 01/23] Add custom timeout option for self.assert_no_404_errors() --- seleniumbase/fixtures/base_case.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index c80a4f00e02..5758e32028b 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -81,6 +81,7 @@ def __init__(self, *args, **kwargs): self.__called_setup = False self.__called_teardown = False self.__start_time_ms = None + self.__requests_timeout = None self.__screenshot_count = 0 self.__will_be_skipped = False self.__passed_then_skipped = False @@ -3208,10 +3209,15 @@ def get_unique_links(self): def get_link_status_code(self, link, allow_redirects=False, timeout=5): """Get the status code of a link. - If the timeout is exceeded, will return a 404. + If the timeout is set to less than 1, it becomes 1. + If the timeout is exceeded by requests.get(), it will return a 404. 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 status_code = page_utils._get_link_status_code( link, allow_redirects=allow_redirects, timeout=timeout ) @@ -3234,9 +3240,10 @@ def __get_link_if_404_error(self, link): else: return None - def assert_no_404_errors(self, multithreaded=True): + 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. (A 404 error represents a broken link on a web page.) """ all_links = self.get_unique_links() @@ -3248,6 +3255,12 @@ def assert_no_404_errors(self, multithreaded=True): and "data:" not in link ): links.append(link) + if timeout: + if not type(timeout) is int and not type(timeout) is 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 @@ -3264,6 +3277,7 @@ def assert_no_404_errors(self, multithreaded=True): for link in links: if self.__get_link_if_404_error(link): broken_links.append(link) + self.__requests_timeout = None # Reset the requests.get() timeout if len(broken_links) > 0: bad_links_str = "\n".join(broken_links) if len(broken_links) == 1: From a18511edb81c0f0b96283775b518e57e08a7f589 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 21:12:01 -0400 Subject: [PATCH 02/23] Add a default Chromium option for disabling notifications --- seleniumbase/core/browser_launcher.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 7327921e65c..a337c9a2228 100755 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -303,6 +303,7 @@ def _set_chrome_options( if user_agent: chrome_options.add_argument("--user-agent=%s" % user_agent) chrome_options.add_argument("--disable-infobars") + chrome_options.add_argument("--disable-notifications") chrome_options.add_argument("--disable-save-password-bubble") chrome_options.add_argument("--disable-single-click-autofill") chrome_options.add_argument( @@ -1248,6 +1249,7 @@ def get_local_driver( abs_path = os.path.abspath(extension_dir) edge_options.add_argument("--load-extension=%s" % abs_path) edge_options.add_argument("--disable-infobars") + edge_options.add_argument("--disable-notifications") edge_options.add_argument("--disable-save-password-bubble") edge_options.add_argument("--disable-single-click-autofill") edge_options.add_argument( From 68a7e27b2851a5c61f9b0888836824740a5ad9db Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:31:15 -0400 Subject: [PATCH 03/23] Auto-repair ChromeDriver to sync with Chrome (Mac/Win only) --- seleniumbase/core/browser_launcher.py | 132 +++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index a337c9a2228..16aa15236cf 100755 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -108,6 +108,57 @@ def is_headless_iedriver_on_path(): return os.path.exists(LOCAL_HEADLESS_IEDRIVER) +def _repair_chromedriver(chrome_options, headless_options): + import subprocess + + driver = None + subprocess.check_call( + "sbase install chromedriver 2.44", shell=True + ) + try: + driver = webdriver.Chrome(options=headless_options) + except Exception: + subprocess.check_call( + "sbase install chromedriver latest-1", shell=True + ) + return + chrome_version = None + if "version" in driver.capabilities: + chrome_version = driver.capabilities["version"] + else: + chrome_version = driver.capabilities["browserVersion"] + major_chrome_ver = chrome_version.split(".")[0] + chrome_dict = driver.capabilities["chrome"] + driver.quit() + chromedriver_ver = chrome_dict["chromedriverVersion"] + chromedriver_ver = chromedriver_ver.split(" ")[0] + major_chromedriver_ver = chromedriver_ver.split(".")[0] + if major_chromedriver_ver != major_chrome_ver: + subprocess.check_call( + "sbase install chromedriver %s" % major_chrome_ver, + shell=True + ) + return + + +def _mark_chromedriver_repaired(): + import codecs + + abs_path = os.path.abspath(".") + chromedriver_repaired_lock = constants.MultiBrowser.CHROMEDRIVER_REPAIRED + file_path = os.path.join(abs_path, chromedriver_repaired_lock) + out_file = codecs.open(file_path, "w+", encoding="utf-8") + out_file.writelines("") + out_file.close() + + +def _was_chromedriver_repaired(): + abs_path = os.path.abspath(".") + chromedriver_repaired_lock = constants.MultiBrowser.CHROMEDRIVER_REPAIRED + file_path = os.path.join(abs_path, chromedriver_repaired_lock) + return os.path.exists(file_path) + + def _add_chrome_proxy_extension( chrome_options, proxy_string, proxy_user, proxy_pass ): @@ -1426,8 +1477,87 @@ def get_local_driver( print("\nWarning: chromedriver not found. Installing now:") sb_install.main(override="chromedriver") sys.argv = sys_args # Put back the original sys args + else: + import fasteners + from seleniumbase.console_scripts import sb_install + + chromedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.CHROMEDRIVER_FIXING_LOCK + ) + with chromedriver_fixing_lock: + if not is_chromedriver_on_path(): + sys_args = sys.argv # Save a copy of sys args + print( + "\nWarning: chromedriver not found. " + "Installing now:" + ) + sb_install.main(override="chromedriver") + sys.argv = sys_args # Put back original sys args if not headless or "linux" not in PLATFORM: - return webdriver.Chrome(options=chrome_options) + try: + driver = webdriver.Chrome(options=chrome_options) + except Exception as e: + auto_upgrade_chromedriver = False + if "This version of ChromeDriver only supports" in e.msg: + auto_upgrade_chromedriver = True + if not auto_upgrade_chromedriver: + raise Exception(e.msg) # Not an obvious fix. Raise. + else: + pass # Try upgrading ChromeDriver to match Chrome. + headless = True + headless_options = _set_chrome_options( + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + user_agent, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + no_sandbox, + disable_gpu, + incognito, + guest_mode, + devtools, + remote_debug, + swiftshader, + block_images, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + args = " ".join(sys.argv) + if ("-n" in sys.argv or " -n=" in args or args == "-c"): + import fasteners + + chromedriver_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.CHROMEDRIVER_FIXING_LOCK + ) + with chromedriver_fixing_lock: + if not _was_chromedriver_repaired(): + _repair_chromedriver( + chrome_options, headless_options + ) + _mark_chromedriver_repaired() + else: + if not _was_chromedriver_repaired(): + _repair_chromedriver( + chrome_options, headless_options + ) + _mark_chromedriver_repaired() + driver = webdriver.Chrome(options=chrome_options) + return driver else: # Running headless on Linux try: return webdriver.Chrome(options=chrome_options) From c6f70b78b13b536a9b0164ffd56e98d463fae0b9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:37:01 -0400 Subject: [PATCH 04/23] Add lock files for repairing ChromeDriver in multi-process mode --- seleniumbase/fixtures/constants.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index e3730c1ed3c..ef19ccfa15e 100755 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -48,6 +48,11 @@ class Dashboard: DASH_PIE_PNG_3 = encoded_images.DASH_PIE_PNG_3 # Faster than CDN +class MultiBrowser: + CHROMEDRIVER_FIXING_LOCK = Files.DOWNLOADS_FOLDER + "/driver_fixing.lock" + CHROMEDRIVER_REPAIRED = Files.DOWNLOADS_FOLDER + "/driver_fixed.lock" + + class SavedCookies: STORAGE_FOLDER = "saved_cookies" From bcd817d57b311c95d1d9b821b47af2e58ddddf72 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:37:45 -0400 Subject: [PATCH 05/23] Add self.is_attribute_present(selector, attribute, value) --- seleniumbase/fixtures/base_case.py | 12 +++++++++++ seleniumbase/fixtures/page_actions.py | 31 +++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 5758e32028b..c3c6d90f92a 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -776,6 +776,18 @@ def is_text_visible(self, text, selector="html", by=By.CSS_SELECTOR): selector, by = self.__recalculate_selector(selector, by) return page_actions.is_text_visible(self.driver, text, selector, by) + def is_attribute_present( + self, selector, attribute, value=None, by=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) + return page_actions.is_attribute_present( + self.driver, selector, attribute, value, by + ) + def is_link_text_visible(self, link_text): self.wait_for_ready_state_complete() time.sleep(0.01) diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index ed8bc1ae5df..5a299186820 100755 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -106,6 +106,37 @@ def is_text_visible(driver, text, selector, by=By.CSS_SELECTOR): return False +def is_attribute_present( + driver, selector, attribute, value=None, by=By.CSS_SELECTOR +): + """ + Returns whether the specified attribute is present in the given selector. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + attribute - the attribute that is expected for the element (required) + value - the attribute value that is expected (Default: None) + by - the type of selector being used (Default: By.CSS_SELECTOR) + @Returns + Boolean (is attribute present) + """ + try: + element = driver.find_element(by=by, value=selector) + found_value = element.get_attribute(attribute) + if found_value is None: + raise Exception() + + if value is not None: + if found_value == value: + return True + else: + raise Exception() + else: + return True + except Exception: + return False + + def hover_on_element(driver, selector, by=By.CSS_SELECTOR): """ Fires the hover event for the specified element by the given selector. From d9fc9eb93ce14a48dd43166b8c6d91cda482215a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:40:49 -0400 Subject: [PATCH 06/23] Update the error message for fixing a SeleniumBase install --- seleniumbase/fixtures/base_case.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index c3c6d90f92a..019d5a839b2 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -8584,10 +8584,11 @@ def setUp(self, masterqa_mode=False): # Verify that SeleniumBase is installed successfully if not hasattr(self, "browser"): raise Exception( - """SeleniumBase plugins DID NOT load!\n\n""" - """*** Please REINSTALL SeleniumBase using: >\n""" - """ >>> "pip install -r requirements.txt"\n""" - """ >>> "python setup.py install" """ + 'SeleniumBase plugins DID NOT load! * Please REINSTALL!\n' + '*** Either install SeleniumBase in Dev Mode from a clone:\n' + ' >>> "pip install -e ." (Run in DIR with setup.py)\n' + '*** Or install the latest SeleniumBase version from PyPI:\n' + ' >>> "pip install -U seleniumbase" (Run in any DIR)' ) if not hasattr(sb_config, "_is_timeout_changed"): From 7d402a3707c75328b6f9e35fc98c3d2504d1ac41 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:46:37 -0400 Subject: [PATCH 07/23] Add self.wait_for_attribute_not_present() with assert --- seleniumbase/fixtures/base_case.py | 30 ++++++++++++++++ seleniumbase/fixtures/page_actions.py | 51 ++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 019d5a839b2..5b1a4a9a472 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -7499,6 +7499,36 @@ def assert_text_not_visible( ############ + def wait_for_attribute_not_present( + self, selector, attribute, value=None, by=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=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: diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 5a299186820..76aff08f089 100755 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -524,7 +524,7 @@ def wait_for_attribute( attribute - the attribute that is expected for the element (required) value - the attribute value that is expected (Default: None) by - the type of selector being used (Default: By.CSS_SELECTOR) - timeout - the time to wait for elements in seconds + timeout - the time to wait for the element attribute in seconds @Returns A web element object that contains the expected attribute/value """ @@ -701,6 +701,55 @@ def wait_for_text_not_visible( timeout_exception(Exception, message) +def wait_for_attribute_not_present( + driver, + selector, + attribute, + value=None, + by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT +): + """ + Searches for the specified element attribute by the given selector. + Returns True if the attribute isn't present on the page within the timeout. + Also returns True if the element is not present within the timeout. + Raises an exception if the attribute is still present after the timeout. + @Params + driver - the webdriver object (required) + selector - the locator for identifying the page element (required) + attribute - the element attribute (required) + value - the attribute value (Default: None) + by - the type of selector being used (Default: By.CSS_SELECTOR) + timeout - the time to wait for the element attribute in seconds + """ + start_ms = time.time() * 1000.0 + stop_ms = start_ms + (timeout * 1000.0) + for x in range(int(timeout * 10)): + s_utils.check_if_time_limit_exceeded() + if not is_attribute_present( + driver, selector, attribute, value=value, by=by + ): + return True + now_ms = time.time() * 1000.0 + if now_ms >= stop_ms: + break + time.sleep(0.1) + plural = "s" + if timeout == 1: + plural = "" + message = ( + "Attribute {%s} of element {%s} was still present after %s second%s!" + "" % (attribute, selector, timeout, plural) + ) + if value: + message = ( + "Value {%s} for attribute {%s} of element {%s} was still present " + "after %s second%s!" + "" % (value, attribute, selector, timeout, plural) + ) + timeout_exception(Exception, message) + + def find_visible_elements(driver, selector, by=By.CSS_SELECTOR): """ Finds all WebElements that match a selector and are visible. From c731c3725fc67912febf3afcd3cc71d0e3d7b435 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:47:52 -0400 Subject: [PATCH 08/23] Add keyboard shortcuts to MasterQA mode --- seleniumbase/masterqa/master_qa.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/seleniumbase/masterqa/master_qa.py b/seleniumbase/masterqa/master_qa.py index 5877642de82..2053c7e3077 100755 --- a/seleniumbase/masterqa/master_qa.py +++ b/seleniumbase/masterqa/master_qa.py @@ -139,18 +139,22 @@ def __jq_confirm_dialog(self, question): title: '%s', content: '', buttons: { - fail_button: { - btnClass: 'btn-red', - text: 'NO / FAIL', - action: function(){ - $jqc_status = "Failure!" - } - }, pass_button: { btnClass: 'btn-green', text: 'YES / PASS', + keys: ['y', 'p', '1'], action: function(){ - $jqc_status = "Success!" + $jqc_status = "Success!"; + jconfirm.lastButtonText = "Success!"; + } + }, + fail_button: { + btnClass: 'btn-red', + text: 'NO / FAIL', + keys: ['n', 'f', '2'], + action: function(){ + $jqc_status = "Failure!"; + jconfirm.lastButtonText = "Failure!"; } } } @@ -210,12 +214,7 @@ def __manual_page_check(self, *args): try: status = self.execute_script("return $jqc_status") except Exception: - status = "Failure!" - pre_status = self.execute_script( - "return jconfirm.lastClicked.hasClass('btn-green')" - ) - if pre_status: - status = "Success!" + status = self.execute_script("return jconfirm.lastButtonText") else: # Fallback to plain js confirm dialogs if can't load jquery_confirm if self.browser == "ie": From df53d28dd6884ac58c6bd7fa64ee67ee52d6bdf2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:51:12 -0400 Subject: [PATCH 09/23] Add "--slowmo" as an alternative to "--slow-mode" --- seleniumbase/plugins/pytest_plugin.py | 1 + seleniumbase/plugins/selenium_plugin.py | 1 + 2 files changed, 2 insertions(+) diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 77e721aee5e..17ac383c906 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -561,6 +561,7 @@ def pytest_addoption(parser): parser.addoption( "--slow_mode", "--slow-mode", + "--slowmo", "--slow", action="store_true", dest="slow_mode", diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index 3f50b8c2e90..2ccf11b2e50 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -321,6 +321,7 @@ def options(self, parser, env): parser.add_option( "--slow_mode", "--slow-mode", + "--slowmo", "--slow", action="store_true", dest="slow_mode", From d3f0b2fa8cd400eee9aea638908b4b0b08ec9a41 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:52:00 -0400 Subject: [PATCH 10/23] Add "--screenshot" as an alternative to "--save-screenshot" --- seleniumbase/plugins/pytest_plugin.py | 1 + seleniumbase/plugins/selenium_plugin.py | 1 + 2 files changed, 2 insertions(+) diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 17ac383c906..c6a274fafd3 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -804,6 +804,7 @@ def pytest_addoption(parser): maximized.""", ) parser.addoption( + "--screenshot", "--save_screenshot", "--save-screenshot", action="store_true", diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index 2ccf11b2e50..35eeeb4b0a9 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -530,6 +530,7 @@ def options(self, parser, env): help="""The option to start with the web browser maximized.""", ) parser.add_option( + "--screenshot", "--save_screenshot", "--save-screenshot", action="store_true", From 85bdad45219e561104774da8c7858450f6094c74 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:54:37 -0400 Subject: [PATCH 11/23] Update the favicon link in generated reports --- seleniumbase/core/style_sheet.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/seleniumbase/core/style_sheet.py b/seleniumbase/core/style_sheet.py index c998d78c07c..62c92d43fd3 100755 --- a/seleniumbase/core/style_sheet.py +++ b/seleniumbase/core/style_sheet.py @@ -1,10 +1,7 @@ title = """ Test Report """ % ( - "https://raw.githubusercontent.com/seleniumbase/SeleniumBase" - "/master/seleniumbase/resources/favicon.ico" -) + href="https://seleniumbase.io/img/favicon.ico" /> """ style = ( title From db91e6c2fa08d9452cfa4f1021f0be871e99ead2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 22:59:52 -0400 Subject: [PATCH 12/23] Add additional shortcuts for partial_link_text selectors --- seleniumbase/fixtures/page_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index 0bc57b9739e..5fc2fae8a04 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -56,6 +56,9 @@ def is_partial_link_text_selector(selector): selector.startswith("partial_link=") or selector.startswith("partial_link_text=") or selector.startswith("partial_text=") + or selector.startswith("p_link=") + or selector.startswith("p_link_text=") + or selector.startswith("p_text=") ): return True return False From 54d7804b10dcca43a34cc7964155936ea3410edd Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:01:35 -0400 Subject: [PATCH 13/23] Update methods for getting the link text from selectors --- seleniumbase/fixtures/page_utils.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index 5fc2fae8a04..65f05882ea8 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -78,11 +78,11 @@ def get_link_text_from_selector(selector): A basic method to get the link text from a link text selector. """ if selector.startswith("link="): - return selector.split("link=")[1] + return selector[len("link="):] elif selector.startswith("link_text="): - return selector.split("link_text=")[1] + return selector[len("link_text="):] elif selector.startswith("text="): - return selector.split("text=")[1] + return selector[len("text="):] return selector @@ -91,11 +91,17 @@ def get_partial_link_text_from_selector(selector): A basic method to get the partial link text from a partial link selector. """ if selector.startswith("partial_link="): - return selector.split("partial_link=")[1] + return selector[len("partial_link="):] elif selector.startswith("partial_link_text="): - return selector.split("partial_link_text=")[1] + return selector[len("partial_link_text="):] elif selector.startswith("partial_text="): - return selector.split("partial_text=")[1] + return selector[len("partial_text="):] + elif selector.startswith("p_link="): + return selector[len("p_link="):] + elif selector.startswith("p_link_text="): + return selector[len("p_link_text="):] + elif selector.startswith("p_text="): + return selector[len("p_text="):] return selector From 82116af3c37dc524c6ffeddb2d7d72b8e44fce30 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:03:39 -0400 Subject: [PATCH 14/23] Add "&" as a shortcut for a single-syllable "name" selector --- seleniumbase/fixtures/page_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index 65f05882ea8..9c056ffb63f 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -68,7 +68,7 @@ def is_name_selector(selector): """ A basic method to determine if a selector is a name selector. """ - if selector.startswith("name="): + if selector.startswith("name=") or selector.startswith("&"): return True return False @@ -110,7 +110,9 @@ def get_name_from_selector(selector): A basic method to get the name from a name selector. """ if selector.startswith("name="): - return selector.split("name=")[1] + return selector[len("name="):] + if selector.startswith("&"): + return selector[len("&"):] return selector From 6a5cde06e064c2c02ad7e544990e97eb3f7e90ab Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:05:08 -0400 Subject: [PATCH 15/23] Update a docstring --- seleniumbase/fixtures/base_case.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 5b1a4a9a472..a78b744a736 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -7486,7 +7486,7 @@ def assert_text_not_visible( self, text, selector="html", by=By.CSS_SELECTOR, timeout=None ): """Similar to wait_for_text_not_visible() - Raises an exception if the element or the text is not found. + 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: From 1376ae84bd175d950a86c2db891c95eb048a2cf1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:10:50 -0400 Subject: [PATCH 16/23] Add New Feature: SeleniumBase Dialog Boxes --- seleniumbase/core/jqc_helper.py | 338 +++++++++++++++++++++++++++++ seleniumbase/fixtures/base_case.py | 329 ++++++++++++++++++++++++++++ seleniumbase/fixtures/constants.py | 3 + 3 files changed, 670 insertions(+) create mode 100755 seleniumbase/core/jqc_helper.py diff --git a/seleniumbase/core/jqc_helper.py b/seleniumbase/core/jqc_helper.py new file mode 100755 index 00000000000..b8b61115b04 --- /dev/null +++ b/seleniumbase/core/jqc_helper.py @@ -0,0 +1,338 @@ +""" +This module contains methods for opening jquery-confirm boxes. +These helper methods SHOULD NOT be called directly from tests. +""" +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import js_utils + + +form_code = ( + """'
' + + '
' + + '' + + '
' + + '
'""" +) + + +def jquery_confirm_button_dialog(driver, message, buttons, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + if not message: + message = "" + key_row = "" + if len(buttons) == 1: # There's only one button as an option + key_row = "keys: ['enter', 'y', '1']," # Shortcut: "Enter","Y","1" + b_html = ( + """button_%s: { + btnClass: 'btn-%s', + text: '%s', + %s + action: function(){ + jqc_status = '%s'; + $jqc_status = jqc_status; + jconfirm.lastButtonText = jqc_status; + } + },""" + ) + all_buttons = "" + btn_count = 0 + for button in buttons: + btn_count += 1 + text = button[0] + text = js_utils.escape_quotes_if_needed(text) + if len(buttons) > 1 and text.lower() == "yes": + key_row = "keys: ['y']," + if btn_count < 10: + key_row = "keys: ['y', '%s']," % btn_count + elif len(buttons) > 1 and text.lower() == "no": + key_row = "keys: ['n']," + if btn_count < 10: + key_row = "keys: ['n', '%s']," % btn_count + elif len(buttons) > 1: + if btn_count < 10: + key_row = "keys: ['%s']," % btn_count + color = button[1] + if not color: + color = "blue" + new_button = b_html % (btn_count, color, text, key_row, text) + all_buttons += new_button + + content = ( + '
%s' + "" % (message) + ) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = ( + """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '
', + buttons: { + %s + } + });""" + % ( + width, + overlay_opacity, + border_color, + theme, + content, + all_buttons + ) + ) + driver.execute_script(jqcd) + + +def jquery_confirm_text_dialog(driver, message, button=None, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + + if not message: + message = "" + if button: + if not type(button) is list and not type(button) is tuple: + raise Exception('"button" should be a (text, color) tuple!') + if len(button) != 2: + raise Exception('"button" should be a (text, color) tuple!') + else: + button = ("Submit", "blue") + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + btn_text = button[0] + btn_color = button[1] + if not btn_color: + btn_color = "blue" + content = ( + '
%s' + "" % (message) + ) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = ( + """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '
' + + %s, + buttons: { + formSubmit: { + btnClass: 'btn-%s', + text: '%s', + action: function () { + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + $jqc_status = '%s'; // There is only one button + }, + }, + }, + onContentReady: function () { + var jc = this; + this.$content.find('form.jqc_form').on('submit', function (e) { + // User submits the form by pressing "Enter" in the field + e.preventDefault(); + jc.$$formSubmit.trigger('click'); // Click the button + }); + } + });""" + % ( + width, + overlay_opacity, + border_color, + theme, + content, + form_code, + btn_color, + btn_text, + btn_text + ) + ) + driver.execute_script(jqcd) + + +def jquery_confirm_full_dialog(driver, message, buttons, options=None): + js_utils.activate_jquery_confirm(driver) + # These defaults will be overwritten later if set + theme = constants.JqueryConfirm.DEFAULT_THEME + border_color = constants.JqueryConfirm.DEFAULT_COLOR + width = constants.JqueryConfirm.DEFAULT_WIDTH + + if not message: + message = "" + btn_count = 0 + b_html = ( + """button_%s: { + btnClass: 'btn-%s', + text: '%s', + action: function(){ + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + $jqc_status = '%s'; + } + },""" + ) + b1_html = ( + """formSubmit: { + btnClass: 'btn-%s', + text: '%s', + action: function(){ + jqc_input = this.$content.find('.jqc_input').val(); + $jqc_input = this.$content.find('.jqc_input').val(); + jconfirm.lastInputText = jqc_input; + jqc_status = '%s'; + $jqc_status = jqc_status; + jconfirm.lastButtonText = jqc_status; + } + },""" + ) + one_button_trigger = "" + if len(buttons) == 1: + # If there's only one button, allow form submit with "Enter/Return" + one_button_trigger = "jc.$$formSubmit.trigger('click');" + all_buttons = "" + for button in buttons: + text = button[0] + text = js_utils.escape_quotes_if_needed(text) + color = button[1] + if not color: + color = "blue" + btn_count += 1 + if len(buttons) == 1: + new_button = b1_html % (color, text, text) + else: + new_button = b_html % (btn_count, color, text, text) + all_buttons += new_button + if options: + for option in options: + if option[0].lower() == "theme": + theme = option[1] + elif option[0].lower() == "color": + border_color = option[1] + elif option[0].lower() == "width": + width = option[1] + else: + raise Exception('Unknown option: "%s"' % option[0]) + + content = ( + '
%s' + "" % (message) + ) + content = js_utils.escape_quotes_if_needed(content) + overlay_opacity = "0.32" + if theme.lower() == "supervan": + overlay_opacity = "0.56" + if theme.lower() == "bootstrap": + overlay_opacity = "0.64" + if theme.lower() == "modern": + overlay_opacity = "0.5" + if theme.lower() == "material": + overlay_opacity = "0.4" + jqcd = ( + """jconfirm({ + boxWidth: '%s', + useBootstrap: false, + containerFluid: true, + bgOpacity: %s, + type: '%s', + theme: '%s', + animationBounce: 1, + typeAnimated: true, + animation: 'scale', + draggable: true, + dragWindowGap: 1, + container: 'body', + title: '%s', + content: '
' + + %s, + buttons: { + %s + }, + onContentReady: function () { + var jc = this; + this.$content.find('form.jqc_form').on('submit', function (e) { + // User submits the form by pressing "Enter" in the field + e.preventDefault(); + %s + }); + } + });""" + % ( + width, + overlay_opacity, + border_color, + theme, + content, + form_code, + all_buttons, + one_button_trigger + ) + ) + driver.execute_script(jqcd) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index a78b744a736..9acbe215672 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -100,6 +100,10 @@ def __init__(self, *args, **kwargs): self.__device_height = None self.__device_pixel_ratio = None self.__driver_browser_map = {} + 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 = {} @@ -6681,12 +6685,337 @@ def export_tour(self, name=None, filename="my_tour.js", url=None): 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() 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 type(width) is int or type(width) is 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 type(message) is not str: + raise Exception('Expecting a string for arg: "message"!') + if not type(buttons) is list and not type(buttons) is 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 ( + (type(button) is list or type(button) is tuple) and ( + len(button) == 1) + ): + new_buttons.append(button[0]) + elif ( + (type(button) is list or type(button) is 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 type(option) is list and not type(option) is tuple: + raise Exception('"options" should be a list of tuples!') + if self.headless: + return buttons[-1][0] + jqc_helper.jquery_confirm_button_dialog( + self.driver, message, buttons, options + ) + self.sleep(0.02) + jf = "document.querySelector('.jconfirm-box').focus();" + try: + self.execute_script(jf) + except Exception: + pass + waiting_for_response = True + while waiting_for_response: + self.sleep(0.05) + jqc_open = self.execute_script( + "return jconfirm.instances.length" + ) + if str(jqc_open) == "0": + break + self.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 type(message) is not str: + raise Exception('Expecting a string for arg: "message"!') + if button: + if ( + (type(button) is list or type(button) is tuple) and ( + len(button) == 1) + ): + button = (str(button[0]), "") + elif ( + (type(button) is list or type(button) is 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 type(option) is list and not type(option) is tuple: + raise Exception('"options" should be a list of tuples!') + if self.headless: + return "" + jqc_helper.jquery_confirm_text_dialog( + self.driver, message, button, options + ) + self.sleep(0.02) + jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();" + try: + self.execute_script(jf) + except Exception: + pass + waiting_for_response = True + while waiting_for_response: + self.sleep(0.05) + jqc_open = self.execute_script( + "return jconfirm.instances.length" + ) + if str(jqc_open) == "0": + break + self.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 type(message) is not str: + raise Exception('Expecting a string for arg: "message"!') + if not type(buttons) is list and not type(buttons) is 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 ( + (type(button) is list or type(button) is tuple) and ( + len(button) == 1) + ): + new_buttons.append(button[0]) + elif ( + (type(button) is list or type(button) is 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 type(option) is list and not type(option) is tuple: + raise Exception('"options" should be a list of tuples!') + if self.headless: + return ("", buttons[-1][0]) + jqc_helper.jquery_confirm_full_dialog( + self.driver, message, buttons, options + ) + self.sleep(0.02) + jf = "document.querySelector('.jconfirm-box input.jqc_input').focus();" + try: + self.execute_script(jf) + except Exception: + pass + waiting_for_response = True + while waiting_for_response: + self.sleep(0.05) + jqc_open = self.execute_script( + "return jconfirm.instances.length" + ) + if str(jqc_open) == "0": + break + self.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 activate_messenger(self): self.__check_scope() js_utils.activate_messenger(self.driver) diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index ef19ccfa15e..ce01ec779dc 100755 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -290,6 +290,9 @@ class JqueryConfirm: "https://cdnjs.cloudflare.com/ajax/libs/" "jquery-confirm/%s/jquery-confirm.min.js" % VER ) + DEFAULT_THEME = "bootstrap" + DEFAULT_COLOR = "blue" + DEFAULT_WIDTH = "38%" class Shepherd: From 741d7d141213b7610a4fc239424b8acd70405dc2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:12:40 -0400 Subject: [PATCH 17/23] Add a demo script for SeleniumBase Dialog Boxes --- examples/dialog_boxes/dialog_box_tour.py | 117 +++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100755 examples/dialog_boxes/dialog_box_tour.py diff --git a/examples/dialog_boxes/dialog_box_tour.py b/examples/dialog_boxes/dialog_box_tour.py new file mode 100755 index 00000000000..f20292ba7db --- /dev/null +++ b/examples/dialog_boxes/dialog_box_tour.py @@ -0,0 +1,117 @@ +from seleniumbase import BaseCase + + +class DialogBoxTests(BaseCase): + def test_dialog_boxes(self): + self.open("https://xkcd.com/1920/") + self.assert_element('img[alt="Emoji Sports"]') + self.highlight("#comic img") + + skip_button = ["SKIP", "red"] # Can be a [text, color] list or tuple. + buttons = ["Fencing", "Football", "Metaball", "Go/Chess", skip_button] + message = "Choose a sport:" + choice = None + while choice != "STOP": + choice = self.get_jqc_button_input(message, buttons) + if choice == "Fencing": + self.open("https://xkcd.com/1424/") + buttons.remove("Fencing") + elif choice == "Football": + self.open("https://xkcd.com/1107/") + buttons.remove("Football") + elif choice == "Metaball": + self.open("https://xkcd.com/1507/") + buttons.remove("Metaball") + elif choice == "Go/Chess": + self.open("https://xkcd.com/1287/") + buttons.remove("Go/Chess") + else: + break + self.highlight("#comic img") + if len(buttons) == 2: + message = "One Sport Remaining:" + if len(buttons) == 1: + message = "Part One Complete. You saw all 4 sports!" + btn_text_1 = "NEXT Tutorial Please!" + btn_text_2 = "WAIT, Go/Chess is a sport?" + buttons = [(btn_text_1, "green"), (btn_text_2, "purple")] + choice_2 = self.get_jqc_button_input(message, buttons) + if choice_2 == btn_text_2: + self.open("https://xkcd.com/1287/") + message = "Brain sports count as sports!

" + message += "Are you ready for more?" + self.get_jqc_button_input(message, ["Let's Go!"]) + break + + self.open("https://xkcd.com/1117/") + sb_banner_logo = "//seleniumbase.io/cdn/img/sb_logo_cs.png" + self.set_attributes("#news img", "src", sb_banner_logo) + options = [("theme", "material"), ("width", "52%")] + message = 'With one button, you can press "Enter/Return", "Y", or "1".' + self.get_jqc_button_input(message, ["OK"], options) + + self.open("https://xkcd.com/556/") + self.set_attributes("#news img", "src", sb_banner_logo) + options = [("theme", "bootstrap"), ("width", "52%")] + message = 'If the lowercase button text is "yes" or "no", ' + message += '

you can use the "Y" or "N" keys as shortcuts. ' + message += '


Other shortcuts include:

' + message += '"1": 1st button, "2": 2nd button, etc. Got it?' + buttons = [("YES", "green"), ("NO", "red")] + choice = self.get_jqc_button_input(message, buttons, options) + + message = 'You said "%s"!

' % choice + if choice == "YES": + message += "Wonderful! Let's continue with the next example..." + else: + message += "You can learn more from SeleniumBase Docs..." + choice = self.get_jqc_button_input(message, ["OK"], options) + + self.open("https://seleniumbase.io") + self.set_jqc_theme("light", color="green", width="38%") + message = "This is the SeleniumBase Docs website!

" + message += "What would you like to search for?
" + text = self.get_jqc_text_input(message, ["Search"]) + self.update_text('input[aria-label="Search"]', text + "\n") + self.wait_for_ready_state_complete() + self.set_jqc_theme("bootstrap", color="red", width="32%") + if self.is_text_visible("No matching documents", ".md-search-result"): + self.get_jqc_button_input("Your search had no results!", ["OK"]) + elif self.is_element_visible("a.md-search-result__link"): + self.click("a.md-search-result__link") + self.set_jqc_theme("bootstrap", color="green", width="32%") + self.get_jqc_button_input("You found search results!", ["OK"]) + elif self.is_text_visible("Type to start searching", "div.md-search"): + self.get_jqc_button_input("You did not do a search!", ["OK"]) + else: + self.get_jqc_button_input("We're not sure what happened.", ["OK"]) + + self.open("https://seleniumbase.io/help_docs/ReadMe/") + self.highlight("h1") + self.highlight_click('a:contains("Running Example Tests")') + self.highlight("h1") + + self.set_jqc_theme("bootstrap", color="green", width="52%") + message = 'See the "SeleniumBase/examples" section for more info!' + self.get_jqc_button_input(message, ["OK"]) + + self.set_jqc_theme("bootstrap", color="purple", width="56%") + message = "Now let's combine form inputs with multiple button options!" + message += "

" + message += "Pick something to search. Then pick the site to search on." + buttons = ["XKCD.com", "Wikipedia.org"] + text, choice = self.get_jqc_form_inputs(message, buttons) + if choice == "XKCD.com": + self.open("https://relevant-xkcd.github.io/") + else: + self.open("https://en.wikipedia.org/wiki/Main_Page") + self.highlight_update_text('input[name="search"]', text + "\n") + self.wait_for_ready_state_complete() + self.highlight("body") + self.reset_jqc_theme() + self.get_jqc_button_input("Here are your results.", ["OK"]) + message = "

You've reached the end of this tutorial!


" + message += "Thanks for learning about SeleniumBase Dialog Boxes!
" + message += "
Check out the SeleniumBase page on GitHub for more!" + self.set_jqc_theme("modern", color="purple", width="56%") + self.get_jqc_button_input(message, ["Goodbye!"]) From 3c260e00b1e5e62bd9f78ef4e3c8abc078b2ee82 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:12:56 -0400 Subject: [PATCH 18/23] Add the ReadMe for SeleniumBase Dialog Boxes --- examples/dialog_boxes/ReadMe.md | 180 ++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100755 examples/dialog_boxes/ReadMe.md diff --git a/examples/dialog_boxes/ReadMe.md b/examples/dialog_boxes/ReadMe.md new file mode 100755 index 00000000000..a1eb26adb8b --- /dev/null +++ b/examples/dialog_boxes/ReadMe.md @@ -0,0 +1,180 @@ +

SeleniumBase

+ +

πŸ›‚ Dialog Boxes πŸ›‚

+ +SeleniumBase Dialog Boxes let your users provide input in the middle of automation scripts. + +* This feature utilizes the [jquery-confirm](https://craftpip.github.io/jquery-confirm/) library. +* A Python API is used to call the JavaScript API. + +SeleniumBase + +

↕️ (Example: dialog_box_tour.py) ↕️

+ +SeleniumBase + +

Here's how to run that example:

+ +```bash +cd examples/dialog_boxes +pytest test_dialog_boxes.py +``` + +

Here's a code snippet from that:

+ +```python +self.open("https://xkcd.com/1920/") +skip_button = ["SKIP", "red"] # Can be a [text, color] list or tuple. +buttons = ["Fencing", "Football", "Metaball", "Go/Chess", skip_button] +message = "Choose a sport:" +choice = self.get_jqc_button_input(message, buttons) +if choice == "Fencing": + self.open("https://xkcd.com/1424/") +``` + +* You can create forms that include buttons and input fields. + +

Here's a simple form with only buttons as input:

+ +```python +choice = self.get_jqc_button_input("Ready?", ["YES", "NO"]) +print(choice) # This prints "YES" or "NO" + +# You may want to customize the color of buttons: +buttons = [("YES", "green"), ("NO", "red")] +choice = self.get_jqc_button_input("Ready?", buttons) +``` + +

Here's a simple form with an input field:

+ +```python +text = self.get_jqc_text_input("Enter text:", ["Search"]) +print(text) # This prints the text entered +``` + +

This form has an input field and buttons:

+ +```python +message = "Type your name and choose a language:" +buttons = ["Python", "JavaScript"] +text, choice = self.get_jqc_form_inputs(message, buttons) +print("Your name is: %s" % text) +print("You picked %s!" % choice) +``` + +

You can customize options if you want:

+ +```python +# Themes: bootstrap, modern, material, supervan, light, dark, seamless +options = [("theme", "modern"), ("width", "50%")] +self.get_jqc_text_input("You Won!", ["OK"], options) +``` + +

Default options can be set with set_jqc_theme():

+ +```python +self.set_jqc_theme("light", color="green", width="38%") + +# To reset jqc theme settings to factory defaults: +self.reset_jqc_theme() +``` + +

All methods for Dialog Boxes:

+ +```python +self.get_jqc_button_input(message, buttons, options=None) + +self.get_jqc_text_input(message, button=None, options=None) + +self.get_jqc_form_inputs(message, buttons, options=None) + +self.set_jqc_theme(theme, color=None, width=None) + +self.reset_jqc_theme() + +self.activate_jquery_confirm() # Automatic for jqc methods +``` + +

Detailed method summaries for Dialog Boxes:

+ +```python +self.get_jqc_button_input(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". +""" + +self.get_jqc_text_input(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". +""" + +self.get_jqc_form_inputs(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". +""" + +self.set_jqc_theme(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". +""" + +self.reset_jqc_theme() +""" Resets the jqc theme settings to factory defaults. """ + +self.activate_jquery_confirm() # Automatic for jqc methods +""" See https://craftpip.github.io/jquery-confirm/ for usage. """ +``` + +-------- + +

βœ… πŸ›‚ Automated/Manual Hybrid Mode (MasterQA)

+

MasterQA uses SeleniumBase Dialog Boxes to speed up manual testing by having automation perform all the browser actions while the manual tester handles validation. See the MasterQA GitHub page for examples.

From 7e20529bf39dd405753851c65ee1266b02e893c9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 23 Aug 2021 23:13:27 -0400 Subject: [PATCH 19/23] Update deploy dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4879051f5ba..a2aea724b23 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ print("\n*** Installing twine: *** (Required for PyPI uploads)\n") os.system("python -m pip install --upgrade 'twine>=1.15.0'") print("\n*** Installing tqdm: *** (Required for PyPI uploads)\n") - os.system("python -m pip install --upgrade 'tqdm>=4.62.0'") + os.system("python -m pip install --upgrade 'tqdm>=4.62.2'") print("\n*** Publishing The Release to PyPI: ***\n") os.system("python -m twine upload dist/*") # Requires ~/.pypirc Keys print("\n*** The Release was PUBLISHED SUCCESSFULLY to PyPI! :) ***\n") From d290ff89b7ef048bbd542f7ea4869ea0d841c045 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Aug 2021 00:04:25 -0400 Subject: [PATCH 20/23] Update Python dependencies --- requirements.txt | 34 ++++++++++++++++++---------------- setup.py | 34 ++++++++++++++++++---------------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4eacb7c9ab8..bc47c04756d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ pip>=20.3.4;python_version<"3.6" -pip>=21.2.3;python_version>="3.6" +pip>=21.2.4;python_version>="3.6" packaging>=20.9;python_version<"3.6" packaging>=21.0;python_version>="3.6" typing-extensions>=3.10.0.0 @@ -15,16 +15,16 @@ sortedcontainers==2.4.0 certifi>=2021.5.30 six==1.16.0 nose==1.3.7 -ipdb==0.13.4;python_version<"3.6" -ipdb==0.13.9;python_version>="3.6" +ipdb==0.13.4;python_version<"3.5" +ipdb==0.13.9;python_version>="3.5" parso==0.7.1;python_version<"3.6" parso==0.8.2;python_version>="3.6" jedi==0.17.2;python_version<"3.6" jedi==0.18.0;python_version>="3.6" idna==2.10;python_version<"3.6" idna==3.2;python_version>="3.6" -chardet==3.0.4;python_version<"3.6" -chardet==4.0.0;python_version>="3.6" +chardet==3.0.4;python_version<"3.5" +chardet==4.0.0;python_version>="3.5" charset-normalizer==2.0.4;python_version>="3.6" urllib3==1.26.6 requests==2.26.0;python_version<"3.5" @@ -36,8 +36,8 @@ more-itertools==5.0.0;python_version<"3.5" more-itertools==8.8.0;python_version>="3.5" cssselect==1.1.0 filelock==3.0.12 -fasteners==0.16;python_version<"3.6" -fasteners==0.16.3;python_version>="3.6" +fasteners==0.16;python_version<"3.5" +fasteners==0.16.3;python_version>="3.5" execnet==1.9.0 pluggy==0.13.1 py==1.8.1;python_version<"3.5" @@ -60,24 +60,25 @@ pytest-xdist==2.3.0;python_version>="3.6" parameterized==0.8.1 sbvirtualdisplay==1.0.0 soupsieve==1.9.6;python_version<"3.5" -soupsieve==2.0.1;python_version>="3.5" and python_version<"3.6" +soupsieve==2.1;python_version>="3.5" and python_version<"3.6" soupsieve==2.2.1;python_version>="3.6" beautifulsoup4==4.9.3 cryptography==2.9.2;python_version<"3.5" -cryptography==3.0;python_version>="3.5" and python_version<"3.6" +cryptography==3.2.1;python_version>="3.5" and python_version<"3.6" cryptography==3.4.7;python_version>="3.6" -pyopenssl==19.1.0;python_version<"3.6" -pyopenssl==20.0.1;python_version>="3.6" +pyopenssl==19.1.0;python_version<"3.5" +pyopenssl==20.0.1;python_version>="3.5" pygments==2.5.2;python_version<"3.5" -pygments==2.9.0;python_version>="3.5" +pygments==2.10.0;python_version>="3.5" traitlets==4.3.3;python_version<"3.7" traitlets==5.0.5;python_version>="3.7" -prompt-toolkit==1.0.18;python_version<"3.6" -prompt-toolkit==3.0.19;python_version>="3.6" +prompt-toolkit==1.0.18;python_version<"3.5" +prompt-toolkit==2.0.10;python_version>="3.5" and python_version<"3.6.2" +prompt-toolkit==3.0.20;python_version>="3.6.2" decorator==4.4.2;python_version<"3.5" decorator==5.0.9;python_version>="3.5" ipython==5.10.0;python_version<"3.5" -ipython==6.5.0;python_version>="3.5" and python_version<"3.6" +ipython==7.9.0;python_version>="3.5" and python_version<"3.6" ipython==7.16.1;python_version>="3.6" and python_version<"3.7" ipython==7.26.0;python_version>="3.7" matplotlib-inline==0.1.2;python_version>="3.7" @@ -85,7 +86,8 @@ colorama==0.4.4 platformdirs==2.0.2;python_version<"3.6" platformdirs==2.2.0;python_version>="3.6" pathlib2==2.3.5;python_version<"3.5" -importlib-metadata==2.0.0;python_version<"3.6" +importlib-metadata==2.0.0;python_version<"3.5" +importlib-metadata==2.1.1;python_version>="3.5" and python_version<"3.6" virtualenv>=20.7.2 pymysql==0.10.1;python_version<"3.6" pymysql==1.0.2;python_version>="3.6" diff --git a/setup.py b/setup.py index a2aea724b23..57c52f6d624 100755 --- a/setup.py +++ b/setup.py @@ -114,7 +114,7 @@ python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", install_requires=[ 'pip>=20.3.4;python_version<"3.6"', - 'pip>=21.2.3;python_version>="3.6"', + 'pip>=21.2.4;python_version>="3.6"', 'packaging>=20.9;python_version<"3.6"', 'packaging>=21.0;python_version>="3.6"', "typing-extensions>=3.10.0.0", @@ -130,16 +130,16 @@ "certifi>=2021.5.30", "six==1.16.0", "nose==1.3.7", - 'ipdb==0.13.4;python_version<"3.6"', - 'ipdb==0.13.9;python_version>="3.6"', + 'ipdb==0.13.4;python_version<"3.5"', + 'ipdb==0.13.9;python_version>="3.5"', 'parso==0.7.1;python_version<"3.6"', 'parso==0.8.2;python_version>="3.6"', 'jedi==0.17.2;python_version<"3.6"', 'jedi==0.18.0;python_version>="3.6"', 'idna==2.10;python_version<"3.6"', # Must stay in sync with "requests" 'idna==3.2;python_version>="3.6"', # Must stay in sync with "requests" - 'chardet==3.0.4;python_version<"3.6"', # Stay in sync with "requests" - 'chardet==4.0.0;python_version>="3.6"', # Stay in sync with "requests" + 'chardet==3.0.4;python_version<"3.5"', # Stay in sync with "requests" + 'chardet==4.0.0;python_version>="3.5"', # Stay in sync with "requests" 'charset-normalizer==2.0.4;python_version>="3.6"', # Sync "requests" "urllib3==1.26.6", # Must stay in sync with "requests" 'requests==2.26.0;python_version<"3.5"', @@ -151,8 +151,8 @@ 'more-itertools==8.8.0;python_version>="3.5"', "cssselect==1.1.0", "filelock==3.0.12", - 'fasteners==0.16;python_version<"3.6"', - 'fasteners==0.16.3;python_version>="3.6"', + 'fasteners==0.16;python_version<"3.5"', + 'fasteners==0.16.3;python_version>="3.5"', "execnet==1.9.0", "pluggy==0.13.1", 'py==1.8.1;python_version<"3.5"', @@ -175,24 +175,25 @@ "parameterized==0.8.1", "sbvirtualdisplay==1.0.0", 'soupsieve==1.9.6;python_version<"3.5"', - 'soupsieve==2.0.1;python_version>="3.5" and python_version<"3.6"', + 'soupsieve==2.1;python_version>="3.5" and python_version<"3.6"', 'soupsieve==2.2.1;python_version>="3.6"', "beautifulsoup4==4.9.3", 'cryptography==2.9.2;python_version<"3.5"', - 'cryptography==3.0;python_version>="3.5" and python_version<"3.6"', + 'cryptography==3.2.1;python_version>="3.5" and python_version<"3.6"', 'cryptography==3.4.7;python_version>="3.6"', - 'pyopenssl==19.1.0;python_version<"3.6"', - 'pyopenssl==20.0.1;python_version>="3.6"', + 'pyopenssl==19.1.0;python_version<"3.5"', + 'pyopenssl==20.0.1;python_version>="3.5"', 'pygments==2.5.2;python_version<"3.5"', - 'pygments==2.9.0;python_version>="3.5"', + 'pygments==2.10.0;python_version>="3.5"', 'traitlets==4.3.3;python_version<"3.7"', 'traitlets==5.0.5;python_version>="3.7"', - 'prompt-toolkit==1.0.18;python_version<"3.6"', - 'prompt-toolkit==3.0.19;python_version>="3.6"', + 'prompt-toolkit==1.0.18;python_version<"3.5"', + 'prompt-toolkit==2.0.10;python_version>="3.5" and python_version<"3.6.2"', # noqa: E501 + 'prompt-toolkit==3.0.20;python_version>="3.6.2"', 'decorator==4.4.2;python_version<"3.5"', 'decorator==5.0.9;python_version>="3.5"', 'ipython==5.10.0;python_version<"3.5"', - 'ipython==6.5.0;python_version>="3.5" and python_version<"3.6"', + 'ipython==7.9.0;python_version>="3.5" and python_version<"3.6"', 'ipython==7.16.1;python_version>="3.6" and python_version<"3.7"', 'ipython==7.26.0;python_version>="3.7"', 'matplotlib-inline==0.1.2;python_version>="3.7"', @@ -200,7 +201,8 @@ 'platformdirs==2.0.2;python_version<"3.6"', 'platformdirs==2.2.0;python_version>="3.6"', 'pathlib2==2.3.5;python_version<"3.5"', # Sync with "virtualenv" - 'importlib-metadata==2.0.0;python_version<"3.6"', # Sync "virtualenv" + 'importlib-metadata==2.0.0;python_version<"3.5"', + 'importlib-metadata==2.1.1;python_version>="3.5" and python_version<"3.6"', # noqa: E501 "virtualenv>=20.7.2", # Sync with importlib-metadata and pathlib2 'pymysql==0.10.1;python_version<"3.6"', 'pymysql==1.0.2;python_version>="3.6"', From 36fad844797c9969e720ee91f348f367afba4da1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Aug 2021 00:05:04 -0400 Subject: [PATCH 21/23] Update the docs --- README.md | 4 ++-- help_docs/ReadMe.md | 20 ++++++++++---------- help_docs/features_list.md | 6 +++--- help_docs/method_summary.md | 26 +++++++++++++++++++++++++- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4b26326ee05..231ae705148 100755 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ πŸ—ΊοΈ Tours | πŸ“Ά Charts | πŸ“° Present | -πŸ–ΌοΈ VisualTest | -πŸ›‚ MasterQA +πŸ›‚ DialogBox | +πŸ–ΌοΈ VisualTest

diff --git a/help_docs/ReadMe.md b/help_docs/ReadMe.md index 4ab358d8722..d6f7056531c 100755 --- a/help_docs/ReadMe.md +++ b/help_docs/ReadMe.md @@ -5,35 +5,35 @@

πŸš€ Start | πŸ–₯️ CLI | -πŸ—‚οΈ Features +🏰 Features
-πŸ“– Examples | +πŸ‘¨β€πŸ« Examples | πŸ“± Mobile
-πŸ”‘ Syntax Formats | +πŸ”  Syntax Formats | πŸ€– CI
-πŸ“— API | -πŸ“‹ Reports | +πŸ“š API | +πŸ“Š Reports | πŸ—ΊοΈ Tours
-πŸ’» Console Scripts | +πŸ§™β€ Console Scripts | 🌐 Grid
♻️ Boilerplates | πŸ—Ύ Locales
-πŸ—„οΈ PkgManager | +πŸ•ΉοΈ JSManager | πŸ–ΌοΈ VisualTest
🌏 Translate | -πŸ›‚ MasterQA +πŸ›‚ DialogBoxes
⏺️ Recorder | πŸƒ NodeRunner
-πŸ“‘ Presenter | -πŸ“Š ChartMaker +πŸ“° Presenter | +πŸ“Ά ChartMaker

-------- diff --git a/help_docs/features_list.md b/help_docs/features_list.md index 7fd31240ec7..db531edc26a 100755 --- a/help_docs/features_list.md +++ b/help_docs/features_list.md @@ -28,16 +28,16 @@ * Has a [global config file](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/config/settings.py) for configuring settings as needed. * Includes a tool for [creating interactive web presentations](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/presenter/ReadMe.md). * Includes [Chart Maker](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/chart_maker/ReadMe.md), a tool for creating interactive charts. +* Includes a [dialog box builder](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/dialog_boxes/ReadMe.md) to allow user-input during automation. * Includes a [website tour builder](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/tour_examples/ReadMe.md) for creating interactive walkthroughs. -* Has a tool to [export Katalon Recorder scripts into SeleniumBase format](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/katalon/ReadMe.md). +* Includes integrations for [GitHub Actions](https://seleniumbase.io/integrations/github/workflows/ReadMe/), [Google Cloud](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/google_cloud/ReadMe.md), [Azure](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/azure/jenkins/ReadMe.md), [S3](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py), and [Docker](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/docker/ReadMe.md). * Can handle Google Authenticator logins with [Python's one-time password library](https://pyotp.readthedocs.io/en/latest/). * Can load and make assertions on PDF files from websites or the local file system. * Is backwards-compatible with Python [WebDriver](https://www.selenium.dev/projects/) methods. (Use: ``self.driver``) * Can execute JavaScript code from Python calls. (Use: ``self.execute_script()``) * Can pierce through Shadow DOM selectors. (Add ``::shadow`` to CSS fragments.) -* Includes integrations for [MySQL](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/core/testcase_manager.py), [Selenium Grid](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_grid), [Azure](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/azure/jenkins/ReadMe.md), [GCP](https://github.com/seleniumbase/SeleniumBase/tree/master/integrations/google_cloud/ReadMe.md), [AWS](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/plugins/s3_logging_plugin.py), and [Docker](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/docker/ReadMe.md). * Includes a hybrid-automation solution, [MasterQA](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/masterqa/ReadMe.md), for speeding up manual testing. -* Includes a tool for [converting Selenium IDE recordings](https://github.com/seleniumbase/SeleniumBase/tree/master/seleniumbase/utilities/selenium_ide) into SeleniumBase scripts. +* Includes a tool to [convert Katalon & Selenium IDE recordings](https://github.com/seleniumbase/SeleniumBase/blob/master/integrations/katalon/ReadMe.md) into SeleniumBase scripts. * Includes useful [Python decorators and password obfuscation methods](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/common/ReadMe.md). -------- diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index bffe581c2c3..fc840326561 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -69,6 +69,8 @@ self.is_element_enabled(selector, by=By.CSS_SELECTOR) self.is_text_visible(text, selector="html", by=By.CSS_SELECTOR) +self.is_attribute_present(selector, attribute, value=None, by=By.CSS_SELECTOR) + self.is_link_text_visible(link_text) self.is_partial_link_text_visible(partial_link_text) @@ -303,7 +305,7 @@ self.get_link_status_code(link, allow_redirects=False, timeout=5) self.assert_link_status_code_is_not_404(link) -self.assert_no_404_errors(multithreaded=True) +self.assert_no_404_errors(multithreaded=True, timeout=None) # Duplicates: self.assert_no_broken_links(multithreaded=True) self.print_unique_links_with_status_codes() @@ -512,8 +514,22 @@ self.play_tour(name=None) self.export_tour(name=None, filename="my_tour.js", url=None) +############ + self.activate_jquery_confirm() +self.set_jqc_theme(theme, color=None, width=None) + +self.reset_jqc_theme() + +self.get_jqc_button_input(message, buttons, options=None) + +self.get_jqc_text_input(message, button=None, options=None) + +self.get_jqc_form_inputs(message, buttons, options=None) + +############ + self.activate_messenger() self.post_message(message, duration=None, pause=True, style="info") @@ -613,6 +629,14 @@ self.assert_text_not_visible(text, selector="html", by=By.CSS_SELECTOR, timeout= ############ +self.wait_for_attribute_not_present( + selector, attribute, value=None, by=By.CSS_SELECTOR, timeout=None) + +self.assert_attribute_not_present( + selector, attribute, value=None, by=By.CSS_SELECTOR, timeout=None) + +############ + self.accept_alert(timeout=None) # Duplicates: self.wait_for_and_accept_alert(timeout=None) From f55bad8b5fae4d784c348555fef9fc7be4f740af Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Aug 2021 00:21:20 -0400 Subject: [PATCH 22/23] Update the docs --- README.md | 2 +- mkdocs.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 231ae705148..38e91c8ae3f 100755 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ nosetests [FILE_NAME.py]:[CLASS_NAME].[METHOD_NAME] NO MORE FLAKY TESTS! βœ… Automated/Manual Hybrid Mode: -

SeleniumBase includes a solution called MasterQA, which speeds up manual testing by having automation perform all the browser actions while the manual tester handles validation.

+

SeleniumBase includes a solution called MasterQA, which speeds up manual testing by having automation perform all the browser actions while the manual tester handles validation.

βœ… Feature-Rich:

For a full list of SeleniumBase features, Click Here.

diff --git a/mkdocs.yml b/mkdocs.yml index bf5ff5c0044..d69c4f3d9d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -92,8 +92,9 @@ nav: - Locale Codes: help_docs/locale_codes.md - JS Generators: - Tour Maker: examples/tour_examples/ReadMe.md - - Presentation Maker: examples/presenter/ReadMe.md + - Dialog Boxes: examples/dialog_boxes/ReadMe.md - Chart Maker: help_docs/chart_maker.md + - Presentation Maker: examples/presenter/ReadMe.md - Integrations: - Mobile Testing: help_docs/mobile_testing.md - GitHub CI: integrations/github/workflows/ReadMe.md From 14ff0c5419805c41c8a296a21cea0590810eb208 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 24 Aug 2021 00:24:33 -0400 Subject: [PATCH 23/23] Version 1.64.0 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index b02f7779502..6030459ca5a 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "1.63.23" +__version__ = "1.64.0"