diff --git a/examples/raw_browser_launcher.py b/examples/raw_browser_launcher.py index a25ae79ee7f..8d0c193ce6c 100644 --- a/examples/raw_browser_launcher.py +++ b/examples/raw_browser_launcher.py @@ -1,27 +1,22 @@ """Driver() test. Runs with "python". (pytest not needed).""" from seleniumbase import Driver -from seleniumbase import js_utils -from seleniumbase import page_actions -# Example with options. (Also accepts command-line options.) driver = Driver(browser="chrome", headless=False) try: driver.get("https://seleniumbase.io/apps/calculator") - page_actions.wait_for_element(driver, '[id="4"]').click() - page_actions.wait_for_element(driver, '[id="2"]').click() - page_actions.wait_for_text(driver, "42", "#output") - js_utils.highlight_with_js(driver, "#output", loops=6) + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) finally: driver.quit() -# Example 2 using default args or command-line options driver = Driver() try: driver.get("https://seleniumbase.github.io/demo_page") - js_utils.highlight_with_js(driver, "h2", loops=5) - by_css = "css selector" - driver.find_element(by_css, "#myTextInput").send_keys("Automation") - driver.find_element(by_css, "#checkBox1").click() - js_utils.highlight_with_js(driver, "img", loops=5) + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) finally: driver.quit() diff --git a/examples/raw_driver_context.py b/examples/raw_driver_context.py index d730f4f00a8..32609eac026 100644 --- a/examples/raw_driver_context.py +++ b/examples/raw_driver_context.py @@ -1,24 +1,20 @@ """Can run with "python". (pytest not needed).""" -from seleniumbase import js_utils -from seleniumbase import page_actions from seleniumbase import DriverContext -# Driver Context Manager - (By default, browser="chrome". Lots of options) with DriverContext() as driver: driver.get("https://seleniumbase.github.io/") - js_utils.highlight_with_js(driver, 'img[alt="SeleniumBase"]', loops=6) + driver.highlight('img[alt="SeleniumBase"]', loops=6) with DriverContext(browser="chrome", incognito=True) as driver: driver.get("https://seleniumbase.io/apps/calculator") - page_actions.wait_for_element(driver, '[id="4"]').click() - page_actions.wait_for_element(driver, '[id="2"]').click() - page_actions.wait_for_text(driver, "42", "#output") - js_utils.highlight_with_js(driver, "#output", loops=6) + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) with DriverContext() as driver: driver.get("https://seleniumbase.github.io/demo_page") - js_utils.highlight_with_js(driver, "h2", loops=5) - by_css = "css selector" - driver.find_element(by_css, "#myTextInput").send_keys("Automation") - driver.find_element(by_css, "#checkBox1").click() - js_utils.highlight_with_js(driver, "img", loops=5) + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) diff --git a/examples/raw_uc_mode.py b/examples/raw_uc_mode.py index 5b42f860eb7..3cbfcbe24e1 100644 --- a/examples/raw_uc_mode.py +++ b/examples/raw_uc_mode.py @@ -2,15 +2,16 @@ from seleniumbase import SB with SB(uc=True) as sb: - sb.open("https://nowsecure.nl/#relax") - sb.sleep(3) + sb.driver.get("https://nowsecure.nl/#relax") + sb.sleep(2) if not sb.is_text_visible("OH YEAH, you passed!", "h1"): sb.get_new_driver(undetectable=True) - sb.open("https://nowsecure.nl/#relax") - sb.sleep(3) + sb.driver.get("https://nowsecure.nl/#relax") + sb.sleep(2) if not sb.is_text_visible("OH YEAH, you passed!", "h1"): if sb.is_element_visible('iframe[src*="challenge"]'): with sb.frame_switch('iframe[src*="challenge"]'): sb.click("area") - sb.sleep(3) + sb.sleep(4) + sb.activate_demo_mode() sb.assert_text("OH YEAH, you passed!", "h1", timeout=3) diff --git a/examples/test_repeat_tests.py b/examples/test_repeat_tests.py index ddf2f083bbb..94511ec999c 100644 --- a/examples/test_repeat_tests.py +++ b/examples/test_repeat_tests.py @@ -10,14 +10,14 @@ class RepeatTests(BaseCase): @parameterized.expand([[]] * 2) def test_repeat_this_test_with_parameterized(self): - self.open("seleniumbase.github.io") + self.open("seleniumbase.github.io/") self.click('a[href="help_docs/method_summary/"]') self.assert_text("API Reference", "h1") @pytest.mark.parametrize("", [[]] * 2) def test_repeat_this_test_with_pytest_parametrize(sb): - sb.open("seleniumbase.github.io") + sb.open("seleniumbase.github.io/") sb.click('a[href="seleniumbase/console_scripts/ReadMe/"]') sb.assert_text("Console Scripts", "h1") @@ -25,6 +25,6 @@ def test_repeat_this_test_with_pytest_parametrize(sb): class RepeatTestsWithPytest: @pytest.mark.parametrize("", [[]] * 2) def test_repeat_test_with_pytest_parametrize(self, sb): - sb.open("seleniumbase.github.io") + sb.open("seleniumbase.github.io/") sb.click('a[href="help_docs/customizing_test_runs/"]') sb.assert_text("Command Line Options", "h1") diff --git a/examples/test_url_asserts.py b/examples/test_url_asserts.py index 14277695090..be5710c3298 100644 --- a/examples/test_url_asserts.py +++ b/examples/test_url_asserts.py @@ -4,8 +4,8 @@ class URLTestClass(BaseCase): def test_url_asserts(self): - self.open("https://seleniumbase.io/") - self.assert_url("https://seleniumbase.io/") + self.open("https://seleniumbase.github.io/") + self.assert_url("https://seleniumbase.github.io/") self.assert_title_contains("SeleniumBase") self.js_click('nav a:contains("Coffee Cart")') self.assert_url_contains("/coffee") diff --git a/examples/uc_cdp_events.py b/examples/uc_cdp_events.py index acb3858666f..b20468e348f 100644 --- a/examples/uc_cdp_events.py +++ b/examples/uc_cdp_events.py @@ -25,13 +25,13 @@ def fail_me(self): def test_display_cdp_events(self): if not (self.undetectable and self.uc_cdp_events): self.get_new_driver(undetectable=True, uc_cdp_events=True) - self.open("https://nowsecure.nl/#relax") + self.driver.get("https://nowsecure.nl/#relax") try: self.verify_success() except Exception: self.clear_all_cookies() self.get_new_driver(undetectable=True, uc_cdp_events=True) - self.open("https://nowsecure.nl/#relax") + self.driver.get("https://nowsecure.nl/#relax") try: self.verify_success() except Exception: diff --git a/examples/verify_undetected.py b/examples/verify_undetected.py index 46dc2ca73a4..c1c45b58bee 100644 --- a/examples/verify_undetected.py +++ b/examples/verify_undetected.py @@ -20,13 +20,13 @@ def fail_me(self): def test_browser_is_undetected(self): if not (self.undetectable): self.get_new_driver(undetectable=True) - self.open("https://nowsecure.nl/#relax") + self.driver.get("https://nowsecure.nl/#relax") try: self.verify_success() except Exception: self.clear_all_cookies() self.get_new_driver(undetectable=True) - self.open("https://nowsecure.nl/#relax") + self.driver.get("https://nowsecure.nl/#relax") try: self.verify_success() except Exception: diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 505d29e07a3..edd27761b3a 100644 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -171,6 +171,11 @@ self.hover(selector, by="css selector", timeout=None) # self.hover_over_element(selector, by="css selector", timeout=None) self.hover_and_click( + hover_selector, click_selector, + hover_by="css selector", click_by="css selector", + timeout=None, js_click=False) + +self.hover_and_js_click( hover_selector, click_selector, hover_by="css selector", click_by="css selector", timeout=None) @@ -418,6 +423,8 @@ self.disable_beforeunload() self.get_domain_url(url) +self.get_active_element_css() + self.get_beautiful_soup(source=None) self.get_unique_links() diff --git a/help_docs/syntax_formats.md b/help_docs/syntax_formats.md index 3cf941cc8d2..c57361f8e37 100644 --- a/help_docs/syntax_formats.md +++ b/help_docs/syntax_formats.md @@ -2,9 +2,9 @@ -## [](https://github.com/seleniumbase/SeleniumBase/) The 23 Syntax Formats / Design Patterns +

The 23 Syntax Formats / Design Patterns

-

SeleniumBase supports multiple ways of structuring tests:

+

🔡 SeleniumBase supports multiple ways of structuring tests:

