diff --git a/examples/cdp_mode/raw_radwell.py b/examples/cdp_mode/raw_radwell.py new file mode 100644 index 00000000000..34a674ce1ad --- /dev/null +++ b/examples/cdp_mode/raw_radwell.py @@ -0,0 +1,13 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale_code="en", incognito=True) as sb: + url = "https://www.radwell.com/en-US/Search/Advanced/" + sb.activate_cdp_mode(url) + sb.sleep(3) + sb.cdp.press_keys("form#basicsearch input", "821C-PM-111DA-142") + sb.sleep(1) + sb.cdp.click('[value="Search Icon"]') + sb.sleep(3) + sb.cdp.assert_text("MAC VALVES INC", "a.manufacturer-link") + sb.cdp.highlight("a.manufacturer-link") + sb.sleep(1) diff --git a/examples/cdp_mode/raw_tiktok.py b/examples/cdp_mode/raw_tiktok.py new file mode 100644 index 00000000000..360a90ad8d8 --- /dev/null +++ b/examples/cdp_mode/raw_tiktok.py @@ -0,0 +1,15 @@ +from seleniumbase import SB + +with SB( + uc=True, test=True, locale_code="en", incognito=True, ad_block=True +) as sb: + url = "https://www.tiktok.com/@startrek?lang=en" + sb.activate_cdp_mode(url) + sb.sleep(2.5) + sb.cdp.click('button:contains("Refresh")') + sb.sleep(1.5) + print(sb.cdp.get_text('h2[data-e2e="user-bio"]')) + for i in range(55): + sb.cdp.scroll_down(12) + sb.sleep(0.06) + sb.sleep(1) diff --git a/examples/cdp_mode/raw_united.py b/examples/cdp_mode/raw_united.py new file mode 100644 index 00000000000..e819180cd4c --- /dev/null +++ b/examples/cdp_mode/raw_united.py @@ -0,0 +1,31 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, locale_code="en", ad_block=True) as sb: + url = "https://www.united.com/en/us" + sb.activate_cdp_mode(url) + sb.sleep(2.5) + origin_input = 'input[placeholder="Origin"]' + origin = "Boston, MA" + destination_input = 'input[placeholder="Destination"]' + destination = "San Diego, CA" + sb.cdp.gui_click_element(origin_input) + sb.sleep(1.2) + sb.cdp.type(origin_input, origin) + sb.sleep(1.2) + sb.cdp.click('strong:contains("%s")' % origin) + sb.sleep(1.2) + sb.cdp.gui_click_element(destination_input) + sb.sleep(1.2) + sb.cdp.type(destination_input, destination) + sb.sleep(1.2) + sb.cdp.click('strong:contains("%s")' % destination) + sb.sleep(1.2) + sb.cdp.click('button[aria-label="Find flights"]') + sb.sleep(6) + flights = sb.find_elements('div[class*="CardContainer__block"]') + print("**** Flights from %s to %s ****" % (origin, destination)) + if not flights: + print("* No flights found!") + for flight in flights: + print("* " + flight.text.split(" Destination")[0]) + sb.sleep(1.5) diff --git a/examples/master_qa/pytest.ini b/examples/master_qa/pytest.ini index 8f2cb219ab4..0b8fa185bab 100644 --- a/examples/master_qa/pytest.ini +++ b/examples/master_qa/pytest.ini @@ -1,7 +1,7 @@ [pytest] # Display console output. Disable cacheprovider: -addopts = --capture=no -p no:cacheprovider +addopts = --capture=tee-sys -p no:cacheprovider # Skip these directories during test collection: norecursedirs = .* build dist recordings temp assets diff --git a/examples/migration/raw_selenium/pytest.ini b/examples/migration/raw_selenium/pytest.ini index 5478c5d12e5..a1f9578ce5e 100644 --- a/examples/migration/raw_selenium/pytest.ini +++ b/examples/migration/raw_selenium/pytest.ini @@ -1,7 +1,7 @@ [pytest] # Display console output. Disable cacheprovider: -addopts = --capture=no -p no:cacheprovider +addopts = --capture=tee-sys -p no:cacheprovider # Skip these directories during test collection: norecursedirs = .* build dist recordings temp assets diff --git a/examples/pytest.ini b/examples/pytest.ini index 8e6ebeda9fc..38ebd219597 100644 --- a/examples/pytest.ini +++ b/examples/pytest.ini @@ -1,7 +1,7 @@ [pytest] # Display console output. Disable cacheprovider: -addopts = --capture=no -p no:cacheprovider +addopts = --capture=tee-sys -p no:cacheprovider # Skip these directories during test collection: norecursedirs = .* build dist recordings temp assets diff --git a/examples/translations/pytest.ini b/examples/translations/pytest.ini index 8f2cb219ab4..0b8fa185bab 100644 --- a/examples/translations/pytest.ini +++ b/examples/translations/pytest.ini @@ -1,7 +1,7 @@ [pytest] # Display console output. Disable cacheprovider: -addopts = --capture=no -p no:cacheprovider +addopts = --capture=tee-sys -p no:cacheprovider # Skip these directories during test collection: norecursedirs = .* build dist recordings temp assets diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index f235ce45891..bc3a47e9931 100644 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -970,6 +970,44 @@ self._print(TEXT) # Calls Python's print() / Allows for translations ############ +# **** UC Mode methods. (uc=True / --uc) **** + +# (Mainly for CDP Mode) - (For all CDP methods, see the CDP Mode Docs) + +self.activate_cdp_mode(url=None) # Activate CDP Mode on the given URL + +self.reconnect(timeout=0.1) # disconnect() + sleep(timeout) + connect() + +self.disconnect() # Stops the webdriver service to prevent detection + +self.connect() # Starts the webdriver service to allow actions again + +# (For regular UC Mode) + +self.uc_open(url) # (Open in same tab with default reconnect_time) + +self.uc_open_with_tab(url) # (New tab with default reconnect_time) + +self.uc_open_with_reconnect(url, reconnect_time=None) # (New tab) + +self.uc_open_with_disconnect(url, timeout=None) # New tab + sleep() + +self.uc_click(selector) # A stealthy click for evading bot-detection + +self.uc_gui_press_key(key) # Use PyAutoGUI to press the keyboard key + +self.uc_gui_press_keys(keys) # Use PyAutoGUI to press a list of keys + +self.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster + +self.uc_gui_click_x_y(x, y, timeframe=0.25) # PyAutoGUI click screen + +self.uc_gui_click_captcha(frame="iframe", retry=False, blind=False) + +self.uc_gui_handle_captcha(frame="iframe") + +############ + # "driver"-specific methods added (or modified) by SeleniumBase driver.default_get(url) # Because driver.get(url) works differently in UC Mode @@ -1076,7 +1114,9 @@ driver.uc_open_with_reconnect(url, reconnect_time=None) # (New tab) driver.uc_open_with_disconnect(url, timeout=None) # New tab + sleep() -driver.reconnect(timeout) # disconnect() + sleep(timeout) + connect() +driver.uc_activate_cdp_mode(url=None) # Activate CDP Mode on the given URL + +driver.reconnect(timeout=0.1) # disconnect() + sleep(timeout) + connect() driver.disconnect() # Stops the webdriver service to prevent detection @@ -1093,12 +1133,8 @@ driver.uc_gui_write(text) # Similar to uc_gui_press_keys(), but faster driver.uc_gui_click_x_y(x, y, timeframe=0.25) # PyAutoGUI click screen driver.uc_gui_click_captcha(frame="iframe", retry=False, blind=False) -# driver.uc_gui_click_cf(frame="iframe", retry=False, blind=False) -# driver.uc_gui_click_rc(frame="iframe", retry=False, blind=False) -driver.uc_gui_handle_captcha(frame="iframe") # (Auto-detects the CAPTCHA) -# driver.uc_gui_handle_cf(frame="iframe") # PyAutoGUI click CF Turnstile -# driver.uc_gui_handle_rc(frame="iframe") # PyAutoGUI click G. reCAPTCHA +driver.uc_gui_handle_captcha(frame="iframe") ``` -------- diff --git a/help_docs/recorder_mode.md b/help_docs/recorder_mode.md index cf6a78b5a24..8f30b99cd5b 100644 --- a/help_docs/recorder_mode.md +++ b/help_docs/recorder_mode.md @@ -75,7 +75,7 @@ sbase print ./recordings/TEST_NAME_rec.py -n cp ./recordings/TEST_NAME_rec.py ./TEST_NAME.py ``` -The first command creates a boilerplate test with a breakpoint; the second command runs the test with the Recorder activated; the third command prints the completed test to the console; and the fourth command replaces the initial boilerplate with the completed test. If you're just experimenting with the Recorder, you can run the second command as many times as you want, and it'll override previous recordings saved to ``./recordings/TEST_NAME_rec.py``. (Note that ``-s`` is needed to allow breakpoints, unless you already have a ``pytest.ini`` file present with ``addopts = --capture=no`` in it. The ``-q`` is optional, which shortens ``pytest`` console output.) +The first command creates a boilerplate test with a breakpoint; the second command runs the test with the Recorder activated; the third command prints the completed test to the console; and the fourth command replaces the initial boilerplate with the completed test. If you're just experimenting with the Recorder, you can run the second command as many times as you want, and it'll override previous recordings saved to ``./recordings/TEST_NAME_rec.py``. (Note that ``-s`` is needed to allow breakpoints, unless you already have a ``pytest.ini`` file present where you set it. The ``-q`` is optional, which shortens ``pytest`` console output.) ⏺️ You can also use the Recorder to add code to an existing test. To do that, you'll first need to create a breakpoint in your code to insert manual browser actions: diff --git a/pyproject.toml b/pyproject.toml index 336999d3b49..02bc74f6768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ packages = [ ] [tool.pytest.ini_options] -addopts = ["--capture=no", "-p no:cacheprovider"] +addopts = ["--capture=tee-sys", "-p no:cacheprovider"] norecursedirs = [".*", "build", "dist", "recordings", "temp", "assets"] filterwarnings = [ "ignore::pytest.PytestWarning", diff --git a/pytest.ini b/pytest.ini index 8e6ebeda9fc..38ebd219597 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ [pytest] # Display console output. Disable cacheprovider: -addopts = --capture=no -p no:cacheprovider +addopts = --capture=tee-sys -p no:cacheprovider # Skip these directories during test collection: norecursedirs = .* build dist recordings temp assets diff --git a/requirements.txt b/requirements.txt index 95bf5543309..1c9f1bcd2fe 100755 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ pynose>=1.5.3 platformdirs>=4.3.6 typing-extensions>=4.12.2 sbvirtualdisplay>=1.3.0 -six>=1.16.0 +six>=1.17.0 parse>=1.20.2 parse-type>=0.6.4 colorama>=0.4.6 @@ -43,9 +43,8 @@ sortedcontainers==2.4.0 execnet==2.1.1 iniconfig==2.0.0 pluggy==1.5.0 -py==1.11.0 pytest==8.3.4 -pytest-html==2.0.1 +pytest-html==4.1.1 pytest-metadata==3.1.1 pytest-ordering==0.6 pytest-rerunfailures==14.0;python_version<"3.9" diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 60922fb1d81..773faecf514 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.33.3" +__version__ = "4.33.4" diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index 7db1f6b2035..585ce88cd6f 100644 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -120,7 +120,7 @@ def main(): data = [] data.append("[pytest]") - data.append("addopts = --capture=no -p no:cacheprovider") + data.append("addopts = --capture=tee-sys -p no:cacheprovider") data.append("norecursedirs = .* build dist recordings temp assets") data.append("filterwarnings =") data.append(" ignore::pytest.PytestWarning") diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 54be391799e..9398b9ab19c 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -97,6 +97,13 @@ def log_d(message): print(message) +def make_writable(file_path): + # Set permissions to: "If you can read it, you can write it." + mode = os.stat(file_path).st_mode + mode |= (mode & 0o444) >> 1 # copy R bits to W + os.chmod(file_path, mode) + + def make_executable(file_path): # Set permissions to: "If you can read it, you can execute it." mode = os.stat(file_path).st_mode @@ -815,6 +822,13 @@ def install_pyautogui_if_missing(driver): pip_find_lock = fasteners.InterProcessLock( constants.PipInstall.FINDLOCK ) + try: + with pip_find_lock: + pass + except Exception: + # Need write permissions + with suppress(Exception): + make_writable(constants.PipInstall.FINDLOCK) with pip_find_lock: # Prevent issues with multiple processes try: import pyautogui diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 5913ec9fad3..e1522530a67 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -13983,6 +13983,15 @@ def __activate_virtual_display_as_needed(self): pip_find_lock = fasteners.InterProcessLock( constants.PipInstall.FINDLOCK ) + try: + with pip_find_lock: + pass + except Exception: + # Need write permissions + with suppress(Exception): + mode = os.stat(constants.PipInstall.FINDLOCK).st_mode + mode |= (mode & 0o444) >> 1 # copy R bits to W + os.chmod(constants.PipInstall.FINDLOCK, mode) with pip_find_lock: # Prevent issues with multiple processes if self.undetectable and not (self.headless or self.headless2): import Xlib.display diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index bbab084e8a3..6f4e8dd1b04 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -17,6 +17,7 @@ python3_11_or_newer = True py311_patch2 = constants.PatchPy311.PATCH sys_argv = sys.argv +full_time = None pytest_plugins = ["pytester"] # Adds the "testdir" fixture @@ -2038,6 +2039,7 @@ def pytest_runtest_teardown(item): if ( "-s" in sys_argv or "--capture=no" in sys_argv + or "--capture=tee-sys" in sys_argv or ( hasattr(sb_config.pytest_config, "invocation_params") and ( @@ -2045,6 +2047,9 @@ def pytest_runtest_teardown(item): or "--capture=no" in ( sb_config.pytest_config.invocation_params.args ) + or "--capture=tee-sys" in ( + sb_config.pytest_config.invocation_params.args + ) ) ) ): @@ -2053,6 +2058,10 @@ def pytest_runtest_teardown(item): sys.stdout.write("\n=> Fail Page: %s\n" % sb_config._fail_page) +def pytest_html_duration_format(duration): + return "%.2f" % duration + + def pytest_sessionfinish(session): pass @@ -2103,9 +2112,11 @@ def pytest_terminal_summary(terminalreporter): ) -def _perform_pytest_unconfigure_(): +def _perform_pytest_unconfigure_(config): from seleniumbase.core import proxy_helper + reporter = config.pluginmanager.get_plugin("terminalreporter") + duration = time.time() - reporter._sessionstarttime if ( (hasattr(sb_config, "multi_proxy") and not sb_config.multi_proxy) or not hasattr(sb_config, "multi_proxy") @@ -2133,6 +2144,63 @@ def _perform_pytest_unconfigure_(): log_helper.clear_empty_logs() # Dashboard post-processing: Disable time-based refresh and stamp complete if not hasattr(sb_config, "dashboard") or not sb_config.dashboard: + html_report_path = None + the_html_r = None + abs_path = os.path.abspath(".") + if sb_config._html_report_name: + html_report_path = os.path.join( + abs_path, sb_config._html_report_name + ) + if ( + sb_config._using_html_report + and html_report_path + and os.path.exists(html_report_path) + ): + with open(html_report_path, "r", encoding="utf-8") as f: + the_html_r = f.read() + assets_chunk = "if (assets.length === 1) {" + remove_media = "container.classList.remove('media-container')" + rm_n_left = '
<
' + rm_n_right = '
>
' + the_html_r = the_html_r.replace( + assets_chunk, + "%s %s" % (assets_chunk, remove_media), + ) + the_html_r = the_html_r.replace(rm_n_left, "") + the_html_r = the_html_r.replace(rm_n_right, "") + the_html_r = the_html_r.replace("