From 63cc1773f72487c55ecf02f8413733d11ea10c35 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Sep 2025 17:15:51 -0400 Subject: [PATCH 1/5] Add / improve PDF methods --- examples/cdp_mode/ReadMe.md | 1 + help_docs/method_summary.md | 3 ++ sbase/steps.py | 9 +++++ seleniumbase/behave/behave_helper.py | 2 ++ seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/recorder_helper.py | 3 ++ seleniumbase/core/sb_cdp.py | 3 ++ seleniumbase/fixtures/base_case.py | 51 ++++++++++++++++++++++++++- 8 files changed, 72 insertions(+), 1 deletion(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 3e29e8362de..0af7140209f 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -521,6 +521,7 @@ sb.cdp.scroll_up(amount=25) sb.cdp.scroll_down(amount=25) sb.cdp.save_screenshot(name, folder=None, selector=None) sb.cdp.print_to_pdf(name, folder=None) +sb.cdp.save_as_pdf(name, folder=None) ``` -------- diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 9b87ace0c4c..19bff926767 100644 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -247,6 +247,7 @@ self.switch_to_driver(driver) self.switch_to_default_driver() self.save_screenshot(name, folder=None, selector=None, by="css selector") self.save_screenshot_to_logs(name=None, selector=None, by="css selector") +self.save_as_pdf_to_logs(name=None) self.save_data_to_logs(data, file_name=None) self.append_data_to_logs(data, file_name=None) self.save_page_source(name, folder=None) @@ -342,6 +343,8 @@ self.save_data_as(data, file_name, destination_folder=None) self.append_data_to_file(data, file_name, destination_folder=None) self.get_file_data(file_name, folder=None) self.print_to_pdf(name, folder=None) +# Duplicates: +# self.save_as_pdf(name, folder=None) self.get_downloads_folder() self.get_browser_downloads_folder() self.get_downloaded_files(regex=None, browser=False) diff --git a/sbase/steps.py b/sbase/steps.py index f2078f94882..e1d9f986d89 100644 --- a/sbase/steps.py +++ b/sbase/steps.py @@ -1191,6 +1191,15 @@ def set_attributes(context, selector, attribute, value): sb.set_attributes(selector, attribute, value) +@step("Save as PDF to logs") +@step("Save as PDF to the logs") +@step("User saves page as PDF to logs") +@step("User saves page as PDF to the logs") +def save_as_pdf_to_logs(context): + sb = context.sb + sb.save_as_pdf_to_logs() + + @step("Save page source to logs") @step("Save the page source to the logs") @step("User saves page source to logs") diff --git a/seleniumbase/behave/behave_helper.py b/seleniumbase/behave/behave_helper.py index fbd9548df19..1f5e4618816 100644 --- a/seleniumbase/behave/behave_helper.py +++ b/seleniumbase/behave/behave_helper.py @@ -522,6 +522,8 @@ def generate_gherkin(srt_actions): ) elif action[0] == "ss_tl": sb_actions.append("Save screenshot to logs") + elif action[0] == "pdftl": + sb_actions.append("Save as PDF to logs") elif action[0] == "spstl": sb_actions.append("Save page source to logs") elif action[0] == "sh_fc": diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index e72614a8ac0..2cffa217205 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -865,6 +865,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.scroll_down = CDPM.scroll_down cdp.save_screenshot = CDPM.save_screenshot cdp.print_to_pdf = CDPM.print_to_pdf + cdp.save_as_pdf = CDPM.save_as_pdf cdp.page = page # async world cdp.driver = driver.cdp_base # async world cdp.tab = cdp.page # shortcut (original) diff --git a/seleniumbase/core/recorder_helper.py b/seleniumbase/core/recorder_helper.py index 6f3802d86b9..8d039070804 100644 --- a/seleniumbase/core/recorder_helper.py +++ b/seleniumbase/core/recorder_helper.py @@ -556,6 +556,9 @@ def generate_sbase_code(srt_actions): elif action[0] == "ss_tl": method = "save_screenshot_to_logs" sb_actions.append("self.%s()" % method) + elif action[0] == "pdftl": + method = "save_as_pdf_to_logs" + sb_actions.append("self.%s()" % method) elif action[0] == "spstl": method = "save_page_source_to_logs" sb_actions.append("self.%s()" % method) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index e813202ef11..4901f21f0b7 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -2571,6 +2571,9 @@ def print_to_pdf(self, name, folder=None): filename = os.path.join(folder, name) self.loop.run_until_complete(self.page.print_to_pdf(filename)) + def save_as_pdf(self, *args, **kwargs): + self.print_to_pdf(*args, **kwargs) + class Chrome(CDPMethods): def __init__(self, url=None, **kwargs): diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 38a41e03b64..3d746705f75 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -134,6 +134,7 @@ def __initialize_variables(self): self.__requests_timeout = None self.__page_source_count = 0 self.__screenshot_count = 0 + self.__saved_pdf_count = 0 self.__logs_data_count = 0 self.__last_data_file = None self.__level_0_visual_f = False @@ -4492,7 +4493,8 @@ def save_screenshot_to_logs( If a provided selector is not found, then takes a full-page screenshot. (The last_page / failure screenshot is always "screenshot.png") The screenshot will be in PNG format.""" - self.wait_for_ready_state_complete() + if not self.__is_cdp_swap_needed(): + self.wait_for_ready_state_complete() test_logpath = os.path.join(self.log_path, self.__get_test_id()) self.__create_log_path_as_needed(test_logpath) if name: @@ -4510,6 +4512,11 @@ def save_screenshot_to_logs( if selector and by: selector, by = self.__recalculate_selector(selector, by) if page_actions.is_element_present(self.driver, selector, by): + if self.__is_cdp_swap_needed(): + selector = self.convert_to_css_selector(selector, by=by) + return self.cdp.save_screenshot( + name, folder=test_logpath, selector=selector + ) return page_actions.save_screenshot( self.driver, name, test_logpath, selector, by ) @@ -4523,8 +4530,49 @@ def save_screenshot_to_logs( action = ["ss_tl", "", origin, time_stamp] self.__extra_actions.append(action) sb_config._has_logs = True + if self.__is_cdp_swap_needed(): + return self.cdp.save_screenshot(name, folder=test_logpath) return page_actions.save_screenshot(self.driver, name, test_logpath) + def save_as_pdf(self, name, folder=None): + """Same as self.print_to_pdf()""" + return self.print_to_pdf(name, folder=folder) + + def save_as_pdf_to_logs(self, name=None): + """Saves the page as a PDF to the "latest_logs/" folder. + Naming is automatic: + If NO NAME provided: "_1_PDF.pdf", "_2_PDF.pdf", etc. + If NAME IS provided, then: "_1_name.pdf", "_2_name.pdf", etc.""" + if not self.__is_cdp_swap_needed(): + self.wait_for_ready_state_complete() + test_logpath = os.path.join(self.log_path, self.__get_test_id()) + self.__create_log_path_as_needed(test_logpath) + if name: + name = str(name) + self.__saved_pdf_count += 1 + if not name or len(name) == 0: + name = "_%s_PDF.pdf" % self.__saved_pdf_count + else: + pre_name = "_%s_" % self.__saved_pdf_count + if len(name) >= 4 and name[-4:].lower() == ".pdf": + name = name[:-4] + if len(name) == 0: + name = "PDF" + name = "%s%s.pdf" % (pre_name, name) + if self.recorder_mode: + url = self.get_current_url() + if url and len(url) > 0: + if ("http:") in url or ("https:") in url or ("file:") in url: + if self.get_session_storage_item("pause_recorder") == "no": + time_stamp = self.execute_script("return Date.now();") + origin = self.get_origin() + action = ["pdftl", "", origin, time_stamp] + self.__extra_actions.append(action) + sb_config._has_logs = True + if self.__is_cdp_swap_needed(): + return self.cdp.print_to_pdf(name, folder=test_logpath) + return self.print_to_pdf(name, test_logpath) + def save_page_source_to_logs(self, name=None): """Saves the page HTML to the "latest_logs/" folder. Naming is automatic: @@ -5517,6 +5565,7 @@ def __process_recorded_actions(self): ext_actions.append("s_scr") ext_actions.append("ss_tf") ext_actions.append("ss_tl") + ext_actions.append("pdftl") ext_actions.append("spstl") ext_actions.append("da_el") ext_actions.append("da_ep") From 41dd48295ce9c99553edaa827166b4b7049fe2a0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Sep 2025 17:17:00 -0400 Subject: [PATCH 2/5] Refresh Python dependencies --- mkdocs_build/requirements.txt | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 728a5dda7a0..da5050d56ad 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -14,7 +14,7 @@ pathspec==0.12.1 Babel==2.17.0 paginate==0.5.7 mkdocs==1.6.1 -mkdocs-material==9.6.19 +mkdocs-material==9.6.20 mkdocs-exclude-search==0.6.6 mkdocs-simple-hooks==0.1.5 mkdocs-material-extensions==1.3.1 diff --git a/setup.py b/setup.py index c6459f2d693..bc266315a34 100755 --- a/setup.py +++ b/setup.py @@ -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==45.0.7;python_version>="3.9"', + 'cryptography==46.0.1;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"', @@ -296,7 +296,7 @@ ], # pip install -e .[psutil] "psutil": [ - "psutil==7.0.0", + "psutil==7.1.0", ], # pip install -e .[pyautogui] "pyautogui": [ From 8e72c04695160a32520876f8bd69dad1b1e6ee25 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Sep 2025 17:17:24 -0400 Subject: [PATCH 3/5] Update the Google Search example --- examples/raw_google.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/raw_google.py b/examples/raw_google.py index 6a7491ba87c..046e2ff7250 100644 --- a/examples/raw_google.py +++ b/examples/raw_google.py @@ -2,8 +2,12 @@ with SB(test=True, uc=True) as sb: sb.open("https://google.com/ncr") - sb.type('[title="Search"]', "SeleniumBase GitHub page\n") - sb.sleep(1) - sb.click('[href*="github.com/seleniumbase/"]') - sb.save_screenshot_to_logs() # ./latest_logs/ + sb.type('[title="Search"]', "SeleniumBase GitHub page") + sb.click("div:not([jsname]) > * > input") print(sb.get_page_title()) + sb.sleep(2) # Wait for the "AI Overview" result + if sb.is_text_visible("Generating"): + sb.wait_for_text("AI Overview") + sb.save_as_pdf_to_logs() # Saved to ./latest_logs/ + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() From 9bf7057099a1e6a1dcb9dd1df04d97365e7f5b01 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Sep 2025 17:17:36 -0400 Subject: [PATCH 4/5] Update the ReadMe --- README.md | 47 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 55d81fc8cf5..90b8f2e63f1 100755 --- a/README.md +++ b/README.md @@ -65,26 +65,31 @@ -------- -

📗 Here's raw_google.py, which performs a Google search:

+

📗 For performing a Google Search without hitting the "unusual traffic" page, you can use SeleniumBase UC Mode. Here's SeleniumBase/examples/raw_google.py, which exports the search results into different formats (PDF, HTML, PNG):

```python from seleniumbase import SB with SB(test=True, uc=True) as sb: sb.open("https://google.com/ncr") - sb.type('[title="Search"]', "SeleniumBase GitHub page\n") - sb.click('[href*="github.com/seleniumbase/"]') - sb.save_screenshot_to_logs() # ./latest_logs/ + sb.type('[title="Search"]', "SeleniumBase GitHub page") + sb.click("div:not([jsname]) > * > input") print(sb.get_page_title()) + sb.sleep(2) # Wait for the "AI Overview" result + if sb.is_text_visible("Generating"): + sb.wait_for_text("AI Overview") + sb.save_as_pdf_to_logs() # Saved to ./latest_logs/ + sb.save_page_source_to_logs() + sb.save_screenshot_to_logs() ``` > `python raw_google.py` -SeleniumBase Test +SeleniumBase on Google -------- -

📗 Here's an example of bypassing Cloudflare's challenge page: SeleniumBase/examples/cdp_mode/raw_gitlab.py

+

📗 Here's an example of bypassing Cloudflare's challenge page with UC Mode + CDP Mode: SeleniumBase/examples/cdp_mode/raw_gitlab.py

```python from seleniumbase import SB @@ -92,16 +97,36 @@ 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) + sb.assert_text("Username", '[for="user_login"]', timeout=3) + sb.assert_element('label[for="user_login"]') + sb.highlight('button:contains("Sign in")') + sb.highlight('h1:contains("GitLab.com")') + sb.post_message("SeleniumBase wasn't detected", duration=4) ``` +

📙 You can also use SeleniumBase's pure CDP Mode, which doesn't use chromedriver, Selenium, or a Python context manager at all: SeleniumBase/examples/cdp_mode/raw_cdp_gitlab.py

+ +```python +from seleniumbase import sb_cdp + +url = "https://gitlab.com/users/sign_in" +sb = sb_cdp.Chrome(url) +sb.sleep(2.5) +sb.gui_click_captcha() +sb.highlight('h1:contains("GitLab.com")') +sb.highlight('button:contains("Sign in")') +sb.driver.stop() +``` + +> (Due to a change in Chrome 137 where the --load-extension switch was removed, one limitation with this format is that you can't load extensions directly. The other formats weren't affected by this change.) + -------- -

📗 Here's test_get_swag.py, which tests an e-commerce site:

+

📗 Here's SeleniumBase/examples/test_get_swag.py, which tests an e-commerce site:

```python from seleniumbase import BaseCase @@ -133,7 +158,7 @@ class MyTestClass(BaseCase): -------- -

📗 Here's test_coffee_cart.py, which verifies an e-commerce site:

+

📗 Here's SeleniumBase/examples/test_coffee_cart.py, which verifies an e-commerce site:

```zsh pytest test_coffee_cart.py --demo @@ -147,7 +172,7 @@ pytest test_coffee_cart.py --demo -

📗 Here's test_demo_site.py, which covers several actions:

+

📗 Here's SeleniumBase/examples/test_demo_site.py, which covers several actions:

```zsh pytest test_demo_site.py From 1ad760188be768bd8db1c33988c77158a8852cdc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Sep 2025 17:17:48 -0400 Subject: [PATCH 5/5] Version 4.41.7 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 5dd828fa63e..18d3517699c 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.41.6" +__version__ = "4.41.7"