@@ -873,30 +873,26 @@ with SB(test=True, rtf=True, demo=True) as sb: This pure Python format gives you a raw webdriver instance in a with block. The SeleniumBase Driver Manager will automatically make sure that your driver is compatible with your browser version. It gives you full access to customize driver options via method args or via the command-line. The driver will automatically call quit() after the code leaves the with block. Here are some examples: ```python -"""This script can be run with pure "python". (pytest not needed).""" -from seleniumbase import js_utils -from seleniumbase import page_actions +"""Can run with "python". (pytest not needed).""" from seleniumbase import DriverContext -# Driver Context Manager - (By default, browser="chrome". Lots of options) with DriverContext() as driver: driver.get("https://seleniumbase.github.io/") - js_utils.highlight_with_js(driver, 'img[alt="SeleniumBase"]', loops=6) + driver.highlight('img[alt="SeleniumBase"]', loops=6) with DriverContext(browser="chrome", incognito=True) as driver: driver.get("https://seleniumbase.io/apps/calculator") - page_actions.wait_for_element(driver, '[id="4"]').click() - page_actions.wait_for_element(driver, '[id="2"]').click() - page_actions.wait_for_text(driver, "42", "#output") - js_utils.highlight_with_js(driver, "#output", loops=6) + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) with DriverContext() as driver: driver.get("https://seleniumbase.github.io/demo_page") - js_utils.highlight_with_js(driver, "h2", loops=5) - by_css = "css selector" - driver.find_element(by_css, "#myTextInput").send_keys("Automation") - driver.find_element(by_css, "#checkBox1").click() - js_utils.highlight_with_js(driver, "img", loops=5) + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) ``` (See examples/raw_driver_context.py for an example.) @@ -907,31 +903,48 @@ with DriverContext() as driver: Another way of running Selenium tests with pure ``python`` (as opposed to using ``pytest`` or ``pynose``) is by using this format, which bypasses [BaseCase](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/base_case.py) methods while still giving you a flexible driver with a manager. SeleniumBase includes helper files such as [page_actions.py](https://github.com/seleniumbase/SeleniumBase/blob/master/seleniumbase/fixtures/page_actions.py), which may help you get around some of the limitations of bypassing ``BaseCase``. Here's an example: ```python -"""This script can be run with pure "python". (pytest not needed).""" +"""Driver() test. Runs with "python". (pytest not needed).""" from seleniumbase import Driver -from seleniumbase import js_utils -from seleniumbase import page_actions -# Example with options. (Also accepts command-line options.) driver = Driver(browser="chrome", headless=False) try: driver.get("https://seleniumbase.io/apps/calculator") - page_actions.wait_for_element(driver, '[id="4"]').click() - page_actions.wait_for_element(driver, '[id="2"]').click() - page_actions.wait_for_text(driver, "42", "#output") - js_utils.highlight_with_js(driver, "#output", loops=6) + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) finally: driver.quit() -# Example 2 using default args or command-line options driver = Driver() try: driver.get("https://seleniumbase.github.io/demo_page") - js_utils.highlight_with_js(driver, "h2", loops=5) - by_css = "css selector" - driver.find_element(by_css, "#myTextInput").send_keys("Automation") - driver.find_element(by_css, "#checkBox1").click() - js_utils.highlight_with_js(driver, "img", loops=5) + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) +finally: + driver.quit() +"""Driver() test. Runs with "python". (pytest not needed).""" +from seleniumbase import Driver + +driver = Driver(browser="chrome", headless=False) +try: + driver.get("https://seleniumbase.io/apps/calculator") + driver.click('[id="4"]') + driver.click('[id="2"]') + driver.assert_text("42", "#output") + driver.highlight("#output", loops=6) +finally: + driver.quit() + +driver = Driver() +try: + driver.get("https://seleniumbase.github.io/demo_page") + driver.highlight("h2") + driver.type("#myTextInput", "Automation") + driver.click("#checkBox1") + driver.highlight("img", loops=6) finally: driver.quit() ``` diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 46839cfc69a..776180366c2 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -3,7 +3,7 @@ regex>=2023.8.8 PyYAML>=6.0.1 -pymdown-extensions>=10.2.1 +pymdown-extensions>=10.3 pipdeptree>=2.13.0 python-dateutil>=2.8.2 Markdown==3.4.4 @@ -20,7 +20,7 @@ paginate==0.5.6 pyquery==2.0.0 readtime==3.0.0 mkdocs==1.5.2 -mkdocs-material==9.2.6 +mkdocs-material==9.2.8 mkdocs-exclude-search==0.6.5 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.1.1 diff --git a/requirements.txt b/requirements.txt index 6ff22f8139d..ecd0c5ec79e 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ pip>=23.2.1 packaging>=23.1 setuptools>=68.0.0;python_version<"3.8" -setuptools>=68.1.2;python_version>="3.8" +setuptools>=68.2.0;python_version>="3.8" wheel>=0.41.2 attrs>=23.1.0 certifi>=2023.7.22 @@ -22,7 +22,7 @@ sniffio==1.3.0 h11==0.14.0 outcome==1.2.0 trio==0.22.2 -trio-websocket==0.10.3 +trio-websocket==0.10.4 wsproto==1.2.0 selenium==4.11.2 cssselect==1.2.0 @@ -33,7 +33,7 @@ iniconfig==2.0.0 pluggy==1.2.0;python_version<"3.8" pluggy==1.3.0;python_version>="3.8" py==1.11.0 -pytest==7.4.0 +pytest==7.4.2 pytest-html==2.0.1 pytest-metadata==3.0.0 pytest-ordering==0.6 @@ -42,12 +42,13 @@ pytest-xdist==3.3.1 parameterized==0.9.0 sbvirtualdisplay==1.2.0 behave==1.2.6 -soupsieve==2.4.1 +soupsieve==2.4.1;python_version<"3.8" +soupsieve==2.5;python_version>="3.8" beautifulsoup4==4.12.2 pygments==2.16.1 pyreadline3==3.4.1;platform_system=="Windows" -tabcompleter==1.2.1 -pdbp==1.4.6 +tabcompleter==1.3.0 +pdbp==1.5.0 colorama==0.4.6 exceptiongroup==1.1.3 importlib-metadata==4.2.0;python_version<"3.8" @@ -62,7 +63,7 @@ rich==13.5.2 coverage==6.2;python_version<"3.7" coverage==7.2.7;python_version>="3.7" and python_version<"3.8" -coverage==7.3.0;python_version>="3.8" +coverage==7.3.1;python_version>="3.8" pytest-cov==4.0.0;python_version<"3.7" pytest-cov==4.1.0;python_version>="3.7" flake8==5.0.4;python_version<"3.9" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 8b8caec428b..01927e49145 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.18.1" +__version__ = "4.18.2" diff --git a/seleniumbase/config/proxy_list.py b/seleniumbase/config/proxy_list.py index 0fc5894594e..95c5bdfe0a4 100644 --- a/seleniumbase/config/proxy_list.py +++ b/seleniumbase/config/proxy_list.py @@ -15,11 +15,11 @@ Example proxies in PROXY_LIST below are not guaranteed to be active or secure. If you don't already have a proxy server to connect to, you can try finding one from one of following sites: +* https://www.sslproxies.org/ * https://bit.ly/36GtZa1 * https://www.us-proxy.org/ * https://hidemy.name/en/proxy-list/ * http://free-proxy.cz/en/proxylist/country/all/https/ping/all -* https://github.com/mertguvencli/http-proxy-list """ PROXY_LIST = { diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 132abeea546..df21be99fd1 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -6,6 +6,7 @@ import subprocess import sys import time +import types import urllib3 import warnings from selenium import webdriver @@ -22,6 +23,7 @@ from seleniumbase.core import detect_b_ver from seleniumbase.core import download_helper from seleniumbase.core import proxy_helper +from seleniumbase.core import sb_driver from seleniumbase.fixtures import constants from seleniumbase.fixtures import shared_utils @@ -102,6 +104,66 @@ def make_driver_executable_if_not(driver_path): make_executable(driver_path) +def extend_driver(driver): + # Extend the driver with new methods + DM = sb_driver.DriverMethods(driver) + page = types.SimpleNamespace() + page.open = DM.open_url + page.click = DM.click + page.send_keys = DM.send_keys + page.type = DM.update_text + page.assert_element = DM.assert_element_visible + page.assert_element_present = DM.assert_element_present + page.assert_element_not_visible = DM.assert_element_not_visible + page.assert_text = DM.assert_text + page.assert_exact_text = DM.assert_exact_text + page.wait_for_element = DM.wait_for_element + page.wait_for_text = DM.wait_for_text + page.wait_for_exact_text = DM.wait_for_exact_text + page.wait_for_and_accept_alert = DM.wait_for_and_accept_alert + page.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert + page.is_element_present = DM.is_element_present + page.is_element_visible = DM.is_element_visible + page.is_text_visible = DM.is_text_visible + page.is_exact_text_visible = DM.is_exact_text_visible + page.get_text = DM.get_text + driver.page = page + js = types.SimpleNamespace() + js.js_click = DM.js_click + js.get_active_element_css = DM.get_active_element_css + js.get_locale_code = DM.get_locale_code + js.get_origin = DM.get_origin + js.get_user_agent = DM.get_user_agent + js.highlight = DM.highlight + driver.js = js + driver.open = DM.open_url + driver.click = DM.click + driver.send_keys = DM.send_keys + driver.type = DM.update_text + driver.assert_element = DM.assert_element_visible + driver.assert_element_present = DM.assert_element_present + driver.assert_element_not_visible = DM.assert_element_not_visible + driver.assert_text = DM.assert_text + driver.assert_exact_text = DM.assert_exact_text + driver.wait_for_element = DM.wait_for_element + driver.wait_for_text = DM.wait_for_text + driver.wait_for_exact_text = DM.wait_for_exact_text + driver.wait_for_and_accept_alert = DM.wait_for_and_accept_alert + driver.wait_for_and_dismiss_alert = DM.wait_for_and_dismiss_alert + driver.is_element_present = DM.is_element_present + driver.is_element_visible = DM.is_element_visible + driver.is_text_visible = DM.is_text_visible + driver.is_exact_text_visible = DM.is_exact_text_visible + driver.get_text = DM.get_text + driver.js_click = DM.js_click + driver.get_active_element_css = DM.get_active_element_css + driver.get_locale_code = DM.get_locale_code + driver.get_origin = DM.get_origin + driver.get_user_agent = DM.get_user_agent + driver.highlight = DM.highlight + return driver + + @decorators.rate_limited(4) def requests_get(url, proxy_string=None): import requests @@ -1607,10 +1669,11 @@ def get_remote_driver( for key in extension_capabilities: ext_caps = extension_capabilities chrome_options.set_capability(key, ext_caps[key]) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=chrome_options, ) + return extend_driver(driver) elif browser_name == constants.Browser.FIREFOX: firefox_options = _set_firefox_options( downloads_path, @@ -1666,18 +1729,20 @@ def get_remote_driver( for key in extension_capabilities: ext_caps = extension_capabilities firefox_options.set_capability(key, ext_caps[key]) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=firefox_options, ) + return extend_driver(driver) elif browser_name == constants.Browser.INTERNET_EXPLORER: capabilities = webdriver.DesiredCapabilities.INTERNETEXPLORER remote_options = ArgOptions() remote_options.set_capability("cloud:options", desired_caps) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=remote_options, ) + return extend_driver(driver) elif browser_name == constants.Browser.EDGE: edge_options = _set_chrome_options( browser_name, @@ -1767,26 +1832,29 @@ def get_remote_driver( for key in extension_capabilities: ext_caps = extension_capabilities edge_options.set_capability(key, ext_caps[key]) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=edge_options, ) + return extend_driver(driver) elif browser_name == constants.Browser.SAFARI: capabilities = webdriver.DesiredCapabilities.SAFARI remote_options = ArgOptions() remote_options.set_capability("cloud:options", desired_caps) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=remote_options, ) + return extend_driver(driver) elif browser_name == constants.Browser.REMOTE: remote_options = ArgOptions() for cap_name, cap_value in desired_caps.items(): remote_options.set_capability(cap_name, cap_value) - return webdriver.Remote( + driver = webdriver.Remote( command_executor=address, options=remote_options, ) + return extend_driver(driver) def get_local_driver( @@ -1913,10 +1981,11 @@ def get_local_driver( log_output=os.devnull, ) try: - return webdriver.Firefox( + driver = webdriver.Firefox( service=service, options=firefox_options, ) + return extend_driver(driver) except BaseException as e: if ( "geckodriver unexpectedly exited" in str(e) @@ -1942,19 +2011,21 @@ def get_local_driver( ) ): firefox_options.add_argument("-headless") - return webdriver.Firefox( + driver = webdriver.Firefox( service=service, options=firefox_options, ) + return extend_driver(driver) else: raise # Not an obvious fix. else: service = FirefoxService(log_output=os.devnull) try: - return webdriver.Firefox( + driver = webdriver.Firefox( service=service, options=firefox_options, ) + return extend_driver(driver) except BaseException as e: if ( "geckodriver unexpectedly exited" in str(e) @@ -1980,10 +2051,11 @@ def get_local_driver( ) ): firefox_options.add_argument("-headless") - return webdriver.Firefox( + driver = webdriver.Firefox( service=service, options=firefox_options, ) + return extend_driver(driver) else: raise # Not an obvious fix. elif browser_name == constants.Browser.INTERNET_EXPLORER: @@ -2036,13 +2108,15 @@ def get_local_driver( sys.argv = sys_args # Put back the original sys args if not headless: warnings.simplefilter("ignore", category=DeprecationWarning) - return webdriver.Ie(capabilities=ie_capabilities) + driver = webdriver.Ie(capabilities=ie_capabilities) + return extend_driver(driver) else: warnings.simplefilter("ignore", category=DeprecationWarning) - return webdriver.Ie( + driver = webdriver.Ie( executable_path=LOCAL_HEADLESS_IEDRIVER, capabilities=ie_capabilities, ) + return extend_driver(driver) elif browser_name == constants.Browser.EDGE: prefs = { "download.default_directory": downloads_path, @@ -2384,7 +2458,10 @@ def get_local_driver( chromium_arg_item = "-" + chromium_arg_item else: chromium_arg_item = "--" + chromium_arg_item - if "set-binary" in chromium_arg_item and not binary_location: + if ( + (IS_LINUX or "set-binary" in chromium_arg_item) + and not binary_location + ): br_app = "edge" binary_loc = detect_b_ver.get_binary_location(br_app) if os.path.exists(binary_loc): @@ -2433,7 +2510,8 @@ def get_local_driver( edge_options.add_argument( "--remote-debugging-port=%s" % free_port ) - return Edge(service=service, options=edge_options) + driver = Edge(service=service, options=edge_options) + return extend_driver(driver) if not auto_upgrade_edgedriver: raise # Not an obvious fix. else: @@ -2463,7 +2541,7 @@ def get_local_driver( service_args=["--disable-build-check"], ) driver = Edge(service=service, options=edge_options) - return driver + return extend_driver(driver) elif browser_name == constants.Browser.SAFARI: args = " ".join(sys.argv) if ("-n" in sys.argv or " -n=" in args or args == "-c"): @@ -2487,9 +2565,10 @@ def get_local_driver( ): # Only change it if not "normal", which is the default. options.page_load_strategy = settings.PAGE_LOAD_STRATEGY.lower() - return webdriver.safari.webdriver.WebDriver( + driver = webdriver.safari.webdriver.WebDriver( service=service, options=options ) + return extend_driver(driver) elif browser_name == constants.Browser.GOOGLE_CHROME: try: chrome_options = _set_chrome_options( @@ -3017,9 +3096,10 @@ def get_local_driver( warnings.simplefilter( "ignore", category=DeprecationWarning ) - return webdriver.Chrome( + driver = webdriver.Chrome( service=service, options=chrome_options ) + return extend_driver(driver) if not auto_upgrade_chromedriver: raise # Not an obvious fix. else: @@ -3136,11 +3216,11 @@ def get_local_driver( driver.uc_open_with_reconnect = ( lambda url: uc_open_with_reconnect(driver, url) ) - driver.open = driver.get # Shortcut - return driver + return extend_driver(driver) else: # Running headless on Linux (and not using --uc) try: - return webdriver.Chrome(options=chrome_options) + driver = webdriver.Chrome(options=chrome_options) + return extend_driver(driver) except Exception as e: if not hasattr(e, "msg"): raise @@ -3159,9 +3239,10 @@ def get_local_driver( warnings.simplefilter( "ignore", category=DeprecationWarning ) - return webdriver.Chrome( + driver = webdriver.Chrome( service=service, options=chrome_options ) + return extend_driver(driver) mcv = None # Major Chrome Version if "Current browser version is " in e.msg: line = e.msg.split("Current browser version is ")[1] @@ -3201,10 +3282,11 @@ def get_local_driver( log_output=os.devnull, service_args=["--disable-build-check"], ) - return webdriver.Chrome( + driver = webdriver.Chrome( service=service, options=chrome_options, ) + return extend_driver(driver) except Exception: pass # Use the virtual display on Linux during headless errors @@ -3219,16 +3301,18 @@ def get_local_driver( log_output=os.devnull, service_args=["--disable-build-check"] ) - return webdriver.Chrome( + driver = webdriver.Chrome( service=service, options=chrome_options ) + return extend_driver(driver) except Exception: try: # Try again if Chrome didn't launch service = ChromeService(service_args=["--disable-build-check"]) - return webdriver.Chrome( + driver = webdriver.Chrome( service=service, options=chrome_options ) + return extend_driver(driver) except Exception: pass if headless: @@ -3245,7 +3329,8 @@ def get_local_driver( log_output=os.devnull, service_args=["--disable-build-check"] ) - return webdriver.Chrome(service=service) + driver = webdriver.Chrome(service=service) + return extend_driver(driver) else: raise Exception( "%s is not a valid browser option for this system!" % browser_name diff --git a/seleniumbase/core/proxy_helper.py b/seleniumbase/core/proxy_helper.py index 29900a35373..875b9e42b70 100644 --- a/seleniumbase/core/proxy_helper.py +++ b/seleniumbase/core/proxy_helper.py @@ -31,6 +31,7 @@ def create_proxy_ext(proxy_string, proxy_user, proxy_pass, zip_it=True): """ rules: {\n""" """ singleProxy: {\n""" """ scheme: "http",\n""" + """ bypassList: [],\n""" """ host: "%s",\n""" """ port: parseInt("%s")\n""" """ },\n""" diff --git a/seleniumbase/core/sb_driver.py b/seleniumbase/core/sb_driver.py new file mode 100644 index 00000000000..32abcb2fbfd --- /dev/null +++ b/seleniumbase/core/sb_driver.py @@ -0,0 +1,87 @@ +"""Add new methods to extend the driver""" +from seleniumbase.fixtures import js_utils +from seleniumbase.fixtures import page_actions + + +class DriverMethods(): + def __init__(self, driver): + self.driver = driver + + def open_url(self, *args, **kwargs): + page_actions.open_url(self.driver, *args, **kwargs) + + def click(self, *args, **kwargs): + page_actions.click(self.driver, *args, **kwargs) + + def send_keys(self, *args, **kwargs): + page_actions.send_keys(self.driver, *args, **kwargs) + + def update_text(self, *args, **kwargs): + page_actions.update_text(self.driver, *args, **kwargs) + + def assert_element_visible(self, *args, **kwargs): + page_actions.assert_element_visible(self.driver, *args, **kwargs) + + def assert_element_present(self, *args, **kwargs): + page_actions.assert_element_present(self.driver, *args, **kwargs) + + def assert_element_not_visible(self, *args, **kwargs): + page_actions.assert_element_not_visible(self.driver, *args, **kwargs) + + def assert_text(self, *args, **kwargs): + page_actions.assert_text(self.driver, *args, **kwargs) + + def assert_exact_text(self, *args, **kwargs): + page_actions.assert_exact_text(self.driver, *args, **kwargs) + + def wait_for_element(self, *args, **kwargs): + return page_actions.wait_for_element(self.driver, *args, **kwargs) + + def wait_for_text(self, *args, **kwargs): + return page_actions.wait_for_text(self.driver, *args, **kwargs) + + def wait_for_exact_text(self, *args, **kwargs): + return page_actions.wait_for_exact_text(self.driver, *args, **kwargs) + + def wait_for_and_accept_alert(self, *args, **kwargs): + return page_actions.wait_for_and_accept_alert( + self.driver, *args, **kwargs + ) + + def wait_for_and_dismiss_alert(self, *args, **kwargs): + return page_actions.wait_for_and_dismiss_alert( + self.driver, *args, **kwargs + ) + + def is_element_present(self, *args, **kwargs): + return page_actions.is_element_present(self.driver, *args, **kwargs) + + def is_element_visible(self, *args, **kwargs): + return page_actions.is_element_visible(self.driver, *args, **kwargs) + + def is_text_visible(self, *args, **kwargs): + return page_actions.is_text_visible(self.driver, *args, **kwargs) + + def is_exact_text_visible(self, *args, **kwargs): + return page_actions.is_exact_text_visible(self.driver, *args, **kwargs) + + def get_text(self, *args, **kwargs): + return page_actions.get_text(self.driver, *args, **kwargs) + + def js_click(self, *args, **kwargs): + return page_actions.js_click(self.driver, *args, **kwargs) + + def get_active_element_css(self, *args, **kwargs): + return js_utils.get_active_element_css(self.driver, *args, **kwargs) + + def get_locale_code(self, *args, **kwargs): + return js_utils.get_locale_code(self.driver, *args, **kwargs) + + def get_origin(self, *args, **kwargs): + return js_utils.get_origin(self.driver, *args, **kwargs) + + def get_user_agent(self, *args, **kwargs): + return js_utils.get_user_agent(self.driver, *args, **kwargs) + + def highlight(self, *args, **kwargs): + js_utils.highlight(self.driver, *args, **kwargs) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 1e3e13fcdb2..5858dcc24bd 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -719,9 +719,12 @@ def double_click(self, selector, by="css selector", timeout=None): self.execute_script(double_click_script) else: double_click_script = ( - """jQuery('%s').dblclick();""" % css_selector + """var targetElement1 = arguments[0]; + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('dblclick', true, true); + targetElement1.dispatchEvent(clickEvent1);""" ) - self.safe_execute_script(double_click_script) + self.execute_script(double_click_script, element) if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() else: @@ -797,9 +800,12 @@ def context_click(self, selector, by="css selector", timeout=None): self.execute_script(right_click_script) else: right_click_script = ( - """jQuery('%s').contextmenu();""" % css_selector + """var targetElement1 = arguments[0]; + var clickEvent1 = document.createEvent('MouseEvents'); + clickEvent1.initEvent('contextmenu', true, true); + targetElement1.dispatchEvent(clickEvent1);""" ) - self.safe_execute_script(right_click_script) + self.execute_script(right_click_script, element) if settings.WAIT_FOR_RSC_ON_CLICKS: self.wait_for_ready_state_complete() else: @@ -1971,25 +1977,28 @@ def get_property_value( timeout = self.__get_new_timeout(timeout) selector, by = self.__recalculate_selector(selector, by) self.wait_for_ready_state_complete() - page_actions.wait_for_element_present( + element = page_actions.wait_for_element_present( self.driver, selector, by, timeout ) try: selector = self.convert_to_css_selector(selector, by=by) except Exception: - # Don't run action if can't convert to CSS_Selector for JavaScript - raise Exception( - "Exception: Could not convert {%s}(by=%s) to CSS_SELECTOR!" - % (selector, by) + # If can't convert to CSS_Selector for JS, use element directly + script = ( + """var $elm = arguments[0]; + $val = window.getComputedStyle($elm).getPropertyValue('%s'); + return $val;""" % property ) + value = self.execute_script(script, element) + if value is not None: + return value + else: + return "" selector = re.escape(selector) selector = self.__escape_quotes_if_needed(selector) script = """var $elm = document.querySelector('%s'); $val = window.getComputedStyle($elm).getPropertyValue('%s'); - return $val;""" % ( - selector, - property, - ) + return $val;""" % (selector, property) value = self.execute_script(script) if value is not None: return value @@ -2508,6 +2517,7 @@ def hover_and_click( hover_by="css selector", click_by="css selector", timeout=None, + js_click=False, ): """When you want to hover over an element or dropdown menu, and then click an element that appears after that.""" @@ -2578,6 +2588,7 @@ def hover_and_click( hover_by, click_by, timeout, + js_click, ) latest_window_count = len(self.driver.window_handles) if ( @@ -2608,6 +2619,23 @@ def hover_and_click( self.__slow_mode_pause_if_active() return element + def hover_and_js_click( + self, + hover_selector, + click_selector, + hover_by="css selector", + click_by="css selector", + timeout=None, + ): + self.hover_and_click( + hover_selector=hover_selector, + click_selector=click_selector, + hover_by=hover_by, + click_by=click_by, + timeout=timeout, + js_click=True, + ) + def hover_and_double_click( self, hover_selector, @@ -5331,13 +5359,15 @@ def bring_to_front(self, selector, by="css selector"): Other element would receive the click: ... }""" self.__check_scope() selector, by = self.__recalculate_selector(selector, by) - self.wait_for_element_visible( + element = self.wait_for_element_visible( selector, by=by, timeout=settings.SMALL_TIMEOUT ) try: selector = self.convert_to_css_selector(selector, by=by) except Exception: - # Don't run action if can't convert to CSS_Selector for JavaScript + # If can't convert to CSS_Selector for JS, use element directly + script = ("""arguments[0].style.zIndex = '999999';""") + self.execute_script(script, element) return selector = re.escape(selector) selector = self.__escape_quotes_if_needed(selector) @@ -5406,11 +5436,12 @@ def __highlight( selector, by=by, timeout=settings.SMALL_TIMEOUT ) self.__slow_scroll_to_element(element) + use_element_directly = False try: selector = self.convert_to_css_selector(selector, by=by) except Exception: - # Don't highlight if can't convert to CSS_SELECTOR - return + # If can't convert to CSS_Selector for JS, use element directly + use_element_directly = True if self.highlights: loops = self.highlights if self.browser == "ie": @@ -5436,7 +5467,9 @@ def __highlight( box_end = style.find(";", box_start) + 1 original_box_shadow = style[box_start:box_end] o_bs = original_box_shadow - if ":contains" not in selector and ":first" not in selector: + if use_element_directly: + self.__highlight_element_with_js(element, loops, o_bs) + elif ":contains" not in selector and ":first" not in selector: selector = re.escape(selector) selector = self.__escape_quotes_if_needed(selector) self.__highlight_with_js(selector, loops, o_bs) @@ -5746,9 +5779,8 @@ def js_click( if ":contains\\(" not in css_selector: self.__js_click(selector, by=by) else: - click_script = """jQuery('%s')[0].click();""" % css_selector try: - self.safe_execute_script(click_script) + self.__js_click_element(element) except Exception: self.wait_for_ready_state_complete() element = self.wait_for_element_present( @@ -5758,7 +5790,7 @@ def js_click( if self.is_element_clickable(selector): self.__element_click(element) else: - self.safe_execute_script(click_script) + self.__js_click_element(element) else: if ":contains\\(" not in css_selector: self.__js_click_all(selector, by=by) @@ -5910,14 +5942,23 @@ def jquery_click_all(self, selector, by="css selector", timeout=None): def hide_element(self, selector, by="css selector"): """Hide the first element on the page that matches the selector.""" self.__check_scope() + element = None try: self.wait_for_element_visible("body", timeout=1.5) - self.wait_for_element_present(selector, by=by, timeout=0.5) + element = self.wait_for_element_present( + selector, by=by, timeout=0.5 + ) except Exception: pass selector, by = self.__recalculate_selector(selector, by) css_selector = self.convert_to_css_selector(selector, by=by) - if ":contains(" in css_selector: + if ":contains(" in css_selector and element: + script = ( + 'const e = arguments[0];' + 'e.style.display="none";e.style.visibility="hidden";' + ) + self.execute_script(script, element) + elif ":contains(" in css_selector and not element: selector = self.__make_css_match_first_element_only(css_selector) script = """jQuery('%s').hide();""" % selector self.safe_execute_script(script) @@ -5927,7 +5968,8 @@ def hide_element(self, selector, by="css selector"): script = ( 'const e = document.querySelector("%s");' 'e.style.display="none";e.style.visibility="hidden";' - % css_selector) + % css_selector + ) self.execute_script(script) def hide_elements(self, selector, by="css selector"): @@ -5958,14 +6000,21 @@ def hide_elements(self, selector, by="css selector"): def show_element(self, selector, by="css selector"): """Show the first element on the page that matches the selector.""" self.__check_scope() + element = None try: self.wait_for_element_visible("body", timeout=1.5) - self.wait_for_element_present(selector, by=by, timeout=1) + element = self.wait_for_element_present(selector, by=by, timeout=1) except Exception: pass selector, by = self.__recalculate_selector(selector, by) css_selector = self.convert_to_css_selector(selector, by=by) - if ":contains(" in css_selector: + if ":contains(" in css_selector and element: + script = ( + 'const e = arguments[0];' + 'e.style.display="";e.style.visibility="visible";' + ) + self.execute_script(script, element) + elif ":contains(" in css_selector and not element: selector = self.__make_css_match_first_element_only(css_selector) script = """jQuery('%s').show(0);""" % selector self.safe_execute_script(script) @@ -6007,14 +6056,23 @@ def show_elements(self, selector, by="css selector"): def remove_element(self, selector, by="css selector"): """Remove the first element on the page that matches the selector.""" self.__check_scope() + element = None try: self.wait_for_element_visible("body", timeout=1.5) - self.wait_for_element_present(selector, by=by, timeout=0.5) + element = self.wait_for_element_present( + selector, by=by, timeout=0.5 + ) except Exception: pass selector, by = self.__recalculate_selector(selector, by) css_selector = self.convert_to_css_selector(selector, by=by) - if ":contains(" in css_selector: + if ":contains(" in css_selector and element: + script = ( + 'const e = arguments[0];' + 'e.parentElement.removeChild(e);' + ) + self.execute_script(script, element) + elif ":contains(" in css_selector and not element: selector = self.__make_css_match_first_element_only(css_selector) script = """jQuery('%s').remove();""" % selector self.safe_execute_script(script) @@ -6129,6 +6187,9 @@ def get_domain_url(self, url): self.__check_scope() return page_utils.get_domain_url(url) + def get_active_element_css(self): + return js_utils.get_active_element_css(self.driver) + def get_beautiful_soup(self, source=None): """BeautifulSoup is a toolkit for dissecting an HTML document and extracting what you need. It's great for screen-scraping! @@ -7525,8 +7586,11 @@ def set_value( ) self.execute_script(script) else: - script = """jQuery('%s')[0].value='%s';""" % (css_selector, value) - self.safe_execute_script(script) + element = self.wait_for_element_present( + original_selector, by=by, timeout=timeout + ) + script = """arguments[0].value='%s';""" % value + self.execute_script(script, element) if text.endswith("\n"): element = self.wait_for_element_present( original_selector, by=by, timeout=timeout @@ -7546,6 +7610,19 @@ def set_value( self.execute_script(mouse_move_script) except Exception: pass + elif the_type == "range" and ":contains\\(" in css_selector: + try: + element = self.wait_for_element_present( + original_selector, by=by, timeout=1 + ) + mouse_move_script = ( + """m_elm = arguments[0];""" + """m_evt = new Event('mousemove');""" + """m_elm.dispatchEvent(m_evt);""" + ) + self.execute_script(mouse_move_script, element) + except Exception: + pass self.__demo_mode_pause_if_active() def js_update_text(self, selector, text, by="css selector", timeout=None): @@ -7642,11 +7719,8 @@ def set_text_content( ) self.execute_script(script) else: - script = """jQuery('%s')[0].textContent='%s';""" % ( - css_selector, - value, - ) - self.safe_execute_script(script) + script = """arguments[0].textContent='%s';""" % value + self.execute_script(script, element) self.__demo_mode_pause_if_active() def jquery_update_text( @@ -7737,8 +7811,9 @@ def get_value(self, selector, by="css selector", timeout=None): ) value = self.execute_script(script) else: - script = """return jQuery('%s')[0].value;""" % css_selector - value = self.safe_execute_script(script) + element = self.wait_for_element_present(selector, by=by, timeout=1) + script = """return arguments[0].value;""" + value = self.execute_script(script, element) return value def set_time_limit(self, time_limit): @@ -12514,6 +12589,40 @@ def __js_click(self, selector, by="css selector"): ) self.execute_script(script) + def __js_click_element(self, element): + """Clicks an element using pure JS. Does not use jQuery.""" + is_visible = element.is_displayed() + current_url = self.get_current_url() + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var someLink = arguments[0]; + simulateClick(someLink);""" + ) + if hasattr(self, "recorder_mode") and self.recorder_mode: + self.save_recorded_actions() + try: + self.execute_script(script, element) + except Exception: + # If element was visible but no longer, or on a different page now, + # assume that the click actually worked and continue with the test. + if ( + (is_visible and not element.is_displayed()) + or current_url != self.get_current_url() + ): + return # The click worked, but threw an Exception. Keep going. + # It appears the first click didn't work. Make another attempt. + self.wait_for_ready_state_complete() + # If the regular mouse-simulated click fails, do a basic JS click + script = ("""arguments[0].click();""") + self.execute_script(script, element) + def __js_click_all(self, selector, by="css selector"): """Clicks all matching elements using pure JS. (No jQuery)""" selector, by = self.__recalculate_selector(selector, by) @@ -12851,53 +12960,12 @@ def __click_dropdown_partial_link_text(self, link_text, link_css): def __recalculate_selector(self, selector, by, xp_ok=True): """Use autodetection to return the correct selector with "by" updated. If "xp_ok" is False, don't call convert_css_to_xpath(), which is - used to make the ":contains()" selector valid outside of JS calls.""" - _type = type(selector) # First make sure the selector is a string - not_string = False - if _type is not str: - not_string = True - if not_string: - msg = "Expecting a selector of type: \"\" (string)!" - raise Exception('Invalid selector type: "%s"\n%s' % (_type, msg)) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - if page_utils.is_link_text_selector(selector): - selector = page_utils.get_link_text_from_selector(selector) - by = By.LINK_TEXT - if page_utils.is_partial_link_text_selector(selector): - selector = page_utils.get_partial_link_text_from_selector(selector) - by = By.PARTIAL_LINK_TEXT - if page_utils.is_name_selector(selector): - name = page_utils.get_name_from_selector(selector) - selector = '[name="%s"]' % name - by = By.CSS_SELECTOR - if xp_ok: - if ":contains(" in selector and by == By.CSS_SELECTOR: - selector = self.convert_css_to_xpath(selector) - by = By.XPATH - return (selector, by) + used to make the ":contains()" selector valid outside of JS calls. + Returns a (selector, by) tuple.""" + return page_utils.recalculate_selector(selector, by, xp_ok=xp_ok) def __looks_like_a_page_url(self, url): - """Returns True if the url parameter looks like a URL. This method - is slightly more lenient than page_utils.is_valid_url(url) due to - possible typos when calling self.get(url), which will try to - navigate to the page if a URL is detected, but will instead call - self.get_element(URL_AS_A_SELECTOR) if the input in not a URL.""" - if ( - url.startswith("http:") - or url.startswith("https:") - or url.startswith("://") - or url.startswith("about:") - or url.startswith("blob:") - or url.startswith("chrome:") - or url.startswith("data:") - or url.startswith("edge:") - or url.startswith("file:") - or url.startswith("view-source:") - ): - return True - else: - return False + return page_utils.looks_like_a_page_url(url) def __make_css_match_first_element_only(self, selector): # Only get the first match @@ -12996,6 +13064,10 @@ def __highlight_with_js(self, selector, loops, o_bs): self.wait_for_ready_state_complete() js_utils.highlight_with_js(self.driver, selector, loops, o_bs) + def __highlight_element_with_js(self, element, loops, o_bs): + self.wait_for_ready_state_complete() + js_utils.highlight_element_with_js(self.driver, element, loops, o_bs) + def __highlight_with_jquery(self, selector, loops, o_bs): self.wait_for_ready_state_complete() js_utils.highlight_with_jquery(self.driver, selector, loops, o_bs) @@ -13013,6 +13085,19 @@ def __highlight_with_js_2(self, message, selector, o_bs): self.driver, message, selector, o_bs, duration ) + def __highlight_element_with_js_2(self, message, element, o_bs): + duration = self.message_duration + if not duration: + duration = settings.DEFAULT_MESSAGE_DURATION + if ( + (self.headless or self.headless2 or self.xvfb) + and float(duration) > 0.75 + ): + duration = 0.75 + js_utils.highlight_element_with_js_2( + self.driver, message, element, o_bs, duration + ) + def __highlight_with_jquery_2(self, message, selector, o_bs): duration = self.message_duration if not duration: @@ -13051,11 +13136,12 @@ def __highlight_with_assert_success( selector, by=by, timeout=settings.SMALL_TIMEOUT ) self.__slow_scroll_to_element(element) + use_element_directly = False try: selector = self.convert_to_css_selector(selector, by=by) except Exception: - # Don't highlight if can't convert to CSS_SELECTOR - return + # If can't convert to CSS_Selector for JS, use element directly + use_element_directly = True o_bs = "" # original_box_shadow try: @@ -13074,7 +13160,9 @@ def __highlight_with_assert_success( original_box_shadow = style[box_start:box_end] o_bs = original_box_shadow - if ":contains" not in selector and ":first" not in selector: + if use_element_directly: + self.__highlight_element_with_js_2(message, element, o_bs) + elif ":contains" not in selector and ":first" not in selector: selector = re.escape(selector) selector = self.__escape_quotes_if_needed(selector) self.__highlight_with_js_2(message, selector, o_bs) @@ -13452,7 +13540,7 @@ def __wait_for_non_empty_shadow_text_visible(self, selector, timeout): actual_text = self.__get_shadow_text(selector, timeout=1) actual_text = actual_text.strip() if len(actual_text) == 0: - msg = "Element {%s} has no visible text!" % (selector) + msg = "Element {%s} has no visible text!" % selector page_actions.timeout_exception( "TextNotVisibleException", msg ) @@ -13465,7 +13553,7 @@ def __wait_for_non_empty_shadow_text_visible(self, selector, timeout): actual_text = self.__get_shadow_text(selector, timeout=1) actual_text = actual_text.strip() if len(actual_text) == 0: - msg = "Element {%s} has no visible text!" % (selector) + msg = "Element {%s} has no visible text!" % selector page_actions.timeout_exception("TextNotVisibleException", msg) return True diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index c2ae3a217d3..3cf7fbd7e9f 100644 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -7,6 +7,7 @@ from seleniumbase import config as sb_config from seleniumbase.config import settings from seleniumbase.fixtures import constants +from seleniumbase.fixtures import css_to_xpath def wait_for_ready_state_complete(driver, timeout=settings.LARGE_TIMEOUT): @@ -311,6 +312,41 @@ def wait_for_css_query_selector( ) +def is_valid_by(by): + return by in [ + "css selector", "class name", "id", "name", + "link text", "xpath", "tag name", "partial link text", + ] + + +def swap_selector_and_by_if_reversed(selector, by): + if not is_valid_by(by) and is_valid_by(selector): + selector, by = by, selector + return (selector, by) + + +def highlight(driver, selector, by="css selector", loops=4): + """For driver.highlight() / driver.page.highlight()""" + swap_selector_and_by_if_reversed(selector, by) + if ":contains(" in selector: + by = "xpath" + selector = css_to_xpath.convert_css_to_xpath(selector) + element = None + try: + element = driver.find_element(by, selector) + except Exception: + time.sleep(1) + element = driver.find_element(by, selector) + o_bs = "" # original_box_shadow + style = element.get_attribute("style") + if style and "box-shadow: " in style: + box_start = style.find("box-shadow: ") + box_end = style.find(";", box_start) + 1 + original_box_shadow = style[box_start:box_end] + o_bs = original_box_shadow + highlight_element_with_js(driver, element, loops=loops, o_bs=o_bs) + + def highlight_with_js(driver, selector, loops=4, o_bs=""): try: # This closes any pop-up alerts @@ -411,6 +447,82 @@ def highlight_with_js(driver, selector, loops=4, o_bs=""): return +def highlight_element_with_js(driver, element, loops=4, o_bs=""): + try: + # This closes any pop-up alerts + driver.execute_script("") + except Exception: + pass + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + for n in range(loops): + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(255, 0, 0, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(0, 0, 255, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(0, 255, 0, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 0, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = """arguments[0].style.boxShadow = '%s';""" % (o_bs) + try: + driver.execute_script(script, element) + except Exception: + return + + def highlight_with_jquery(driver, selector, loops=4, o_bs=""): try: # This closes any pop-up alerts @@ -892,13 +1004,11 @@ def highlight_with_js_2(driver, message, selector, o_bs, msg_dur): except Exception: return time.sleep(0.0181) - try: activate_jquery(driver) post_messenger_success_message(driver, message, msg_dur) except Exception: pass - script = """document.querySelector('%s').style.boxShadow = '%s';""" % ( selector, o_bs, @@ -909,6 +1019,69 @@ def highlight_with_js_2(driver, message, selector, o_bs, msg_dur): return +def highlight_element_with_js_2(driver, message, element, o_bs, msg_dur): + try: + # This closes any pop-up alerts + driver.execute_script("") + except Exception: + pass + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(205, 30, 0, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(128, 0, 128, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(50, 50, 128, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + script = ( + """arguments[0].style.boxShadow = + '0px 0px 6px 6px rgba(50, 205, 50, 1)';""" + ) + try: + driver.execute_script(script, element) + except Exception: + return + time.sleep(0.0181) + try: + activate_jquery(driver) + post_messenger_success_message(driver, message, msg_dur) + except Exception: + pass + script = """arguments[0].style.boxShadow = '%s';""" % (o_bs) + try: + driver.execute_script(script, element) + except Exception: + return + + def highlight_with_jquery_2(driver, message, selector, o_bs, msg_dur): if selector == "html": selector = "body" @@ -998,6 +1171,19 @@ def get_active_element_css(driver): return driver.execute_script(active_css_js.get_active_element_css) +def get_locale_code(driver): + script = "return navigator.language || navigator.languages[0];" + return driver.execute_script(script) + + +def get_origin(driver): + return driver.execute_script("return window.location.origin;") + + +def get_user_agent(driver): + return driver.execute_script("return navigator.userAgent;") + + def get_scroll_distance_to_element(driver, element): try: scroll_position = driver.execute_script("return window.scrollY;") diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index ee395e2c18f..02e6d65ceed 100644 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -31,6 +31,7 @@ from seleniumbase.common.exceptions import LinkTextNotFoundException from seleniumbase.common.exceptions import TextNotVisibleException from seleniumbase.config import settings +from seleniumbase.fixtures import page_utils from seleniumbase.fixtures import shared_utils @@ -44,6 +45,7 @@ def is_element_present(driver, selector, by="css selector"): @Returns Boolean (is element present) """ + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) try: driver.find_element(by=by, value=selector) return True @@ -61,6 +63,7 @@ def is_element_visible(driver, selector, by="css selector"): @Returns Boolean (is element visible) """ + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) try: element = driver.find_element(by=by, value=selector) return element.is_displayed() @@ -115,6 +118,7 @@ def is_text_visible(driver, text, selector, by="css selector", browser=None): @Returns Boolean (is text visible) """ + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) text = str(text) try: element = driver.find_element(by=by, value=selector) @@ -149,6 +153,7 @@ def is_exact_text_visible( @Returns Boolean (is text visible) """ + selector, by = page_utils.swap_selector_and_by_if_reversed(selector, by) text = str(text) try: element = driver.find_element(by=by, value=selector) @@ -265,6 +270,7 @@ def hover_and_click( hover_by="css selector", click_by="css selector", timeout=settings.SMALL_TIMEOUT, + js_click=False, ): """ Fires the hover event for a specified element by a given selector, then @@ -276,6 +282,7 @@ def hover_and_click( hover_by - the hover selector type to search by (Default: "css selector") click_by - the click selector type to search by (Default: "css selector") timeout - number of seconds to wait for click element to appear after hover + js_click - the option to use js_click() instead of click() on the last part """ start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) @@ -285,7 +292,10 @@ def hover_and_click( try: hover.perform() element = driver.find_element(by=click_by, value=click_selector) - element.click() + if js_click: + driver.execute_script("arguments[0].click();", element) + else: + element.click() return element except Exception: now_ms = time.time() * 1000.0 @@ -1391,8 +1401,6 @@ def switch_to_frame(driver, frame, timeout=settings.SMALL_TIMEOUT): frame - the frame element, name, id, index, or selector timeout - the time to wait for the alert in seconds """ - from seleniumbase.fixtures import page_utils - start_ms = time.time() * 1000.0 stop_ms = start_ms + (timeout * 1000.0) for x in range(int(timeout * 10)): @@ -1497,7 +1505,153 @@ def switch_to_window(driver, window, timeout=settings.SMALL_TIMEOUT): ############ -# Duplicates for easier use without BaseCase +# Support methods for direct use from driver + +def open_url(driver, url): + url = str(url).strip() # Remove leading and trailing whitespace + if not page_utils.looks_like_a_page_url(url): + if page_utils.is_valid_url("https://" + url): + url = "https://" + url + driver.get(url) + + +def click(driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + element.click() + + +def js_click( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_present( + driver, selector, by=by, timeout=timeout + ) + if not element.is_displayed() or not element.is_enabled(): + time.sleep(0.2) # If not clickable, wait a bit longer before clicking + element = wait_for_element_present(driver, selector, by=by, timeout=1) + script = ( + """var simulateClick = function (elem) { + var evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + var canceled = !elem.dispatchEvent(evt); + }; + var someLink = arguments[0]; + simulateClick(someLink);""" + ) + driver.execute_script(script, element) + + +def send_keys( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + element.submit() + + +def update_text( + driver, selector, text, by="css selector", timeout=settings.LARGE_TIMEOUT +): + selector, by = page_utils.recalculate_selector(selector, by) + element = wait_for_element_clickable( + driver, selector, by=by, timeout=timeout + ) + element.clear() + if not text.endswith("\n"): + element.send_keys(text) + else: + element.send_keys(text[:-1]) + element.submit() + + +def assert_element_visible( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_element_visible( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_element_present( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_element_present( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_element_not_visible( + driver, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) + wait_for_element_not_visible( + driver, + selector, + by=by, + timeout=timeout, + original_selector=original_selector, + ) + + +def assert_text( + driver, + text, + selector="html", + by="css selector", + timeout=settings.SMALL_TIMEOUT, +): + browser = driver.capabilities["browserName"].lower() + wait_for_text_visible( + driver, text, selector, by=by, timeout=timeout, browser=browser + ) + + +def assert_exact_text( + driver, text, selector, by="css selector", timeout=settings.SMALL_TIMEOUT +): + browser = driver.capabilities["browserName"].lower() + wait_for_exact_text_visible( + driver, text, selector, by=by, timeout=timeout, browser=browser + ) + def wait_for_element( driver, @@ -1505,11 +1659,18 @@ def wait_for_element( by="css selector", timeout=settings.LARGE_TIMEOUT, ): + original_selector = None + if page_utils.is_valid_by(by): + original_selector = selector + elif page_utils.is_valid_by(selector): + original_selector = by + selector, by = page_utils.recalculate_selector(selector, by) return wait_for_element_visible( driver=driver, selector=selector, by=by, timeout=timeout, + original_selector=original_selector, ) @@ -1536,6 +1697,29 @@ def wait_for_text( ) +def wait_for_exact_text( + driver, + text, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT, +): + browser = None # Only used for covering a Safari edge case + try: + if "safari:platformVersion" in driver.capabilities: + browser = "safari" + except Exception: + pass + return wait_for_exact_text_visible( + driver=driver, + text=text, + selector=selector, + by=by, + timeout=timeout, + browser=browser, + ) + + def wait_for_non_empty_text( driver, selector, @@ -1548,3 +1732,32 @@ def wait_for_non_empty_text( by=by, timeout=timeout, ) + + +def get_text( + driver, + selector, + by="css selector", + timeout=settings.LARGE_TIMEOUT +): + browser = None # Only used for covering a Safari edge case + try: + if "safari:platformVersion" in driver.capabilities: + browser = "safari" + except Exception: + pass + element = wait_for_element( + driver=driver, + selector=selector, + by=by, + timeout=timeout, + ) + element_text = element.text + if browser == "safari": + if element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_attribute("value") + else: + element_text = element.get_attribute("innerText") + elif element.tag_name.lower() in ["input", "textarea"]: + element_text = element.get_property("value") + return element_text diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index 29ca5254d4d..0503564a361 100644 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -4,7 +4,9 @@ import os import re import requests +from selenium.webdriver.common.by import By from seleniumbase.fixtures import constants +from seleniumbase.fixtures import css_to_xpath def get_domain_url(url): @@ -23,6 +25,19 @@ def get_domain_url(url): return domain_url +def is_valid_by(by): + return by in [ + "css selector", "class name", "id", "name", + "link text", "xpath", "tag name", "partial link text", + ] + + +def swap_selector_and_by_if_reversed(selector, by): + if not is_valid_by(by) and is_valid_by(selector): + selector, by = by, selector + return (selector, by) + + def is_xpath_selector(selector): """Determine if a selector is an xpath selector.""" if ( @@ -66,6 +81,65 @@ def is_name_selector(selector): return False +def recalculate_selector(selector, by, xp_ok=True): + """Use autodetection to return the correct selector with "by" updated. + If "xp_ok" is False, don't call convert_css_to_xpath(), which is + used to make the ":contains()" selector valid outside of JS calls. + Returns a (selector, by) tuple.""" + _type = type(selector) + if _type is not str: + msg = "Expecting a selector of type: \"\" (string)!" + raise Exception('Invalid selector type: "%s"\n%s' % (_type, msg)) + _by_type = type(by) + if _by_type is not str: + msg = "Expecting a `by` of type: \"\" (string)!" + raise Exception('Invalid `by` type: "%s"\n%s' % (_by_type, msg)) + if not is_valid_by(by) and is_valid_by(selector): + selector, by = swap_selector_and_by_if_reversed(selector, by) + if is_xpath_selector(selector): + by = By.XPATH + if is_link_text_selector(selector): + selector = get_link_text_from_selector(selector) + by = By.LINK_TEXT + if is_partial_link_text_selector(selector): + selector = get_partial_link_text_from_selector(selector) + by = By.PARTIAL_LINK_TEXT + if is_name_selector(selector): + name = get_name_from_selector(selector) + selector = '[name="%s"]' % name + by = By.CSS_SELECTOR + if xp_ok: + if ":contains(" in selector and by == By.CSS_SELECTOR: + selector = css_to_xpath.convert_css_to_xpath(selector) + by = By.XPATH + if by == "": + by = By.CSS_SELECTOR + return (selector, by) + + +def looks_like_a_page_url(url): + """Returns True if the url parameter looks like a URL. This method + is slightly more lenient than page_utils.is_valid_url(url) due to + possible typos when calling self.get(url), which will try to + navigate to the page if a URL is detected, but will instead call + self.get_element(URL_AS_A_SELECTOR) if the input is not a URL.""" + if ( + url.startswith("http:") + or url.startswith("https:") + or url.startswith("://") + or url.startswith("about:") + or url.startswith("blob:") + or url.startswith("chrome:") + or url.startswith("data:") + or url.startswith("edge:") + or url.startswith("file:") + or url.startswith("view-source:") + ): + return True + else: + return False + + def get_link_text_from_selector(selector): """Get the link text from a link text selector.""" if selector.startswith("link="): diff --git a/seleniumbase/js_code/active_css_js.py b/seleniumbase/js_code/active_css_js.py index c528d297fb0..8c76583af17 100644 --- a/seleniumbase/js_code/active_css_js.py +++ b/seleniumbase/js_code/active_css_js.py @@ -10,8 +10,8 @@ var selector = el.nodeName.toLowerCase(); if (el.id) { elid = el.id; - if (elid.includes(',') || elid.includes('.') || - elid.includes('(') || elid.includes(')') || hasNumber(elid[0])) + if (elid.includes(',') || elid.includes('.') || /\s/.test(elid) || + elid.includes('(') || elid.includes(')') || hasDigit(elid[0])) return cssPathByAttribute(el, 'id'); selector += '#' + elid; path.unshift(selector); @@ -87,8 +87,6 @@ return path.join(' > '); }; var ssOccurrences = function(string, subString, allowOverlapping) { - string += ''; - subString += ''; if (subString.length <= 0) return (string.length + 1); var n = 0; @@ -101,9 +99,12 @@ } return n; }; -function hasNumber(str) { +function hasDigit(str) { return /\d/.test(str); }; +function isGen(str) { + return /[_-]\d/.test(str); +}; function tagName(el) { return el.tagName.toLowerCase(); }; @@ -121,10 +122,9 @@ var getBestSelector = function(el) { if (!(el instanceof Element)) return; el = turnIntoParentAsNeeded(el); - child_sep = ' > '; - selector_by_id = cssPathById(el); - if (!selector_by_id.includes(child_sep)) return selector_by_id; - child_count_by_id = ssOccurrences(selector_by_id, child_sep); + sel_by_id = cssPathById(el); + if (!sel_by_id.includes(' > ') && !isGen(sel_by_id)) return sel_by_id; + child_count_by_id = ssOccurrences(sel_by_id, ' > '); selector_by_class = cssPathByClass(el); tag_name = tagName(el); non_id_attributes = []; @@ -145,7 +145,6 @@ non_id_attributes.push('data-cy'); non_id_attributes.push('data-action'); non_id_attributes.push('data-target'); - non_id_attributes.push('data-content'); non_id_attributes.push('data-tooltip'); non_id_attributes.push('alt'); non_id_attributes.push('title'); @@ -157,6 +156,7 @@ non_id_attributes.push('ng-href'); non_id_attributes.push('href'); non_id_attributes.push('label'); + non_id_attributes.push('data-content'); non_id_attributes.push('class'); non_id_attributes.push('for'); non_id_attributes.push('placeholder'); @@ -175,15 +175,15 @@ else selector_by_attr[i] = cssPathByAttribute(el, n_i_attr); all_by_attr[i] = document.querySelectorAll(selector_by_attr[i]); num_by_attr[i] = all_by_attr[i].length; - if (!selector_by_attr[i].includes(child_sep) && + if (!selector_by_attr[i].includes(' > ') && ((num_by_attr[i] == 1) || (el == all_by_attr[i][0]))) { if (n_i_attr == 'aria-label' || n_i_attr == 'for') - if (hasNumber(selector_by_attr[i])) + if (hasDigit(selector_by_attr[i])) continue; return selector_by_attr[i]; } - child_count_by_attr[i] = ssOccurrences(selector_by_attr[i], child_sep); + child_count_by_attr[i] = ssOccurrences(selector_by_attr[i], ' > '); } basic_tags = []; basic_tags.push('h1'); @@ -201,23 +201,24 @@ contains_tags = []; contains_tags.push('a'); contains_tags.push('b'); - contains_tags.push('i'); contains_tags.push('h1'); contains_tags.push('h2'); contains_tags.push('h3'); contains_tags.push('h4'); contains_tags.push('h5'); - contains_tags.push('li'); - contains_tags.push('td'); - contains_tags.push('th'); contains_tags.push('code'); contains_tags.push('mark'); - contains_tags.push('label'); - contains_tags.push('small'); contains_tags.push('button'); + contains_tags.push('label'); contains_tags.push('legend'); + contains_tags.push('li'); + contains_tags.push('td'); + contains_tags.push('th'); + contains_tags.push('i'); + contains_tags.push('small'); contains_tags.push('strong'); contains_tags.push('summary'); + contains_tags.push('span'); all_by_tag = []; text_content = ''; if (el.textContent) @@ -239,9 +240,9 @@ } } } - best_selector = selector_by_id; + best_selector = sel_by_id; lowest_child_count = child_count_by_id; - child_count_by_class = ssOccurrences(selector_by_class, child_sep); + child_count_by_class = ssOccurrences(selector_by_class, ' > '); if (child_count_by_class < lowest_child_count) { best_selector = selector_by_class; lowest_child_count = child_count_by_class; diff --git a/setup.py b/setup.py index a45045bf5c1..b869e682fb2 100755 --- a/setup.py +++ b/setup.py @@ -134,7 +134,7 @@ 'pip>=23.2.1', 'packaging>=23.1', 'setuptools>=68.0.0;python_version<"3.8"', - 'setuptools>=68.1.2;python_version>="3.8"', + 'setuptools>=68.2.0;python_version>="3.8"', 'wheel>=0.41.2', 'attrs>=23.1.0', "certifi>=2023.7.22", @@ -155,7 +155,7 @@ 'h11==0.14.0', 'outcome==1.2.0', 'trio==0.22.2', - 'trio-websocket==0.10.3', + 'trio-websocket==0.10.4', 'wsproto==1.2.0', 'selenium==4.11.2', 'cssselect==1.2.0', @@ -166,7 +166,7 @@ 'pluggy==1.2.0;python_version<"3.8"', 'pluggy==1.3.0;python_version>="3.8"', "py==1.11.0", - 'pytest==7.4.0', + 'pytest==7.4.2', "pytest-html==2.0.1", # Newer ones had issues 'pytest-metadata==3.0.0', "pytest-ordering==0.6", @@ -175,12 +175,13 @@ 'parameterized==0.9.0', "sbvirtualdisplay==1.2.0", "behave==1.2.6", - 'soupsieve==2.4.1', + 'soupsieve==2.4.1;python_version<"3.8"', + 'soupsieve==2.5;python_version>="3.8"', "beautifulsoup4==4.12.2", 'pygments==2.16.1', 'pyreadline3==3.4.1;platform_system=="Windows"', - "tabcompleter==1.2.1", - "pdbp==1.4.6", + "tabcompleter==1.3.0", + "pdbp==1.5.0", 'colorama==0.4.6', 'exceptiongroup==1.1.3', 'importlib-metadata==4.2.0;python_version<"3.8"', @@ -203,7 +204,7 @@ # Usage: coverage run -m pytest; coverage html; coverage report "coverage": [ 'coverage==7.2.7;python_version<"3.8"', - 'coverage==7.3.0;python_version>="3.8"', + 'coverage==7.3.1;python_version>="3.8"', 'pytest-cov==4.1.0', ], # pip install -e .[flake8]