From 914aaa66b6b84d6cb4c95df8e08c5f3f601517d4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 00:48:49 -0500 Subject: [PATCH 01/10] Allow more customizations for Grid capabilities --- seleniumbase/core/browser_launcher.py | 99 +++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index a943c155454..6ac7d872f55 100755 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -281,6 +281,8 @@ def _set_chrome_options( device_pixel_ratio, ): chrome_options = webdriver.ChromeOptions() + if browser_name == constants.Browser.OPERA: + chrome_options = webdriver.opera.options.Options() prefs = { "download.default_directory": downloads_path, "local_discovery.notifications_enabled": False, @@ -1058,6 +1060,7 @@ def get_remote_driver( selenoid = False selenoid_options = None screen_resolution = None + browser_version = None platform_name = None for key in desired_caps.keys(): capabilities[key] = desired_caps[key] @@ -1066,6 +1069,8 @@ def get_remote_driver( selenoid_options = desired_caps[key] elif key == "screenResolution": screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] elif key == "platform" or key == "platformName": platform_name = desired_caps[key] if selenium4: @@ -1076,6 +1081,9 @@ def get_remote_driver( if screen_resolution: scres = screen_resolution firefox_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + firefox_options.set_capability("browserVersion", br_vers) if platform_name: plat_name = platform_name firefox_options.set_capability("platformName", plat_name) @@ -1149,19 +1157,86 @@ def get_remote_driver( keep_alive=True, ) elif browser_name == constants.Browser.OPERA: - capabilities = webdriver.DesiredCapabilities.OPERA + opera_options = _set_chrome_options( + browser_name, + downloads_path, + headless, + locale_code, + proxy_string, + proxy_auth, + proxy_user, + proxy_pass, + proxy_bypass_list, + user_agent, + recorder_ext, + disable_csp, + enable_ws, + enable_sync, + use_auto_ext, + no_sandbox, + disable_gpu, + incognito, + guest_mode, + devtools, + remote_debug, + swiftshader, + ad_block_on, + block_images, + chromium_arg, + user_data_dir, + extension_zip, + extension_dir, + external_pdf, + servername, + mobile_emulator, + device_width, + device_height, + device_pixel_ratio, + ) + capabilities = None if selenium4: - remote_options = ArgOptions() - remote_options.set_capability("cloud:options", desired_caps) + capabilities = webdriver.DesiredCapabilities.OPERA + else: + opera_options = webdriver.opera.options.Options() + capabilities = opera_options.to_capabilities() + # Set custom desired capabilities + selenoid = False + selenoid_options = None + screen_resolution = None + browser_version = None + platform_name = None + for key in desired_caps.keys(): + capabilities[key] = desired_caps[key] + if key == "selenoid:options": + selenoid = True + selenoid_options = desired_caps[key] + elif key == "screenResolution": + screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] + elif key == "platform" or key == "platformName": + platform_name = desired_caps[key] + if selenium4: + opera_options.set_capability("cloud:options", capabilities) + if selenoid: + snops = selenoid_options + opera_options.set_capability("selenoid:options", snops) + if screen_resolution: + scres = screen_resolution + opera_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + opera_options.set_capability("browserVersion", br_vers) + if platform_name: + plat_name = platform_name + opera_options.set_capability("platformName", plat_name) return webdriver.Remote( command_executor=address, - options=remote_options, + options=opera_options, keep_alive=True, ) else: warnings.simplefilter("ignore", category=DeprecationWarning) - for key in desired_caps.keys(): - capabilities[key] = desired_caps[key] return webdriver.Remote( command_executor=address, desired_capabilities=capabilities, @@ -1246,12 +1321,18 @@ def get_remote_driver( selenoid = False selenoid_options = None screen_resolution = None + browser_version = None + platform_name = None for key in desired_caps.keys(): if key == "selenoid:options": selenoid = True selenoid_options = desired_caps[key] elif key == "screenResolution": screen_resolution = desired_caps[key] + elif key == "version" or key == "browserVersion": + browser_version = desired_caps[key] + elif key == "platform" or key == "platformName": + platform_name = desired_caps[key] if selenium4: remote_options = ArgOptions() remote_options.set_capability("cloud:options", desired_caps) @@ -1261,6 +1342,12 @@ def get_remote_driver( if screen_resolution: scres = screen_resolution remote_options.set_capability("screenResolution", scres) + if browser_version: + br_vers = browser_version + firefox_options.set_capability("browserVersion", br_vers) + if platform_name: + plat_name = platform_name + firefox_options.set_capability("platformName", plat_name) return webdriver.Remote( command_executor=address, options=remote_options, From 20455f9a3ec31a876a5e98916c6a5aa9f3882224 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 00:51:20 -0500 Subject: [PATCH 02/10] Add more browser types for local Selenium Grid nodes --- seleniumbase/utilities/selenium_grid/grid-node | 2 +- seleniumbase/utilities/selenium_grid/grid_node.py | 2 ++ seleniumbase/utilities/selenium_grid/register-grid-node.bat | 2 +- seleniumbase/utilities/selenium_grid/register-grid-node.sh | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/seleniumbase/utilities/selenium_grid/grid-node b/seleniumbase/utilities/selenium_grid/grid-node index 1c3908713b9..1e538bcac1e 100755 --- a/seleniumbase/utilities/selenium_grid/grid-node +++ b/seleniumbase/utilities/selenium_grid/grid-node @@ -40,7 +40,7 @@ if [ "$GRID_NODE_VERBOSE_LOGS" == "True" ]; then fi WEBDRIVER_SERVER_JAR=${DIR}/selenium-server-standalone.jar -WEBDRIVER_NODE_PARAMS="-role node -hub http://${GRID_HUB_SERVER_IP}:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver" +WEBDRIVER_NODE_PARAMS="-role node -hub http://${GRID_HUB_SERVER_IP}:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=opera,maxInstances=5,version=latest,seleniumProtocol=WebDriver" WEBDRIVER_NODE_PIDFILE="/tmp/webdriver_node.pid" if [ ! -f $WEBDRIVER_SERVER_JAR ]; then diff --git a/seleniumbase/utilities/selenium_grid/grid_node.py b/seleniumbase/utilities/selenium_grid/grid_node.py index fa6f25ffb71..5304110ce79 100755 --- a/seleniumbase/utilities/selenium_grid/grid_node.py +++ b/seleniumbase/utilities/selenium_grid/grid_node.py @@ -120,6 +120,8 @@ def main(): """Name=chrome,maxInstances=5,version=latest,""" """seleniumProtocol=WebDriver -browser browserName=firefox,""" """maxInstances=5,version=latest,seleniumProtocol=WebDriver""" + """ -browser browserName=opera,""" + """maxInstances=5,version=latest,seleniumProtocol=WebDriver""" % (dir_path, server_ip) ) print("\nStarting Selenium-WebDriver Grid node...\n") diff --git a/seleniumbase/utilities/selenium_grid/register-grid-node.bat b/seleniumbase/utilities/selenium_grid/register-grid-node.bat index aed2f9078a9..b8cb6820277 100755 --- a/seleniumbase/utilities/selenium_grid/register-grid-node.bat +++ b/seleniumbase/utilities/selenium_grid/register-grid-node.bat @@ -1 +1 @@ -java -jar selenium-server-standalone.jar -role node -hub http://127.0.0.1:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver \ No newline at end of file +java -jar selenium-server-standalone.jar -role node -hub http://127.0.0.1:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=opera,maxInstances=5,version=latest,seleniumProtocol=WebDriver \ No newline at end of file diff --git a/seleniumbase/utilities/selenium_grid/register-grid-node.sh b/seleniumbase/utilities/selenium_grid/register-grid-node.sh index c80b1c7c8bc..0ac2e5868f5 100755 --- a/seleniumbase/utilities/selenium_grid/register-grid-node.sh +++ b/seleniumbase/utilities/selenium_grid/register-grid-node.sh @@ -1,2 +1,2 @@ #!/bin/bash -java -jar selenium-server-standalone.jar -role node -hub http://127.0.0.1:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver \ No newline at end of file +java -jar selenium-server-standalone.jar -role node -hub http://127.0.0.1:4444/grid/register -browser browserName=chrome,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=firefox,maxInstances=5,version=latest,seleniumProtocol=WebDriver -browser browserName=opera,maxInstances=5,version=latest,seleniumProtocol=WebDriver \ No newline at end of file From 780f6c2f83bf6eb5617e52fba531ac6e75f775d4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 00:53:26 -0500 Subject: [PATCH 03/10] Better error messages when converting from XPath to CSS --- seleniumbase/fixtures/xpath_to_css.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/seleniumbase/fixtures/xpath_to_css.py b/seleniumbase/fixtures/xpath_to_css.py index e4c0402a700..5f7164f1eaa 100755 --- a/seleniumbase/fixtures/xpath_to_css.py +++ b/seleniumbase/fixtures/xpath_to_css.py @@ -56,7 +56,7 @@ def _handle_brackets_in_strings(xpath): return new_xpath -def _filter_xpath_grouping(xpath): +def _filter_xpath_grouping(xpath, original): """ This method removes the outer parentheses for xpath grouping. The xpath converter will break otherwise. @@ -71,12 +71,16 @@ def _filter_xpath_grouping(xpath): index = xpath.rfind(")") index_p1 = index + 1 # Make "flake8" and "black" agree if index == -1: - raise XpathException("Invalid or unsupported Xpath: %s" % xpath) + raise XpathException( + "\nInvalid or unsupported XPath:\n%s\n" + "(Unable to convert XPath Selector to CSS Selector)" + "" % original + ) xpath = xpath[:index] + xpath[index_p1:] return xpath -def _get_raw_css_from_xpath(xpath): +def _get_raw_css_from_xpath(xpath, original): css = "" attr = "" position = 0 @@ -84,7 +88,11 @@ def _get_raw_css_from_xpath(xpath): while position < len(xpath): node = prog.match(xpath[position:]) if node is None: - raise XpathException("Invalid or unsupported Xpath: %s" % xpath) + raise XpathException( + "\nInvalid or unsupported XPath:\n%s\n" + "(Unable to convert XPath Selector to CSS Selector)" + "" % original + ) match = node.groupdict() if position != 0: @@ -135,6 +143,7 @@ def _get_raw_css_from_xpath(xpath): def convert_xpath_to_css(xpath): + original = xpath xpath = xpath.replace(" = '", "='") # **** Start of handling special xpath edge cases instantly **** @@ -204,7 +213,7 @@ def convert_xpath_to_css(xpath): xpath = xpath.replace(swap, "_STAR_=") if xpath.startswith("("): - xpath = _filter_xpath_grouping(xpath) + xpath = _filter_xpath_grouping(xpath, original) css = "" if "/descORself/" in xpath and ("@id" in xpath or "@class" in xpath): @@ -213,10 +222,12 @@ def convert_xpath_to_css(xpath): for xpath_section in xpath_sections: if not xpath_section.startswith("//"): xpath_section = "//" + xpath_section - css_sections.append(_get_raw_css_from_xpath(xpath_section)) + css_sections.append(_get_raw_css_from_xpath( + xpath_section, original) + ) css = "/descORself/".join(css_sections) else: - css = _get_raw_css_from_xpath(xpath) + css = _get_raw_css_from_xpath(xpath, original) attribute_defs = re.findall(r"(\[\w+\=\S+\])", css) for attr_def in attribute_defs: From e1e2c976b801e61b69a628bcc33eac5b3c87b6c9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 00:57:37 -0500 Subject: [PATCH 04/10] Better compatibility when running automation on Safari --- seleniumbase/fixtures/base_case.py | 13 +++++- seleniumbase/fixtures/page_actions.py | 57 +++++++++++++++++++++------ 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index d9c3ffe325e..1438af92971 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1245,6 +1245,8 @@ def get_text(self, selector, by=By.CSS_SELECTOR, timeout=None): ) try: element_text = element.text + if self.browser == "safari": + element_text = element.get_attribute("innerText") except (StaleElementReferenceException, ENI_Exception): self.wait_for_ready_state_complete() time.sleep(0.14) @@ -1252,6 +1254,8 @@ def get_text(self, selector, by=By.CSS_SELECTOR, timeout=None): self.driver, selector, by, timeout ) element_text = element.text + if self.browser == "safari": + element_text = element.get_attribute("innerText") return element_text def get_attribute( @@ -6214,6 +6218,11 @@ def __is_shadow_element_enabled(self, selector): def __is_shadow_text_visible(self, text, selector): 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 @@ -9243,7 +9252,7 @@ def wait_for_text_visible( text, selector, timeout ) return page_actions.wait_for_text_visible( - self.driver, text, selector, by, timeout + self.driver, text, selector, by, timeout, self.browser ) def wait_for_exact_text_visible( @@ -9260,7 +9269,7 @@ def wait_for_exact_text_visible( text, selector, timeout ) return page_actions.wait_for_exact_text_visible( - self.driver, text, selector, by, timeout + self.driver, text, selector, by, timeout, self.browser ) def wait_for_text( diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index 9c9b4d97f57..df90b70d0a8 100755 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -31,6 +31,7 @@ from selenium.common.exceptions import NoSuchFrameException from selenium.common.exceptions import NoSuchWindowException from selenium.common.exceptions import StaleElementReferenceException +from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from seleniumbase.config import settings @@ -384,7 +385,12 @@ def wait_for_element_visible( def wait_for_text_visible( - driver, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT + driver, + text, + selector, + by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT, + browser=None ): """ Searches for the specified element by the given selector. Returns the @@ -412,11 +418,21 @@ def wait_for_text_visible( try: element = driver.find_element(by=by, value=selector) is_present = True - if element.is_displayed() and text in element.text: - return element + if browser == "safari": + if ( + element.is_displayed() + and text in element.get_attribute("innerText") + ): + return element + else: + element = None + raise Exception() else: - element = None - raise Exception() + if element.is_displayed() and text in element.text: + return element + else: + element = None + raise Exception() except Exception: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: @@ -443,7 +459,12 @@ def wait_for_text_visible( def wait_for_exact_text_visible( - driver, text, selector, by=By.CSS_SELECTOR, timeout=settings.LARGE_TIMEOUT + driver, + text, + selector, + by=By.CSS_SELECTOR, + timeout=settings.LARGE_TIMEOUT, + browser=None ): """ Searches for the specified element by the given selector. Returns the @@ -471,11 +492,25 @@ def wait_for_exact_text_visible( try: element = driver.find_element(by=by, value=selector) is_present = True - if element.is_displayed() and text.strip() == element.text.strip(): - return element + if browser == "safari": + if ( + element.is_displayed() + and text.strip() == element.get_attribute( + "innerText").strip() + ): + return element + else: + element = None + raise Exception() else: - element = None - raise Exception() + if ( + element.is_displayed() + and text.strip() == element.text.strip() + ): + return element + else: + element = None + raise Exception() except Exception: now_ms = time.time() * 1000.0 if now_ms >= stop_ms: @@ -958,7 +993,7 @@ def switch_to_frame(driver, frame, timeout=settings.SMALL_TIMEOUT): try: driver.switch_to.frame(frame) return True - except NoSuchFrameException: + except (NoSuchFrameException, TimeoutException): if type(frame) is str: by = None if page_utils.is_xpath_selector(frame): From 2c92555c35c236ec2ab45e204398b4f40d64e63b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 00:59:15 -0500 Subject: [PATCH 05/10] Update example tests --- examples/basic_test.py | 22 +++++++----- examples/list_assert_test.py | 4 +-- examples/my_first_test.py | 44 +++++++++++++++--------- examples/swag_labs_suite.py | 2 +- examples/test_download_files.py | 8 ++--- examples/test_event_firing.py | 9 +++-- examples/test_swag_labs.py | 4 +-- examples/test_xkcd.py | 15 ++++++++ examples/xpath_test.py | 10 +++--- seleniumbase/console_scripts/sb_mkdir.py | 37 ++++++++++++-------- 10 files changed, 99 insertions(+), 56 deletions(-) create mode 100755 examples/test_xkcd.py diff --git a/examples/basic_test.py b/examples/basic_test.py index 61ab446a551..7d00a171c10 100755 --- a/examples/basic_test.py +++ b/examples/basic_test.py @@ -1,14 +1,18 @@ +""" +Add an item to a shopping cart, and verify. +""" from seleniumbase import BaseCase class MyTestClass(BaseCase): def test_basics(self): - self.open("https://store.xkcd.com/search") - self.type('input[name="q"]', "xkcd book\n") - self.assert_text("xkcd book", "div.results") - self.open("https://xkcd.com/353/") - self.click('a[rel="license"]') - self.go_back() - self.click_link("About") - self.click_link("comic #249") - self.assert_element('img[alt*="Chess"]') + self.open("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("#inventory_container") + self.assert_exact_text("PRODUCTS", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_exact_text("YOUR CART", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.js_click("a#logout_sidebar_link") diff --git a/examples/list_assert_test.py b/examples/list_assert_test.py index 3fd574ef82a..aba9118fa75 100644 --- a/examples/list_assert_test.py +++ b/examples/list_assert_test.py @@ -7,8 +7,8 @@ class MyTestClass(BaseCase): def test_assert_list_of_elements(self): - self.open("https://store.xkcd.com/collections/posters") + self.open("https://seleniumbase.io/demo_page") self.assert_elements_present("head", "style", "script") self.assert_elements("h1", "h2", "h3") - my_list = ["#top-menu", "#col-main", "#col-widgets"] + my_list = ["#myDropdown", "#myButton", "#svgRect"] self.assert_elements(my_list) diff --git a/examples/my_first_test.py b/examples/my_first_test.py index 53040c0a3a0..b70ea87dcfd 100755 --- a/examples/my_first_test.py +++ b/examples/my_first_test.py @@ -1,21 +1,31 @@ +""" +A complete end-to-end test for an e-commerce website. +""" from seleniumbase import BaseCase class MyTestClass(BaseCase): - def test_basics(self): - url = "https://store.xkcd.com/collections/posters" - self.open(url) - self.type('input[name="q"]', "xkcd book") - self.click('input[value="Search"]') - self.assert_text("xkcd: volume 0", "h3") - self.open("https://xkcd.com/353/") - self.assert_title("xkcd: Python") - self.assert_element('img[alt="Python"]') - self.click('a[rel="license"]') - self.assert_text("free to copy and reuse") - self.go_back() - self.click_link("About") - self.assert_exact_text("xkcd.com", "h2") + def test_swag_labs(self): + self.open("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("#inventory_container") + self.assert_text("PRODUCTS", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("YOUR CART", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("CHECKOUT: OVERVIEW") + self.assert_text("Backpack", "div.cart_item") + self.click("button#finish") + self.assert_exact_text("THANK YOU FOR YOUR ORDER", "h2") + self.assert_element('img[alt="Pony Express"]') + self.js_click("a#logout_sidebar_link") #### @@ -123,10 +133,12 @@ def test_basics(self): # whitespace in the TEXT assertion. # So, self.assert_exact_text("Some Text") will find [" Some Text "]. # - # 7. If a URL starts with "://", then "https://" is automatically used. + # 7. self.js_click(SELECTOR) can be used to click on hidden elements. + # + # 8. If a URL starts with "://", then "https://" is automatically used. # Example: [self.open("://URL")] becomes [self.open("https://URL")] # This helps by reducing the line length by 5 characters. # - # 8. For the full method list, see one of the following: + # 9. For the full method list, see one of the following: # * SeleniumBase/seleniumbase/fixtures/base_case.py # * SeleniumBase/help_docs/method_summary.md diff --git a/examples/swag_labs_suite.py b/examples/swag_labs_suite.py index 78f6045af27..976313939b9 100755 --- a/examples/swag_labs_suite.py +++ b/examples/swag_labs_suite.py @@ -45,7 +45,7 @@ def test_swag_labs_basic_flow(self, username): self.assert_exact_text("1", "span.shopping_cart_badge") # Verify your cart - self.click("#shopping_cart_container") + self.click("#shopping_cart_container a") self.assert_element('span:contains("Your Cart")') self.assert_text(item_name, "div.inventory_item_name") self.assert_exact_text("1", "div.cart_quantity") diff --git a/examples/test_download_files.py b/examples/test_download_files.py index 4e8e11c25d4..1ab5ea7ea7d 100644 --- a/examples/test_download_files.py +++ b/examples/test_download_files.py @@ -60,12 +60,12 @@ def test_download_files(self): # Get file sizes in kB to compare actual values with displayed values whl_file_kb = whl_file_bytes / 1000.0 - whl_line_fi = self.get_text('a[href$=".whl"]') - whl_line = self.get_text('tbody th:contains("%s")' % whl_line_fi) + whl_line_fi = self.get_text('a[href$=".whl"]').strip() + whl_line = self.get_text('div.file:contains("%s")' % whl_line_fi) whl_display_kb = float(whl_line.split("(")[1].split(" ")[0]) tar_gz_file_kb = tar_gz_file_bytes / 1000.0 - tar_gz_line_fi = self.get_text('a[href$=".tar.gz"]') - tar_gz_line = self.get_text('tbody th:contains("%s")' % tar_gz_line_fi) + tar_gz_line_fi = self.get_text('a[href$=".tar.gz"]').strip() + tar_gz_line = self.get_text('div.file:contains("%s")' % tar_gz_line_fi) tar_gz_display_kb = float(tar_gz_line.split("(")[1].split(" ")[0]) # Verify downloaded files are the correct size (account for rounding) diff --git a/examples/test_event_firing.py b/examples/test_event_firing.py index 8adaf8ad7a1..938810beb77 100755 --- a/examples/test_event_firing.py +++ b/examples/test_event_firing.py @@ -31,6 +31,9 @@ def test_event_firing_webdriver(self): print("\n* EventFiringWebDriver example *") self.open("https://xkcd.com/1862/") self.click("link=About") - self.open("https://store.xkcd.com/search") - self.type('input[name="q"]', "xkcd book\n") - self.open("https://xkcd.com/1822/") + self.open("https://xkcd.com/1820/") + self.assert_text("Security Advice", "#ctitle") + self.click('a:contains("Next >")') + self.assert_text("Incinerator", "#ctitle") + self.click('a[rel="next"]') + self.assert_text("Existential Bug Reports", "#ctitle") diff --git a/examples/test_swag_labs.py b/examples/test_swag_labs.py index 5ea8f61fa5e..beb8d8edd49 100755 --- a/examples/test_swag_labs.py +++ b/examples/test_swag_labs.py @@ -34,7 +34,7 @@ def test_swag_labs_basic_flow(self): self.assert_exact_text("1", "span.shopping_cart_badge") # Verify your cart - self.click("#shopping_cart_container") + self.click("#shopping_cart_container a") self.assert_element('span:contains("Your Cart")') self.assert_text(item_name, "div.inventory_item_name") self.assert_exact_text("1", "div.cart_quantity") @@ -60,7 +60,7 @@ def test_swag_labs_basic_flow(self): # Finish Checkout and verify that the cart is now empty self.click("button#finish") self.assert_exact_text("THANK YOU FOR YOUR ORDER", "h2") - self.click("#shopping_cart_container") + self.click("#shopping_cart_container a") self.assert_element_absent("div.inventory_item_name") self.click("button#continue-shopping") self.assert_element_absent("span.shopping_cart_badge") diff --git a/examples/test_xkcd.py b/examples/test_xkcd.py new file mode 100755 index 00000000000..e4772c42832 --- /dev/null +++ b/examples/test_xkcd.py @@ -0,0 +1,15 @@ +from seleniumbase import BaseCase + + +class MyTestClass(BaseCase): + def test_xkcd(self): + self.open("https://xkcd.com/353/") + self.assert_title("xkcd: Python") + self.assert_element('img[alt="Python"]') + self.click('a[rel="license"]') + self.assert_text("free to copy and reuse") + self.go_back() + self.click_link("About") + self.assert_exact_text("xkcd.com", "h2") + self.click_link("comic #249") + self.assert_element('img[alt*="Chess"]') diff --git a/examples/xpath_test.py b/examples/xpath_test.py index 182c6ba306d..49fecccd97a 100755 --- a/examples/xpath_test.py +++ b/examples/xpath_test.py @@ -3,8 +3,8 @@ class XPathTests(BaseCase): def test_xpath(self): - self.open("https://xkcd.com/1319/") - self.assert_element("//img") - self.assert_element("/html/body/div[2]/div[2]/img") - self.click("//ul/li[6]/a") - self.assert_text("xkcd.com", "//h2") + self.open("https://seleniumbase.io/demo_page") + self.assert_element("/html/body/form/table/tbody/tr[1]/td[1]/h1") + self.type('//*[@id="myTextInput"]', "XPath Test!") + self.click("/html/body/form/table/tbody/tr[3]/td[4]/button") + self.assert_text("SeleniumBase", '//table/tbody/tr[1]/td[2]/h2') diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index 4fca0f282f6..673003ac374 100755 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -315,20 +315,29 @@ def main(): data.append("") data.append("") data.append("class MyTestClass(BaseCase):") - data.append(" def test_basics(self):") - data.append(' url = "https://store.xkcd.com/collections/posters"') - data.append(" self.open(url)") - data.append(' self.type(\'input[name="q"]\', "xkcd book")') - data.append(" self.click('input[value=\"Search\"]')") - data.append(' self.assert_text("xkcd: volume 0", "h3")') - data.append(' self.open("https://xkcd.com/353/")') - data.append(' self.assert_title("xkcd: Python")') - data.append(" self.assert_element('img[alt=\"Python\"]')") - data.append(" self.click('a[rel=\"license\"]')") - data.append(' self.assert_text("free to copy and reuse")') - data.append(" self.go_back()") - data.append(' self.click_link("About")') - data.append(' self.assert_exact_text("xkcd.com", "h2")') + data.append(" def test_swag_labs(self):") + data.append(' self.open("https://www.saucedemo.com")') + data.append(' self.type("#user-name", "standard_user")') + data.append(' self.type("#password", "secret_sauce\\n")') + data.append(' self.assert_element("#inventory_container")') + data.append(' self.assert_text("PRODUCTS", "span.title")') + data.append(" self.click('button[name*=\"backpack\"]')") + data.append(' self.click("#shopping_cart_container a")') + data.append(' self.assert_text("YOUR CART", "span.title")') + data.append(' self.assert_text("Backpack", "div.cart_item")') + data.append(' self.click("button#checkout")') + data.append(' self.type("#first-name", "SeleniumBase")') + data.append(' self.type("#last-name", "Automation")') + data.append(' self.type("#postal-code", "77123")') + data.append(' self.click("input#continue")') + data.append(' self.assert_text("CHECKOUT: OVERVIEW")') + data.append(' self.assert_text("Backpack", "div.cart_item")') + data.append(' self.click("button#finish")') + data.append( + ' self.assert_exact_text("THANK YOU FOR YOUR ORDER", "h2")' + ) + data.append(" self.assert_element('img[alt=\"Pony Express\"]')") + data.append(' self.js_click("a#logout_sidebar_link")') data.append("") file_path = "%s/%s" % (dir_name, "my_first_test.py") file = codecs.open(file_path, "w+", "utf-8") From 39a3696f7649672ebeda9f1cec01d3fed1d12683 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 01:00:24 -0500 Subject: [PATCH 06/10] Improve Shadow-DOM compatibility --- seleniumbase/fixtures/base_case.py | 53 ++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 1438af92971..a470bd3ccb7 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -5895,7 +5895,9 @@ def skip(self, reason=""): # Shadow DOM / Shadow-root methods - def __get_shadow_element(self, selector, timeout=None): + 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 @@ -5911,6 +5913,7 @@ def __get_shadow_element(self, selector, timeout=None): 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 ( @@ -5997,6 +6000,11 @@ def __get_shadow_element(self, selector, timeout=None): try: element = shadow_root.find_element( By.CSS_SELECTOR, value=selector_part) + is_present = True + if must_be_visible: + if not element.is_displayed(): + raise Exception( + "Shadow Root element not visible!") found = True break except Exception: @@ -6005,6 +6013,10 @@ def __get_shadow_element(self, selector, timeout=None): if not found: element = shadow_root.find_element( By.CSS_SELECTOR, value=selector_part) + is_present = True + if 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, @@ -6013,11 +6025,16 @@ def __get_shadow_element(self, selector, timeout=None): 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 not present after %s seconds!" - % (selector_chain, timeout) + "Shadow DOM Element {%s} was %s after %s seconds!" + % (selector_chain, error, timeout) ) - page_actions.timeout_exception("NoSuchElementException", msg) + page_actions.timeout_exception(the_exception, msg) return element def __fail_if_invalid_shadow_selector_usage(self, selector): @@ -6035,11 +6052,15 @@ def __is_shadow_selector(self, selector): return False def __shadow_click(self, selector, timeout): - element = self.__get_shadow_element(selector, timeout=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) + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) if clear_first: try: element.clear() @@ -6060,7 +6081,9 @@ def __shadow_type(self, selector, text, timeout, clear_first=True): self.wait_for_ready_state_complete() def __shadow_clear(self, selector, timeout): - element = self.__get_shadow_element(selector, timeout=timeout) + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) try: element.clear() backspaces = Keys.BACK_SPACE * 42 # Autofill Defense @@ -6069,8 +6092,13 @@ def __shadow_clear(self, selector, timeout): pass def __get_shadow_text(self, selector, timeout): - element = self.__get_shadow_element(selector, timeout=timeout) - return element.text + 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) @@ -6248,10 +6276,9 @@ def __wait_for_shadow_element_present(self, selector, timeout): return element def __wait_for_shadow_element_visible(self, selector, timeout): - element = self.__get_shadow_element(selector, timeout=timeout) - if not element.is_displayed(): - msg = "Shadow DOM Element {%s} was not visible!" % selector - page_actions.timeout_exception("NoSuchElementException", msg) + element = self.__get_shadow_element( + selector, timeout=timeout, must_be_visible=True + ) return element def __wait_for_shadow_attribute_present( From 76c7e70b6cb6ec420b133b5fa0c3a92fef4acdc2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 01:01:27 -0500 Subject: [PATCH 07/10] Refresh docs dependencies --- mkdocs_build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 3d805e05e21..38b5f996dd2 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -21,7 +21,7 @@ lunr==0.6.1;python_version>="3.6" nltk==3.6.7;python_version>="3.6" watchdog==2.1.6;python_version>="3.6" mkdocs==1.2.3;python_version>="3.6" -mkdocs-material==8.1.8;python_version>="3.6" +mkdocs-material==8.1.9;python_version>="3.6" mkdocs-exclude-search==0.6.4;python_version>="3.6" mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.0.3;python_version>="3.6" From 32c1e0f768331077a052afba79e920e586f2a197 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 01:03:43 -0500 Subject: [PATCH 08/10] Refresh Python dependencies --- requirements.txt | 13 +++++++------ setup.py | 13 +++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/requirements.txt b/requirements.txt index 63da7690b7e..b68922815af 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,12 @@ pip>=20.3.4;python_version<"3.6" -pip>=21.3.1;python_version>="3.6" +pip>=21.3.1;python_version>="3.6" and python_version<"3.7" +pip>=22.0.2;python_version>="3.7" packaging>=20.9;python_version<"3.6" packaging>=21.3;python_version>="3.6" setuptools>=44.1.1;python_version<"3.5" setuptools>=50.3.2;python_version>="3.5" and python_version<"3.6" setuptools>=59.6.0;python_version>="3.6" and python_version<"3.7" -setuptools>=60.5.0;python_version>="3.7" +setuptools>=60.6.0;python_version>="3.7" setuptools-scm>=5.0.2;python_version<"3.6" setuptools-scm>=6.4.2;python_version>="3.6" tomli>=1.2.2;python_version>="3.6" and python_version<"3.7" @@ -27,7 +28,7 @@ idna==2.10;python_version<"3.6" idna==3.3;python_version>="3.6" chardet==3.0.4;python_version<"3.5" chardet==4.0.0;python_version>="3.5" -charset-normalizer==2.0.10;python_version>="3.5" +charset-normalizer==2.0.11;python_version>="3.5" urllib3==1.26.8 requests==2.27.0;python_version<"3.5" requests==2.25.1;python_version>="3.5" and python_version<"3.6" @@ -37,7 +38,7 @@ sniffio==1.2.0;python_version>="3.7" h11==0.13.0;python_version>="3.7" trio==0.19.0;python_version>="3.7" trio-websocket==0.9.2;python_version>="3.7" -pyopenssl==21.0.0;python_version>="3.7" +pyopenssl==22.0.0;python_version>="3.7" wsproto==1.0.0;python_version>="3.7" selenium==3.141.0;python_version<"3.7" selenium==4.1.0;python_version>="3.7" @@ -88,7 +89,7 @@ pygments==2.5.2;python_version<"3.5" pygments==2.11.2;python_version>="3.5" 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.24;python_version>="3.6.2" +prompt-toolkit==3.0.26;python_version>="3.6.2" decorator==4.4.2;python_version<"3.5" decorator==5.1.1;python_version>="3.5" ipython==5.10.0;python_version<"3.5" @@ -118,7 +119,7 @@ Pillow==8.4.0;python_version>="3.6" and python_version<"3.7" Pillow==9.0.0;python_version>="3.7" typing-extensions==3.10.0.2;python_version<"3.6" typing-extensions==4.0.0;python_version>="3.6" and python_version<"3.8" -rich==11.0.0;python_version>="3.6" and python_version<"4.0" +rich==11.1.0;python_version>="3.6.2" and python_version<"4.0" tornado==5.1.1;python_version<"3.5" tornado==6.1;python_version>="3.5" pdfminer.six==20191110;python_version<"3.5" diff --git a/setup.py b/setup.py index 31e0bd4a662..ccff972d233 100755 --- a/setup.py +++ b/setup.py @@ -124,13 +124,14 @@ 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.3.1;python_version>="3.6"', + 'pip>=21.3.1;python_version>="3.6" and python_version<"3.7"', + 'pip>=22.0.2;python_version>="3.7"', 'packaging>=20.9;python_version<"3.6"', 'packaging>=21.3;python_version>="3.6"', 'setuptools>=44.1.1;python_version<"3.5"', 'setuptools>=50.3.2;python_version>="3.5" and python_version<"3.6"', 'setuptools>=59.6.0;python_version>="3.6" and python_version<"3.7"', - 'setuptools>=60.5.0;python_version>="3.7"', + 'setuptools>=60.6.0;python_version>="3.7"', 'setuptools-scm>=5.0.2;python_version<"3.6"', 'setuptools-scm>=6.4.2;python_version>="3.6"', 'tomli>=1.2.2;python_version>="3.6" and python_version<"3.7"', @@ -152,7 +153,7 @@ 'idna==3.3;python_version>="3.6"', # Must 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.10;python_version>="3.5"', # Sync "requests" + 'charset-normalizer==2.0.11;python_version>="3.5"', # Sync "requests" "urllib3==1.26.8", # Must stay in sync with "requests" 'requests==2.27.0;python_version<"3.5"', 'requests==2.25.1;python_version>="3.5" and python_version<"3.6"', @@ -162,7 +163,7 @@ 'h11==0.13.0;python_version>="3.7"', 'trio==0.19.0;python_version>="3.7"', 'trio-websocket==0.9.2;python_version>="3.7"', - 'pyopenssl==21.0.0;python_version>="3.7"', + 'pyopenssl==22.0.0;python_version>="3.7"', 'wsproto==1.0.0;python_version>="3.7"', 'selenium==3.141.0;python_version<"3.7"', 'selenium==4.1.0;python_version>="3.7"', @@ -213,7 +214,7 @@ 'pygments==2.11.2;python_version>="3.5"', '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.24;python_version>="3.6.2"', + 'prompt-toolkit==3.0.26;python_version>="3.6.2"', 'decorator==4.4.2;python_version<"3.5"', 'decorator==5.1.1;python_version>="3.5"', 'ipython==5.10.0;python_version<"3.5"', @@ -243,7 +244,7 @@ 'Pillow==9.0.0;python_version>="3.7"', 'typing-extensions==3.10.0.2;python_version<"3.6"', # <3.8 for "rich" 'typing-extensions==4.0.0;python_version>="3.6" and python_version<"3.8"', # noqa: E501 - 'rich==11.0.0;python_version>="3.6" and python_version<"4.0"', + 'rich==11.1.0;python_version>="3.6.2" and python_version<"4.0"', 'tornado==5.1.1;python_version<"3.5"', 'tornado==6.1;python_version>="3.5"', 'pdfminer.six==20191110;python_version<"3.5"', From ec82ca0496acbcfaed9143a754b9c3e48a216304 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 01:04:06 -0500 Subject: [PATCH 09/10] Update the docs --- README.md | 31 ++++++++++++++++++++++++++++++- examples/ReadMe.md | 10 ++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 97d15ff5db8..8456ab494f5 100755 --- a/README.md +++ b/README.md @@ -149,7 +149,36 @@ pytest my_first_test.py --demo > (Chrome is the default browser if not specified with ``--browser``. On Linux, ``--headless`` is the default behavior.) -SeleniumBase Test +SeleniumBase Test + +

Here's the code for my_first_test.py:

+ +```python +from seleniumbase import BaseCase + +class MyTestClass(BaseCase): + def test_swag_labs(self): + self.open("https://www.saucedemo.com") + self.type("#user-name", "standard_user") + self.type("#password", "secret_sauce\n") + self.assert_element("#inventory_container") + self.assert_text("PRODUCTS", "span.title") + self.click('button[name*="backpack"]') + self.click("#shopping_cart_container a") + self.assert_text("YOUR CART", "span.title") + self.assert_text("Backpack", "div.cart_item") + self.click("button#checkout") + self.type("#first-name", "SeleniumBase") + self.type("#last-name", "Automation") + self.type("#postal-code", "77123") + self.click("input#continue") + self.assert_text("CHECKOUT: OVERVIEW") + self.assert_text("Backpack", "div.cart_item") + self.click("button#finish") + self.assert_exact_text("THANK YOU FOR YOUR ORDER", "h2") + self.assert_element('img[alt="Pony Express"]') + self.js_click("a#logout_sidebar_link") +``` * By default, **[CSS Selectors](https://www.w3schools.com/cssref/css_selectors.asp)** are used for finding page elements. * If you're new to CSS Selectors, games like [CSS Diner](http://flukeout.github.io/) can help you learn. diff --git a/examples/ReadMe.md b/examples/ReadMe.md index 6c5f4e6427f..eb33971b46b 100755 --- a/examples/ReadMe.md +++ b/examples/ReadMe.md @@ -21,6 +21,8 @@ Run an example test in Chrome: (Default: ``--browser=chrome``) pytest my_first_test.py ``` +
+ Run an example test in Firefox: ```bash @@ -29,14 +31,6 @@ pytest my_first_test.py --browser=firefox Run an example test in Demo Mode: (highlight assertions) -```bash -pytest my_first_test.py --demo -``` - -
- -Run a different example in Demo Mode: - ```bash pytest test_swag_labs.py --demo ``` From dd60a75ac668cd4dde0ebad7835eeac729e302b9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 1 Feb 2022 01:05:31 -0500 Subject: [PATCH 10/10] Version 2.4.4 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 0f4d03129dc..914bf13bb82 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "2.4.3" +__version__ = "2.4.4"