From 270652814202f284005b199998b3ba850a96bf02 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:47:02 -0400 Subject: [PATCH 1/6] Update CDP Mode --- seleniumbase/core/browser_launcher.py | 58 ++++++++++++++++++---- seleniumbase/core/sb_cdp.py | 71 +++++++++++++++++---------- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 1e02737089d..92917b1cd99 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -835,6 +835,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.wait_for_text = CDPM.wait_for_text cdp.wait_for_text_not_visible = CDPM.wait_for_text_not_visible cdp.wait_for_element_visible = CDPM.wait_for_element_visible + cdp.wait_for_element = CDPM.wait_for_element cdp.wait_for_element_not_visible = CDPM.wait_for_element_not_visible cdp.wait_for_element_absent = CDPM.wait_for_element_absent cdp.wait_for_any_of_elements_visible = ( @@ -1365,14 +1366,16 @@ def _uc_gui_click_captcha( frame = '[style="display: grid;"] div div' elif ( driver.is_element_present('[name*="cf-turnstile-"]') - and driver.is_element_present("[class*=spacer] + div div") + and driver.is_element_present( + ".spacer + div div:not([class])" + ) ): - frame = '[class*=spacer] + div div' + frame = '.spacer + div div:not([class])' elif ( driver.is_element_present('[name*="cf-turnstile-"]') - and driver.is_element_present("div.spacer div") + and driver.is_element_present(".spacer div:not([class])") ): - frame = "div.spacer div" + frame = ".spacer div:not([class])" elif ( driver.is_element_present('script[src*="challenges.c"]') and driver.is_element_present( @@ -1384,6 +1387,10 @@ def _uc_gui_click_captcha( "div#turnstile-widget div:not([class])" ): frame = "div#turnstile-widget div:not([class])" + elif driver.is_element_present( + "ngx-turnstile div:not([class])" + ): + frame = "ngx-turnstile div:not([class])" elif driver.is_element_present( 'form div:not([class]):has(input[name*="cf-turn"])' ): @@ -1404,6 +1411,14 @@ def _uc_gui_click_captcha( frame = ".cf-turnstile-wrapper" elif driver.is_element_present('[class="cf-turnstile"]'): frame = '[class="cf-turnstile"]' + elif driver.is_element_present( + '[id*="turnstile"] div:not([class])' + ): + frame = '[id*="turnstile"] div:not([class])' + elif driver.is_element_present( + '[class*="turnstile"] div:not([class])' + ): + frame = '[class*="turnstile"] div:not([class])' elif driver.is_element_present( '[data-callback="onCaptchaSuccess"]' ): @@ -1455,9 +1470,11 @@ def _uc_gui_click_captcha( else: driver.execute_script(script) elif ( - driver.is_element_present("form") - and driver.is_element_present( - 'form [id*="turnstile"] > div:not([class])' + driver.is_element_present( + 'form [id*="turnstile"] div:not([class])' + ) + or driver.is_element_present( + 'form [class*="turnstile"] div:not([class])' ) ): script = ( @@ -1465,12 +1482,35 @@ def _uc_gui_click_captcha( 'form [id*="turnstile"]'); var index = 0, length = $elements.length; for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');} + var $elements = document.querySelectorAll( + 'form [class*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ $elements[index].setAttribute('align', 'left');}""" ) if __is_cdp_swap_needed(driver): driver.cdp.evaluate(script) else: driver.execute_script(script) + elif ( + driver.is_element_present( + '[style*="text-align: center;"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + '[style*="text-align: center;"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + if __is_cdp_swap_needed(driver): + driver.cdp.evaluate(script) + else: + driver.execute_script(script) if not is_in_frame or needs_switch: # Currently not in frame (or nested frame outside CF one) try: @@ -1690,9 +1730,9 @@ def _uc_gui_handle_captcha_(driver, frame="iframe", ctype=None): frame = '[data-callback="onCaptchaSuccess"]' elif ( driver.is_element_present('[name*="cf-turnstile-"]') - and driver.is_element_present("div.spacer div") + and driver.is_element_present(".spacer div:not([class])") ): - frame = "div.spacer div" + frame = ".spacer div:not([class])" elif ( driver.is_element_present('script[src*="challenges.c"]') and driver.is_element_present( diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 9102569c3c1..b98f38b7d70 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -301,28 +301,7 @@ def select(self, selector, timeout=None): self.__add_light_pause() selector = self.__convert_to_css_if_xpath(selector) if (":contains(" in selector): - tag_name = selector.split(":contains(")[0].split(" ")[-1] - text = selector.split(":contains(")[1].split(")")[0][1:-1] - with suppress(Exception): - new_timeout = timeout - if new_timeout < 1: - new_timeout = 1 - self.loop.run_until_complete( - self.page.select(tag_name, timeout=new_timeout) - ) - self.loop.run_until_complete( - self.page.find(text, timeout=new_timeout) - ) - elements = self.find_elements_by_text(text, tag_name=tag_name) - if not elements: - plural = "s" - if timeout == 1: - plural = "" - msg = "\n Element {%s} was not found after %s second%s!" - message = msg % (selector, timeout, plural) - raise Exception(message) - element = self.__add_sync_methods(elements[0]) - return element + return self.find_element(selector, timeout=timeout) failure = False try: element = self.loop.run_until_complete( @@ -1708,9 +1687,9 @@ def gui_click_captcha(self): selector = '[class*=spacer] + div div' elif ( self.is_element_present('[name*="cf-turnstile-"]') - and self.is_element_present("div.spacer div") + and self.is_element_present(".spacer div:not([class])") ): - selector = "div.spacer div" + selector = ".spacer div:not([class])" elif ( self.is_element_present('script[src*="challenges.c"]') and self.is_element_present( @@ -1722,6 +1701,8 @@ def gui_click_captcha(self): "div#turnstile-widget div:not([class])" ): selector = "div#turnstile-widget div:not([class])" + elif self.is_element_present("ngx-turnstile div:not([class])"): + selector = "ngx-turnstile div:not([class])" elif self.is_element_present( 'form div:not([class]):has(input[name*="cf-turn"])' ): @@ -1742,6 +1723,14 @@ def gui_click_captcha(self): selector = ".cf-turnstile-wrapper" elif self.is_element_present('[class="cf-turnstile"]'): selector = '[class="cf-turnstile"]' + elif self.is_element_present( + '[id*="turnstile"] div:not([class])' + ): + selector = '[id*="turnstile"] div:not([class])' + elif self.is_element_present( + '[class*="turnstile"] div:not([class])' + ): + selector = '[class*="turnstile"] div:not([class])' elif self.is_element_present( '[data-callback="onCaptchaSuccess"]' ): @@ -1793,9 +1782,11 @@ def gui_click_captcha(self): self.loop.run_until_complete(self.page.evaluate(script)) self.loop.run_until_complete(self.page.wait()) elif ( - self.is_element_present("form") - and self.is_element_present( - 'form [id*="turnstile"] > div:not([class])' + self.is_element_present( + 'form [id*="turnstile"] div:not([class])' + ) + or self.is_element_present( + 'form [class*="turnstile"] div:not([class])' ) ): script = ( @@ -1803,11 +1794,33 @@ def gui_click_captcha(self): 'form [id*="turnstile"]'); var index = 0, length = $elements.length; for(; index < length; index++){ + $elements[index].setAttribute('align', 'left');} + var $elements = document.querySelectorAll( + 'form [class*="turnstile"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ $elements[index].setAttribute('align', 'left');}""" ) with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(script)) self.loop.run_until_complete(self.page.wait()) + elif ( + self.is_element_present( + '[style*="text-align: center;"] div:not([class])' + ) + ): + script = ( + """var $elements = document.querySelectorAll( + '[style*="text-align: center;"]'); + var index = 0, length = $elements.length; + for(; index < length; index++){ + the_style = $elements[index].getAttribute('style'); + new_style = the_style.replaceAll('center', 'left'); + $elements[index].setAttribute('style', new_style);}""" + ) + with suppress(Exception): + self.loop.run_until_complete(self.page.evaluate(script)) + self.loop.run_until_complete(self.page.wait()) with suppress(Exception): time.sleep(0.08) element_rect = self.get_gui_element_rect(selector, timeout=1) @@ -2163,6 +2176,10 @@ def wait_for_element_visible(self, selector, timeout=None): time.sleep(0.1) raise Exception("Element {%s} was not visible!" % selector) + def wait_for_element(self, selector, **kwargs): + """Same as wait_for_element_visible()""" + return self.wait_for_element_visible(selector, **kwargs) + def wait_for_element_not_visible(self, selector, timeout=None): """Wait for element to not be visible on page. (May still be in DOM)""" if not timeout: From 1e7355f76a3b03de0c2cc39aaedbb0aa0542d92a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:49:17 -0400 Subject: [PATCH 2/6] Update CDP Mode examples --- examples/cdp_mode/raw_ahrefs.py | 2 ++ examples/cdp_mode/raw_copilot.py | 34 ++++++++++++++++++++++++++ examples/cdp_mode/raw_gitlab.py | 1 + examples/cdp_mode/raw_multi_captcha.py | 2 +- examples/cdp_mode/raw_planetmc.py | 2 +- examples/cdp_mode/raw_turnstile.py | 9 +++++++ examples/cdp_mode/raw_united.py | 2 +- 7 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 examples/cdp_mode/raw_copilot.py create mode 100644 examples/cdp_mode/raw_turnstile.py diff --git a/examples/cdp_mode/raw_ahrefs.py b/examples/cdp_mode/raw_ahrefs.py index 2d634361c41..f2d080487b2 100644 --- a/examples/cdp_mode/raw_ahrefs.py +++ b/examples/cdp_mode/raw_ahrefs.py @@ -8,7 +8,9 @@ sb.type(input_field, "github.com/seleniumbase/SeleniumBase") sb.cdp.scroll_down(36) sb.click(submit_button) + sb.sleep(1) sb.uc_gui_click_captcha() + sb.sleep(3) sb.wait_for_text_not_visible("Checking", timeout=15) sb.click_if_visible('button[data-cky-tag="close-button"]') sb.highlight('p:contains("github.com/seleniumbase/SeleniumBase")') diff --git a/examples/cdp_mode/raw_copilot.py b/examples/cdp_mode/raw_copilot.py new file mode 100644 index 00000000000..dff77445844 --- /dev/null +++ b/examples/cdp_mode/raw_copilot.py @@ -0,0 +1,34 @@ +from seleniumbase import SB + +with SB(uc=True, test=True, guest=True) as sb: + url = "https://copilot.microsoft.com/" + sb.activate_cdp_mode(url) + textarea = "textarea#userInput" + sb.wait_for_element(textarea) + sb.sleep(1.5) + sb.click_if_visible('[aria-label="Dismiss"]') + sb.sleep(0.5) + sb.click('button[data-testid*="chat-mode-"]') + sb.sleep(1.1) + sb.click('button[title="Think Deeper"]') + sb.sleep(1.1) + query = "How to migrate from Playwright to SeleniumBase?" + sb.press_keys(textarea, query) + sb.sleep(1.1) + sb.click('button[data-testid="submit-button"]') + sb.sleep(2.5) + sb.uc_gui_click_captcha() + sb.sleep(2.5) + sb.uc_gui_click_captcha() + sb.sleep(2.5) + stop_button = '[data-testid="stop-button"]' + thumbs_up = 'button[data-testid*="-thumbs-up-"]' + sb.wait_for_element_absent(stop_button, timeout=30) + sb.wait_for_element(thumbs_up, timeout=30) + sb.sleep(0.5) + sb.click('button[data-testid*="scroll-to-bottom"]') + sb.sleep(1.5) + folder = "downloaded_files" + file_name = "copilot_results.html" + sb.save_page_source(file_name, folder) + print('"./%s/%s" was saved!' % (folder, file_name)) diff --git a/examples/cdp_mode/raw_gitlab.py b/examples/cdp_mode/raw_gitlab.py index 3d645752cb2..5c043fa6b2b 100644 --- a/examples/cdp_mode/raw_gitlab.py +++ b/examples/cdp_mode/raw_gitlab.py @@ -5,6 +5,7 @@ sb.activate_cdp_mode(url) sb.sleep(2.2) sb.uc_gui_click_captcha() + # (The rest is for testing and demo purposes) sb.assert_text("Username", '[for="user_login"]', timeout=3) sb.assert_element('label[for="user_login"]') sb.highlight('button:contains("Sign in")') diff --git a/examples/cdp_mode/raw_multi_captcha.py b/examples/cdp_mode/raw_multi_captcha.py index f865667b934..31df87e07cd 100644 --- a/examples/cdp_mode/raw_multi_captcha.py +++ b/examples/cdp_mode/raw_multi_captcha.py @@ -8,7 +8,7 @@ def main(url): sb = sb_cdp.Chrome(url, lang="en") sb.set_window_rect(randint(4, 680), randint(8, 380), 840, 520) - sb.sleep(2.2) + sb.sleep(2) sb.gui_click_captcha() sb.sleep(2) sb.driver.quit() diff --git a/examples/cdp_mode/raw_planetmc.py b/examples/cdp_mode/raw_planetmc.py index d80ef551178..f45d1230649 100644 --- a/examples/cdp_mode/raw_planetmc.py +++ b/examples/cdp_mode/raw_planetmc.py @@ -4,5 +4,5 @@ url = "www.planetminecraft.com/account/sign_in/" sb.activate_cdp_mode(url) sb.sleep(2) - sb.cdp.gui_click_element("#turnstile-widget div") + sb.uc_gui_click_captcha() sb.sleep(2) diff --git a/examples/cdp_mode/raw_turnstile.py b/examples/cdp_mode/raw_turnstile.py new file mode 100644 index 00000000000..2065ca68b26 --- /dev/null +++ b/examples/cdp_mode/raw_turnstile.py @@ -0,0 +1,9 @@ +from seleniumbase import SB + +with SB(uc=True, test=True) as sb: + url = "https://seleniumbase.io/apps/turnstile" + sb.activate_cdp_mode(url) + sb.uc_gui_click_captcha() + sb.assert_element("img#captcha-success", timeout=3) + sb.set_messenger_theme(location="top_left") + sb.post_message("SeleniumBase wasn't detected", duration=3) diff --git a/examples/cdp_mode/raw_united.py b/examples/cdp_mode/raw_united.py index da9561325e9..5136cc2385f 100644 --- a/examples/cdp_mode/raw_united.py +++ b/examples/cdp_mode/raw_united.py @@ -35,7 +35,7 @@ part_3 = flight.text.split(" Destination")[-1].split(" Aircraft")[0] parts = "%s - %s %s" % (part_1, part_2, part_3) print("* " + parts) - for category in ["ECO-BASIC", "ECONOMY"]: + for category in ["ECONOMY", "ECONOMY-UNRESTRICTED"]: prices = sb.find_elements('[aria-describedby="%s"]' % category) full_prices = [] for item in prices: From 50484a0f524d758c4b6b079191c020557ba856c5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:51:08 -0400 Subject: [PATCH 3/6] Update examples --- examples/boilerplates/samples/sb_swag_test.py | 6 ++++-- examples/boilerplates/samples/swag_labs_test.py | 2 +- examples/hack_the_planet.py | 6 +----- examples/sb_fixture_tests.py | 8 ++++++-- examples/test_override_sb_fixture.py | 6 ++++-- examples/test_request_sb_fixture.py | 8 ++++++-- examples/test_sb_fixture.py | 8 ++++++-- examples/test_usefixtures.py | 4 +++- 8 files changed, 31 insertions(+), 17 deletions(-) diff --git a/examples/boilerplates/samples/sb_swag_test.py b/examples/boilerplates/samples/sb_swag_test.py index e418bdfb22d..5756ba42f03 100644 --- a/examples/boilerplates/samples/sb_swag_test.py +++ b/examples/boilerplates/samples/sb_swag_test.py @@ -1,8 +1,10 @@ """Classic Page Object Model with the "sb" fixture.""" +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) class LoginPage: - def login_to_swag_labs(self, sb, username): + def login_to_swag_labs(self, sb: BaseCase, username): sb.open("https://www.saucedemo.com") sb.type("#user-name", username) sb.type("#password", "secret_sauce") @@ -10,7 +12,7 @@ def login_to_swag_labs(self, sb, username): class MyTests: - def test_swag_labs_login(self, sb): + def test_swag_labs_login(self, sb: BaseCase): LoginPage().login_to_swag_labs(sb, "standard_user") sb.assert_element("div.inventory_list") sb.assert_element('div:contains("Sauce Labs Backpack")') diff --git a/examples/boilerplates/samples/swag_labs_test.py b/examples/boilerplates/samples/swag_labs_test.py index e26096f2f88..57e226b1014 100644 --- a/examples/boilerplates/samples/swag_labs_test.py +++ b/examples/boilerplates/samples/swag_labs_test.py @@ -4,7 +4,7 @@ class LoginPage: - def login_to_swag_labs(self, sb, username): + def login_to_swag_labs(self, sb: BaseCase, username): sb.open("https://www.saucedemo.com") sb.type("#user-name", username) sb.type("#password", "secret_sauce") diff --git a/examples/hack_the_planet.py b/examples/hack_the_planet.py index 20fefe56f46..be1d70430e4 100644 --- a/examples/hack_the_planet.py +++ b/examples/hack_the_planet.py @@ -301,11 +301,7 @@ def test_all_your_base_are_belong_to_us(self): self.open("https://git-scm.com/") self.set_text_content("span#tagline", aybabtu) - self.set_text_content("#nav-about h3", ayb) - self.set_text_content("#nav-documentation h3", abtu) - self.highlight("span#tagline", loops=8, scroll=False) - self.highlight("#nav-about h3", loops=5, scroll=False) - self.highlight("#nav-documentation h3", loops=6, scroll=False) + self.highlight("span#tagline", loops=10, scroll=False) self.open("https://pragprog.com/") self.set_text_content("header p", aybabtu) diff --git a/examples/sb_fixture_tests.py b/examples/sb_fixture_tests.py index d03924fdff5..f7df081f2a5 100644 --- a/examples/sb_fixture_tests.py +++ b/examples/sb_fixture_tests.py @@ -1,5 +1,9 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + # "sb" pytest fixture test in a method with no class -def test_sb_fixture_with_no_class(sb): +def test_sb_fixture_with_no_class(sb: BaseCase): sb.open("seleniumbase.io/simple/login") sb.type("#username", "demo_user") sb.type("#password", "secret_pass") @@ -13,7 +17,7 @@ def test_sb_fixture_with_no_class(sb): # "sb" pytest fixture test in a method inside a class class Test_SB_Fixture: - def test_sb_fixture_inside_class(self, sb): + def test_sb_fixture_inside_class(self, sb: BaseCase): sb.open("seleniumbase.io/simple/login") sb.type("#username", "demo_user") sb.type("#password", "secret_pass") diff --git a/examples/test_override_sb_fixture.py b/examples/test_override_sb_fixture.py index e9b56031104..63284382f05 100644 --- a/examples/test_override_sb_fixture.py +++ b/examples/test_override_sb_fixture.py @@ -1,5 +1,7 @@ """Overriding the "sb" fixture to override the driver.""" import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) @pytest.fixture() @@ -66,7 +68,7 @@ def tearDown(self): sb._needs_tearDown = False -def test_override_fixture_no_class(sb): +def test_override_fixture_no_class(sb: BaseCase): sb.open("https://seleniumbase.io/demo_page") sb.type("#myTextInput", "This is Automated") sb.set_value("input#mySlider", "100") @@ -77,7 +79,7 @@ def test_override_fixture_no_class(sb): class TestOverride: - def test_override_fixture_inside_class(self, sb): + def test_override_fixture_inside_class(self, sb: BaseCase): sb.open("https://seleniumbase.io/demo_page") sb.type("#myTextInput", "This is Automated") sb.set_value("input#mySlider", "100") diff --git a/examples/test_request_sb_fixture.py b/examples/test_request_sb_fixture.py index 546d3b7fc97..b6de1d2bc18 100644 --- a/examples/test_request_sb_fixture.py +++ b/examples/test_request_sb_fixture.py @@ -1,6 +1,10 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + # Use the pytest "request" fixture to get the "sb" fixture (no class) def test_request_sb_fixture(request): - sb = request.getfixturevalue("sb") + sb: BaseCase = request.getfixturevalue("sb") sb.open("https://seleniumbase.io/demo_page") sb.assert_text("SeleniumBase", "#myForm h2") sb.assert_element("input#myTextInput") @@ -12,7 +16,7 @@ def test_request_sb_fixture(request): # Use the pytest "request" fixture to get the "sb" fixture (in class) class Test_Request_Fixture: def test_request_sb_fixture_in_class(self, request): - sb = request.getfixturevalue("sb") + sb: BaseCase = request.getfixturevalue("sb") sb.open("https://seleniumbase.io/demo_page") sb.assert_element("input#myTextInput") sb.type("#myTextarea", "Automated") diff --git a/examples/test_sb_fixture.py b/examples/test_sb_fixture.py index 4e618b03f79..83f414e467b 100644 --- a/examples/test_sb_fixture.py +++ b/examples/test_sb_fixture.py @@ -1,5 +1,9 @@ +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + + # "sb" pytest fixture test in a method with no class -def test_sb_fixture_with_no_class(sb): +def test_sb_fixture_with_no_class(sb: BaseCase): sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') @@ -8,7 +12,7 @@ def test_sb_fixture_with_no_class(sb): # "sb" pytest fixture test in a method inside a class class Test_SB_Fixture: - def test_sb_fixture_inside_class(self, sb): + def test_sb_fixture_inside_class(self, sb: BaseCase): sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') diff --git a/examples/test_usefixtures.py b/examples/test_usefixtures.py index 1c7676105bc..9b7ccc358d3 100644 --- a/examples/test_usefixtures.py +++ b/examples/test_usefixtures.py @@ -1,4 +1,6 @@ import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) @pytest.mark.usefixtures("sb") @@ -7,7 +9,7 @@ def test_usefixtures_on_class(self): if not hasattr(self, "sb"): print("This test is for pytest only!") return - sb = self.sb + sb: BaseCase = self.sb sb.open("https://seleniumbase.io/realworld/login") sb.type("#username", "demo_user") sb.type("#password", "secret_pass") From 53082389a07873334b2b53e1743d2767ec8b8d23 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:52:39 -0400 Subject: [PATCH 4/6] Refresh Python dependencies --- mkdocs_build/requirements.txt | 4 ++-- requirements.txt | 4 ++-- setup.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index b98a1125747..15f32c3ea14 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -6,7 +6,7 @@ pymdown-extensions>=10.16.1 pipdeptree>=2.28.0 python-dateutil>=2.8.2 Markdown==3.9 -click==8.2.1 +click==8.3.0 ghp-import==2.1.0 watchdog==6.0.0 cairocffi==1.7.1 @@ -14,7 +14,7 @@ pathspec==0.12.1 Babel==2.17.0 paginate==0.5.7 mkdocs==1.6.1 -mkdocs-material==9.6.20 +mkdocs-material==9.6.21 mkdocs-exclude-search==0.6.6 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.3.1 diff --git a/requirements.txt b/requirements.txt index af8c6cbfa1b..bdbdc730da6 100755 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ platformdirs>=4.4.0;python_version>="3.9" typing-extensions>=4.13.2 sbvirtualdisplay>=1.4.0 MarkupSafe==2.1.5;python_version<"3.9" -MarkupSafe>=3.0.2;python_version>="3.9" +MarkupSafe>=3.0.3;python_version>="3.9" Jinja2>=3.1.6 six>=1.17.0 parse>=1.20.2 @@ -68,7 +68,7 @@ parameterized==0.9.0 behave==1.2.6 soupsieve==2.7;python_version<"3.9" soupsieve~=2.8;python_version>="3.9" -beautifulsoup4~=4.13.5 +beautifulsoup4~=4.14.2 pyotp==2.9.0 python-xlib==0.33;platform_system=="Linux" markdown-it-py==3.0.0;python_version<"3.10" diff --git a/setup.py b/setup.py index 99e003fc739..5799dcd15db 100755 --- a/setup.py +++ b/setup.py @@ -167,7 +167,7 @@ 'typing-extensions>=4.13.2', "sbvirtualdisplay>=1.4.0", 'MarkupSafe==2.1.5;python_version<"3.9"', - 'MarkupSafe>=3.0.2;python_version>="3.9"', + 'MarkupSafe>=3.0.3;python_version>="3.9"', "Jinja2>=3.1.6", "six>=1.17.0", 'parse>=1.20.2', @@ -216,7 +216,7 @@ "behave==1.2.6", # Newer ones had issues 'soupsieve==2.7;python_version<"3.9"', 'soupsieve~=2.8;python_version>="3.9"', - "beautifulsoup4~=4.13.5", + "beautifulsoup4~=4.14.2", 'pyotp==2.9.0', 'python-xlib==0.33;platform_system=="Linux"', 'markdown-it-py==3.0.0;python_version<"3.10"', @@ -270,7 +270,7 @@ 'pdfminer.six==20250324;python_version<"3.9"', 'pdfminer.six==20250506;python_version>="3.9"', 'cryptography==39.0.2;python_version<"3.9"', - 'cryptography==46.0.1;python_version>="3.9"', + 'cryptography==46.0.2;python_version>="3.9"', 'cffi==1.17.1;python_version<"3.9"', 'cffi==2.0.0;python_version>="3.9"', 'pycparser==2.22;python_version<"3.9"', From 8c11b73f4e90d0c3c838ef732ac9ea0e45efb319 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:53:44 -0400 Subject: [PATCH 5/6] Update the documentation --- README.md | 1 + examples/cdp_mode/ReadMe.md | 102 +++++++++++++++++++++++++++--------- help_docs/syntax_formats.md | 47 ++++++++++------- 3 files changed, 106 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 0b5b65413cf..3259aa293e1 100755 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ with SB(uc=True, test=True, locale="en") as sb: sb.activate_cdp_mode(url) sb.sleep(2.2) sb.uc_gui_click_captcha() + # (The rest is for testing and demo purposes) sb.assert_text("Username", '[for="user_login"]', timeout=3) sb.assert_element('label[for="user_login"]') sb.highlight('button:contains("Sign in")') diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 0af7140209f..cf19aa67ce4 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -45,7 +45,7 @@ That disconnects WebDriver from Chrome (which prevents detection), and gives you access to `sb.cdp` methods (which don't trigger anti-bot checks). -Simple example: ([SeleniumBase/examples/cdp_mode/raw_gitlab.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_gitlab.py)) +Simple example from [SeleniumBase/examples/cdp_mode/raw_gitlab.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_gitlab.py): ```python from seleniumbase import SB @@ -53,20 +53,19 @@ from seleniumbase import SB with SB(uc=True, test=True, locale="en") as sb: url = "https://gitlab.com/users/sign_in" sb.activate_cdp_mode(url) - sb.sleep(1) + sb.sleep(2.2) sb.uc_gui_click_captcha() - sb.sleep(2) ``` -(If the CAPTCHA wasn't bypassed automatically, then `sb.uc_gui_click_captcha()` gets the job done.) +(If the CAPTCHA wasn't bypassed automatically when going to the URL, then `sb.uc_gui_click_captcha()` gets the job done with a mouse click from [PyAutoGUI](https://github.com/asweigart/pyautogui).) -Note that `PyAutoGUI` is an optional dependency. If calling a method that uses it when not already installed, then `SeleniumBase` installs `PyAutoGUI` at run-time. +ℹ️ Note that `PyAutoGUI` is an optional dependency. If calling a method that uses it when not already installed, then `SeleniumBase` installs `PyAutoGUI` at run-time. -------- -For some Cloudflare CAPTCHAs that appear within websites, you may need to use `sb.cdp.gui_click_element(selector)` instead (if the Turnstile wasn't bypassed automatically). Example: ([SeleniumBase/examples/cdp_mode/raw_planetmc.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_planetmc.py)) +You can also use `sb.cdp.gui_click_element(selector)` to click on elements using `PyAutoGUI`. (This is useful when clicking inside `#shadow-root`.) Example: ```python from seleniumbase import SB @@ -86,17 +85,19 @@ Eg. `sb.cdp.gui_click_element("#turnstile-widget div")` +In most cases, `sb.uc_gui_click_captcha()` is good enough for CF Turnstiles without needing `sb.cdp.gui_click_element(selector)`. (See [SeleniumBase/examples/cdp_mode/raw_planetmc.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_planetmc.py)) + -------- ### 🐙 Here are a few common `sb.cdp` methods: * `sb.cdp.click(selector)` (Uses the CDP API to click) -* `sb.cdp.click_if_visible(selector)` +* `sb.cdp.click_if_visible(selector)` (Click if visible) * `sb.cdp.gui_click_element(selector)` (Uses `PyAutoGUI`) -* `sb.cdp.type(selector, text)` +* `sb.cdp.type(selector, text)` (Type text into a selector) * `sb.cdp.press_keys(selector, text)` (Human-speed `type`) -* `sb.cdp.select_all(selector)` -* `sb.cdp.get_text(selector)` +* `sb.cdp.select_all(selector)` (Returns matching elements) +* `sb.cdp.get_text(selector)` (Returns the element's text) Methods that start with `sb.cdp.gui` use `PyAutoGUI` for interaction. @@ -161,18 +162,12 @@ with SB(uc=True, test=True, locale="en", ad_block=True) as sb: sb.sleep(2) sb.cdp.highlight_overlay("div.pokemon-ability-info") sb.sleep(2) - sb.cdp.click('a[href="https://www.pokemon.com/us/play-pokemon/"]') - sb.sleep(0.6) - sb.cdp.click('h3:contains("Find an Event")') - location = "Concord, MA, USA" - sb.cdp.type('input[data-testid="location-search"]', location) - sb.sleep(1.5) - sb.cdp.click("div.autocomplete-dropdown-container div.suggestion-item") - sb.sleep(0.6) - sb.cdp.click('img[alt="search-icon"]') - sb.sleep(2) - events = sb.cdp.select_all('div[data-testid="event-name"]') - print("*** Pokemon events near %s: ***" % location) + sb.cdp.open("https://events.pokemon.com/EventLocator/") + sb.sleep(3) + sb.cdp.click('button span:contains("Premier Events")') + sb.sleep(1) + events = sb.cdp.select_all('div[class="event-info"]') + print("*** Upcoming Premier Events for Pokémon: ***") for event in events: print("* " + event.text) sb.sleep(2) @@ -371,6 +366,9 @@ sb.cdp.select(selector, timeout=None) sb.cdp.select_all(selector, timeout=None) sb.cdp.find_elements(selector, timeout=None) sb.cdp.find_visible_elements(selector, timeout=None) +sb.cdp.click(selector, timeout=None) +sb.cdp.click_if_visible(selector) +sb.cdp.click_visible_elements(selector, limit=0) sb.cdp.click_nth_element(selector, number) sb.cdp.click_nth_visible_element(selector, number) sb.cdp.click_link(link_text) @@ -391,10 +389,7 @@ sb.cdp.bring_active_window_to_front() sb.cdp.bring_to_front() sb.cdp.get_active_element() sb.cdp.get_active_element_css() -sb.cdp.click(selector, timeout=None) sb.cdp.click_active_element() -sb.cdp.click_if_visible(selector) -sb.cdp.click_visible_elements(selector, limit=0) sb.cdp.mouse_click(selector, timeout=None) sb.cdp.nested_click(parent_selector, selector) sb.cdp.get_nested_element(parent_selector, selector) @@ -488,6 +483,7 @@ sb.cdp.is_exact_text_visible(text, selector="body") sb.cdp.wait_for_text(text, selector="body", timeout=None) sb.cdp.wait_for_text_not_visible(text, selector="body", timeout=None) sb.cdp.wait_for_element_visible(selector, timeout=None) +sb.cdp.wait_for_element(selector, timeout=None) sb.cdp.wait_for_element_not_visible(selector, timeout=None) sb.cdp.wait_for_element_absent(selector, timeout=None) sb.cdp.wait_for_any_of_elements_visible(*args, **kwargs) @@ -524,10 +520,66 @@ sb.cdp.print_to_pdf(name, folder=None) sb.cdp.save_as_pdf(name, folder=None) ``` +ℹ️ When available, calling `sb.METHOD()` redirects to `sb.cdp.METHOD()` because regular SB methods automatically call their CDP Mode counterparts to maintain stealth when CDP Mode is active. + +-------- + +### 🐙 Pure CDP Mode (sb_cdp) + +Pure CDP Mode doesn't use WebDriver for anything. The browser is launched using CDP, and all browser actions are performed using CDP (or PyAutoGUI). Initialization: + +```python +from seleniumbase import sb_cdp + +sb = sb_cdp.Chrome(url=None, **kwargs) +``` + +Pure CDP Mode includes all methods from regular CDP Mode, except that they're called directly from sb instead of sb.cdp. Eg: sb.gui_click_captcha(). To quit a CDP-launched browser, use `sb.driver.stop()`. + +Basic example from [SeleniumBase/examples/cdp_mode/raw_cdp_turnstile.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_turnstile.py): + +```python +from seleniumbase import sb_cdp + +url = "https://seleniumbase.io/apps/turnstile" +sb = sb_cdp.Chrome(url) +sb.gui_click_captcha() +sb.sleep(2) +sb.driver.stop() +``` + +Another example: ([SeleniumBase/examples/cdp_mode/raw_cdp_methods.py](https://github.com/seleniumbase/SeleniumBase/blob/master/examples/cdp_mode/raw_cdp_methods.py)) + +```python +from seleniumbase import sb_cdp + +url = "https://seleniumbase.io/demo_page" +sb = sb_cdp.Chrome(url) +sb.press_keys("input", "Text") +sb.highlight("button") +sb.type("textarea", "Here are some words") +sb.click("button") +sb.set_value("input#mySlider", "100") +sb.click_visible_elements("input.checkBoxClassB") +sb.select_option_by_text("#mySelect", "Set to 75%") +sb.gui_hover_and_click("#myDropdown", "#dropOption2") +sb.gui_click_element("#checkBox1") +sb.gui_drag_and_drop("img#logo", "div#drop2") +sb.nested_click("iframe#myFrame3", ".fBox") +sb.sleep(2) +sb.driver.stop() +``` + +ℹ️ Even if you don't call `sb.driver.stop()`, the browser still quits after the script goes out-of-scope. + -------- ### 🐙 CDP Mode WebElement API / Methods +After finding an element in CDP Mode, you can access `WebElement` methods: + +(Eg. After `element = sb.find_element(selector)`) + ```python element.clear_input() element.click() diff --git a/help_docs/syntax_formats.md b/help_docs/syntax_formats.md index 35b0d376abe..aca1e7251c3 100644 --- a/help_docs/syntax_formats.md +++ b/help_docs/syntax_formats.md @@ -119,7 +119,10 @@ class MyTests(BaseTestCase): The pytest framework comes with a unique system called fixtures, which replaces import statements at the top of Python files by importing libraries directly into test definitions. More than just being an import, a pytest fixture can also automatically call predefined setUp() and tearDown() methods at the beginning and end of test methods. To work, sb is added as an argument to each test method definition that needs SeleniumBase functionality. This means you no longer need import statements in your Python files to use SeleniumBase. If using other pytest fixtures in your tests, you may need to use the SeleniumBase fixture (instead of BaseCase class inheritance) for compatibility reasons. Here's an example of the sb fixture in a test that does not use Python classes: ```python -def test_sb_fixture_with_no_class(sb): +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + +def test_sb_fixture_with_no_class(sb: BaseCase): sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') @@ -134,8 +137,11 @@ def test_sb_fixture_with_no_class(sb): The sb pytest fixture can also be used inside of a class. There is a slight change to the syntax because that means test methods must also include self in their argument definitions when test methods are defined. (The self argument represents the class object, and is used in every test method that lives inside of a class.) Once again, no import statements are needed in your Python files for this to work. Here's an example of using the sb fixture in a test method that lives inside of a Python class: ```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + class Test_SB_Fixture: - def test_sb_fixture_inside_class(self, sb): + def test_sb_fixture_inside_class(self, sb: BaseCase): sb.open("seleniumbase.io/help_docs/install/") sb.type('input[aria-label="Search"]', "GUI Commander") sb.click('mark:contains("Commander")') @@ -154,7 +160,7 @@ from seleniumbase import BaseCase BaseCase.main(__name__, __file__) class LoginPage: - def login_to_swag_labs(self, sb, username): + def login_to_swag_labs(self, sb: BaseCase, username): sb.open("https://www.saucedemo.com") sb.type("#user-name", username) sb.type("#password", "secret_sauce") @@ -175,18 +181,23 @@ class MyTests(BaseCase): This is similar to the classic Page Object Model with BaseCase inheritance, except that this time we pass the sb pytest fixture from the test into the sb arg of the page object class method, (instead of passing self). Now that you're using sb as a pytest fixture, you no longer need to import BaseCase anywhere in your code. See the example below: ```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + class LoginPage: - def login_to_swag_labs(self, sb, username): + def login_to_swag_labs(self, sb: BaseCase, username): sb.open("https://www.saucedemo.com") sb.type("#user-name", username) sb.type("#password", "secret_sauce") sb.click('input[type="submit"]') class MyTests: - def test_swag_labs_login(self, sb): + def test_swag_labs_login(self, sb: BaseCase): LoginPage().login_to_swag_labs(sb, "standard_user") sb.assert_element("div.inventory_list") sb.assert_element('div:contains("Sauce Labs Backpack")') + sb.js_click("a#logout_sidebar_link") + sb.assert_element("div#login_button_container") ``` (See examples/boilerplates/samples/sb_swag_test.py for the full test.) @@ -197,8 +208,11 @@ class MyTests: The pytest request fixture can be used to retrieve other pytest fixtures from within tests, such as the sb fixture. This allows you to have more control over when fixtures get initialized because the fixture no longer needs to be loaded at the very beginning of test methods. This is done by calling request.getfixturevalue('sb') from the test. Here's an example of using the pytest request fixture to load the sb fixture in a test method that does not use Python classes: ```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + def test_request_sb_fixture(request): - sb = request.getfixturevalue('sb') + sb: BaseCase = request.getfixturevalue("sb") sb.open("https://seleniumbase.io/demo_page") sb.assert_text("SeleniumBase", "#myForm h2") sb.assert_element("input#myTextInput") @@ -215,9 +229,12 @@ def test_request_sb_fixture(request): The pytest request fixture can also be used to get the sb fixture from inside a Python class. Here's an example of that: ```python +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) + class Test_Request_Fixture: def test_request_sb_fixture_in_class(self, request): - sb = request.getfixturevalue('sb') + sb: BaseCase = request.getfixturevalue("sb") sb.open("https://seleniumbase.io/demo_page") sb.assert_element("input#myTextInput") sb.type("#myTextarea", "Automated") @@ -272,7 +289,6 @@ from seleniumbase import BaseCase from seleniumwire import webdriver # Requires "pip install selenium-wire" BaseCase.main(__name__, __file__) - class WireTestCase(BaseCase): def get_new_driver(self, *args, **kwargs): options = webdriver.ChromeOptions() @@ -298,6 +314,8 @@ When you want to use SeleniumBase methods via the ``sb`` pytest fixture, but you ```python """Overriding the "sb" fixture to override the driver.""" import pytest +from seleniumbase import BaseCase +BaseCase.main(__name__, __file__) @pytest.fixture() def sb(request): @@ -356,12 +374,12 @@ def sb(request): sb.tearDown() sb._needs_tearDown = False -def test_override_fixture_no_class(sb): +def test_override_fixture_no_class(sb: BaseCase): sb.open("https://seleniumbase.io/demo_page") sb.type("#myTextInput", "This is Automated") class TestOverride: - def test_override_fixture_inside_class(self, sb): + def test_override_fixture_inside_class(self, sb: BaseCase): sb.open("https://seleniumbase.io/demo_page") sb.type("#myTextInput", "This is Automated") ``` @@ -446,7 +464,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.chinese import 硒测试用例 硒测试用例.main(__name__, __file__) - class 我的测试类(硒测试用例): def test_例子1(self): self.开启("https://zh.wikipedia.org/wiki/") @@ -483,7 +500,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.dutch import Testgeval Testgeval.main(__name__, __file__) - class MijnTestklasse(Testgeval): def test_voorbeeld_1(self): self.openen("https://nl.wikipedia.org/wiki/Hoofdpagina") @@ -514,7 +530,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.french import CasDeBase CasDeBase.main(__name__, __file__) - class MaClasseDeTest(CasDeBase): def test_exemple_1(self): self.ouvrir("https://fr.wikipedia.org/wiki/") @@ -546,7 +561,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.italian import CasoDiProva CasoDiProva.main(__name__, __file__) - class MiaClasseDiTest(CasoDiProva): def test_esempio_1(self): self.apri("https://it.wikipedia.org/wiki/") @@ -577,7 +591,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.japanese import セレニウムテストケース セレニウムテストケース.main(__name__, __file__) - class 私のテストクラス(セレニウムテストケース): def test_例1(self): self.を開く("https://ja.wikipedia.org/wiki/") @@ -609,7 +622,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.korean import 셀레늄_테스트_케이스 셀레늄_테스트_케이스.main(__name__, __file__) - class 테스트_클래스(셀레늄_테스트_케이스): def test_실시예_1(self): self.열기("https://ko.wikipedia.org/wiki/") @@ -639,7 +651,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.portuguese import CasoDeTeste CasoDeTeste.main(__name__, __file__) - class MinhaClasseDeTeste(CasoDeTeste): def test_exemplo_1(self): self.abrir("https://pt.wikipedia.org/wiki/") @@ -673,7 +684,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.russian import ТестНаСелен ТестНаСелен.main(__name__, __file__) - class МойТестовыйКласс(ТестНаСелен): def test_пример_1(self): self.открыть("https://ru.wikipedia.org/wiki/") @@ -704,7 +714,6 @@ This format is similar to the English version with BaseCase from seleniumbase.translate.spanish import CasoDePrueba CasoDePrueba.main(__name__, __file__) - class MiClaseDePrueba(CasoDePrueba): def test_ejemplo_1(self): self.abrir("https://es.wikipedia.org/wiki/") From 58c8f657ca1d597d0b8dd454061dced2f1f652dd Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 1 Oct 2025 01:54:12 -0400 Subject: [PATCH 6/6] Version 4.41.11 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index baebf676a8e..2ef3ef4e112 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.41.10" +__version__ = "4.41.11"