From 04acb5e8dc1c3621fe8e36529023a4fae8eebc7f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:38:41 -0400 Subject: [PATCH 1/8] Improve the desktop Recorder App and Recorder Mode --- seleniumbase/console_scripts/run.py | 2 + seleniumbase/console_scripts/sb_mkrec.py | 10 + seleniumbase/console_scripts/sb_recorder.py | 303 ++++++++++--- seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/sb_cdp.py | 24 ++ seleniumbase/fixtures/base_case.py | 446 ++++++++++++++------ seleniumbase/plugins/pytest_plugin.py | 41 +- seleniumbase/plugins/sb_manager.py | 37 +- seleniumbase/plugins/selenium_plugin.py | 41 +- 9 files changed, 689 insertions(+), 216 deletions(-) diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index 60fc9a7a2d8..fb8a2828709 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -834,6 +834,8 @@ def show_options(): op += "--help / -h (Display list of all available pytest options.)\n" op += "--ftrace / --final-trace (Enter Debug Mode after tests end.)\n" op += "--recorder / --rec (Save browser actions as Python scripts.)\n" + op += "--rec-sb-mgr (Save Recorder actions as SB() context manager.)\n" + op += "--rec-sb-cdp (Save Recorder actions as Pure CDP Mode sb_cdp.)\n" op += "--rec-behave / --rec-gherkin (Save actions as Gherkin code.)\n" op += "--rec-print (Display recorded scripts when they are created.)\n" op += "--save-screenshot (Save a screenshot at the end of each test.)\n" diff --git a/seleniumbase/console_scripts/sb_mkrec.py b/seleniumbase/console_scripts/sb_mkrec.py index c13874b42f8..eda1e631607 100644 --- a/seleniumbase/console_scripts/sb_mkrec.py +++ b/seleniumbase/console_scripts/sb_mkrec.py @@ -96,6 +96,8 @@ def main(): use_colors = True force_gui = False rec_behave = False + rec_sb_mgr = False + rec_sb_cdp = False sys_executable = sys.executable if " " in sys_executable: @@ -164,6 +166,10 @@ def main(): use_uc = True elif option.lower() in ("--rec-behave", "--behave", "--gherkin"): rec_behave = True + elif option.lower() in ("--rec-sb-mgr"): + rec_sb_mgr = True + elif option.lower() in ("--rec-sb-cdp"): + rec_sb_cdp = True elif option.lower().startswith("--url="): start_page = option[len("--url="):] elif option.lower() == "--url": @@ -306,6 +312,10 @@ def main(): run_cmd += " --uc" if rec_behave: run_cmd += " --rec-behave" + if rec_sb_mgr: + run_cmd += " --rec-sb-mgr" + if rec_sb_cdp: + run_cmd += " --rec-sb-cdp" print(run_cmd) os.system(run_cmd) if os.path.exists(file_path): diff --git a/seleniumbase/console_scripts/sb_recorder.py b/seleniumbase/console_scripts/sb_recorder.py index 0cf91179554..d578f753d74 100644 --- a/seleniumbase/console_scripts/sb_recorder.py +++ b/seleniumbase/console_scripts/sb_recorder.py @@ -20,7 +20,10 @@ import subprocess import sys import tkinter as tk +from tkinter import ttk +from contextlib import suppress from seleniumbase import config as sb_config +from seleniumbase.core import detect_b_ver from seleniumbase.fixtures import page_utils from seleniumbase.fixtures import shared_utils from tkinter import messagebox @@ -76,7 +79,15 @@ def file_name_error(file_name): return error_msg -def do_recording(file_name, url, overwrite_enabled, use_chrome, window): +def do_recording( + file_name, + url, + overwrite_enabled, + brx, + ucb, + output_format, + window, +): poll = None if sb_config.rec_subprocess_used: poll = sb_config.rec_subprocess_p.poll() @@ -101,7 +112,13 @@ def do_recording(file_name, url, overwrite_enabled, use_chrome, window): if not page_utils.is_valid_url(url): if page_utils.is_valid_url("https://" + url): url = "https://" + url - if not page_utils.is_valid_url(url): + if "edge" in brx.lower() and ucb: + messagebox.showwarning( + "Invalid selection", + "MS Edge cannot be combined with UC Mode " + "because it uses msedgedriver, not chromedriver!", + ) + elif not page_utils.is_valid_url(url): messagebox.showwarning( "Invalid URL", "Enter a valid URL! (Eg. seleniumbase.io)" ) @@ -121,13 +138,12 @@ def do_recording(file_name, url, overwrite_enabled, use_chrome, window): else: os.remove(file_name) add_on = "" - command_args = sys.argv[2:] - if ( - "--rec-behave" in command_args - or "--behave" in command_args - or "--gherkin" in command_args - ): + if "Behave" in output_format: add_on = " --rec-behave" + elif "SB()" in output_format: + add_on = " --rec-sb-mgr" + elif "sb_cdp" in output_format: + add_on = " --rec-sb-cdp" command = ( "%s -m seleniumbase mkrec %s --url=%s --gui" % (sys_executable, file_name, url) @@ -142,25 +158,21 @@ def do_recording(file_name, url, overwrite_enabled, use_chrome, window): "%s -m seleniumbase mkrec %s --url='%s' --gui" % (sys_executable, file_name, url) ) - if not use_chrome: + if "edge" in brx.lower(): command += " --edge" - elif "--opera" in command_args: + elif "opera" in brx.lower(): command += " --opera" - elif "--brave" in command_args: + elif "brave" in brx.lower(): command += " --brave" - elif "--comet" in command_args: + elif "comet" in brx.lower(): command += " --comet" - elif "--atlas" in command_args: + elif "atlas" in brx.lower(): command += " --atlas" - elif "--use-chromium" in command_args: + elif "chromium" in brx.lower(): command += " --use-chromium" - if ( - "--uc" in command_args - or "--cdp" in command_args - or "--undetected" in command_args - or "--undetectable" in command_args - ): + if ucb: command += " --uc" + command_args = sys.argv[2:] if "--ee" in command_args: command += " --ee" command += add_on @@ -175,7 +187,7 @@ def do_recording(file_name, url, overwrite_enabled, use_chrome, window): send_window_to_front(window) -def do_playback(file_name, use_chrome, window, demo_mode=False): +def do_playback(file_name, brx, window, demo_mode=False): file_name = file_name.strip() error_msg = file_name_error(file_name) if error_msg: @@ -189,11 +201,22 @@ def do_playback(file_name, use_chrome, window, demo_mode=False): 'File "%s" does not exist in the current directory!' % file_name, ) return - command = "%s -m pytest %s -q -s" % (sys_executable, file_name) + # command = "%s -m pytest %s -q -s" % (sys_executable, file_name) + command = "%s %s -q -s" % (sys_executable, file_name) if shared_utils.is_linux(): command += " --gui" - if not use_chrome: + if "edge" in brx.lower(): command += " --edge" + elif "opera" in brx.lower(): + command += " --opera" + elif "brave" in brx.lower(): + command += " --brave" + elif "comet" in brx.lower(): + command += " --comet" + elif "atlas" in brx.lower(): + command += " --atlas" + elif "chromium" in brx.lower(): + command += " --use-chromium" if demo_mode: command += " --demo" command_args = sys.argv[2:] @@ -223,100 +246,233 @@ def create_tkinter_gui(): default_file_name = "new_recording.py" window = tk.Tk() window.title("SeleniumBase Recorder App") - window.geometry("344x388") + window.geometry("344x564") + my_font = ("TkDefaultFont", 11) frame = tk.Frame(window) frame.pack() - tk.Label(window, text="").pack() fname = tk.StringVar(value=default_file_name) - tk.Label(window, text="Enter filename to save recording as:").pack() + label = tk.Label(window, text="Enter filename to save recording as:") + label.pack(pady=(20, 0.2)) entry = tk.Entry(window, textvariable=fname) - entry.pack() + entry.config(width=21) + entry.pack(pady=(0.6, 0.2)) cbx = tk.IntVar() chk = tk.Checkbutton(window, text="Overwrite existing files", variable=cbx) chk.pack() chk.select() use_stealth = False + use_behave = False + use_sb_mgr = False + use_sb_cdp = False + use_atlas = False command_args = sys.argv[2:] if ( "--uc" in command_args or "--cdp" in command_args + or "--stealth" in command_args or "--undetected" in command_args or "--undetectable" in command_args ): use_stealth = True - browser_display = "Use Chrome over Edge" - if "--opera" in command_args: - browser_display = "Use Opera over Edge" - elif "--brave" in command_args: - browser_display = "Use Brave over Edge" - elif "--comet" in command_args: - browser_display = "Use Comet over Edge" - elif "--atlas" in command_args: - browser_display = "Use Atlas over Edge" - cbb = tk.IntVar() - if not use_stealth: - chkb = tk.Checkbutton(window, text=browser_display, variable=cbb) - chkb.pack() - if "--edge" not in command_args: - chkb.select() + if ( + "--rec-behave" in command_args + or "--behave" in command_args + or "--gherkin" in command_args + ): + use_behave = True + if "--rec-sb-mgr" in command_args: + use_sb_mgr = True + if "--rec-sb-cdp" in command_args: + use_sb_cdp = True + if "--atlas" in command_args: + use_atlas = True + + tk.Label(window, text="\nSelect a web browser to use:").pack() + br_count = 2 + br_order = {"chrome": 0, "chromium": 1} + options_list = ["Google Chrome"] + options_list.append("Chromium Browser") + if shared_utils.is_windows(): + options_list.append("MS Edge (No Stealth)") + br_order["edge"] = br_count + br_count += 1 else: - chkb = tk.Checkbutton( - window, text="Stealthy Chrome Mode", variable=cbb - ) - chkb.pack() + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("edge")): + options_list.append("MS Edge (No Stealth)") + br_order["edge"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("opera")): + options_list.append("Opera Browser") + br_order["opera"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("brave")): + options_list.append("Brave Browser") + br_order["brave"] = br_count + br_count += 1 + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("comet")): + options_list.append("Comet Browser") + br_order["comet"] = br_count + br_count += 1 + if use_atlas: + with suppress(Exception): + if os.path.exists(detect_b_ver.get_binary_location("atlas")): + options_list.append("Atlas Browser") + br_order["atlas"] = br_count + br_count += 1 + brx = tk.StringVar(window) + if "--use-chromium" in command_args or "--chromium" in command_args: + brx.set(options_list[1]) + elif ( + "--edge" in command_args + and "edge" in br_order + ): + brx.set(options_list[2]) + use_stealth = False + elif "--opera" in command_args and "opera" in br_order: + brx.set(options_list[br_order["opera"]]) + elif "--brave" in command_args and "brave" in br_order: + brx.set(options_list[br_order["brave"]]) + elif "--comet" in command_args and "comet" in br_order: + brx.set(options_list[br_order["comet"]]) + elif "--atlas" in command_args and "atlas" in br_order: + brx.set(options_list[br_order["atlas"]]) + else: + brx.set(options_list[0]) + question_menu = tk.OptionMenu(window, brx, *options_list) + question_menu.config(width=16) + question_menu.pack(pady=(0.6, 0.2)) + + ucb = tk.IntVar() + chkb = tk.Checkbutton( + window, text="Stealth Mode / UC + CDP Mode", variable=ucb + ) + chkb.pack(pady=(0.4, 0.4)) + if use_stealth: chkb.select() - chkb.config(state=tk.DISABLED) - tk.Label(window, text="").pack() + # chkb.config(state=tk.DISABLED) + + tk.Label(window, text="\nSelect an output format to use:").pack() + options_list = ["pytest format / BaseCase"] + options_list.append("Context Manager / SB()") + options_list.append("Pure CDP Mode / sb_cdp") + if use_behave: + options_list.append("BehaveBDD Gherkin File") + frx = tk.StringVar(window) + if use_behave and not use_sb_mgr and not use_sb_cdp: + frx.set(options_list[3]) + elif use_sb_mgr: + frx.set(options_list[1]) + elif use_sb_cdp: + frx.set(options_list[2]) + else: + frx.set(options_list[0]) + question_menu = tk.OptionMenu(window, frx, *options_list) + question_menu.config(width=18) + question_menu.pack(pady=(0.6, 0.2)) + url = tk.StringVar() - tk.Label(window, text="Enter the URL to start recording on:").pack() + label = tk.Label(window, text="Enter a URL to start recording on:") + label.pack(pady=(20, 0.2)) entry = tk.Entry(window, textvariable=url) + entry.config(width=23) entry.pack() entry.focus() entry.bind( "", ( lambda _: do_recording( - fname.get(), url.get(), cbx.get(), cbb.get(), window + fname.get(), + url.get(), + cbx.get(), + brx.get(), + ucb.get(), + frx.get(), + window, ) ), ) - tk.Button( + # Automatically set focus on URL field when clicking back into the app + window.bind( + "", lambda event: entry.focus_set() + if event.widget == window + else None + ) + style = ttk.Style() + style.configure( + "Record.TButton", + foreground="red", + font=("TkDefaultFont", 12, "bold"), + width="8", + padding=(4, 3, 4, 1) + ) + ttk.Button( window, text="Record", - fg="red", + style="Record.TButton", command=lambda: do_recording( - fname.get(), url.get(), cbx.get(), cbb.get(), window + fname.get(), + url.get(), + cbx.get(), + brx.get(), + ucb.get(), + frx.get(), + window, ), - ).pack() - tk.Label(window, text="").pack() - tk.Label(window, text="Playback recording (Normal Mode):").pack() - tk.Button( + ).pack(pady=0.2) + label = tk.Label(window, text="Playback recording (Normal Mode):") + label.pack(pady=(18, 0)) + + style.configure( + "Playback.TButton", + foreground="green", + font=("TkDefaultFont", 11, "bold"), + width="8", + padding=(4, 3, 4, 1) + ) + ttk.Button( window, text="Playback", - fg="green", - command=lambda: do_playback(fname.get(), cbb.get(), window), - ).pack() - tk.Label(window, text="").pack() - tk.Label(window, text="Playback recording (Demo Mode):").pack() + style="Playback.TButton", + command=lambda: do_playback(fname.get(), brx.get(), window), + ).pack(pady=0.2) + label = tk.Label(window, text="Playback recording (Demo Mode):") + label.pack(pady=(14, 0)) try: - tk.Button( + style.configure( + "PlaybackDemo.TButton", + foreground="teal", + font=("TkDefaultFont", 11, "bold"), + width="16", + padding=(4, 3, 4, 1) + ) + ttk.Button( window, text="Playback (Demo Mode)", - fg="teal", + style="PlaybackDemo.TButton", command=lambda: do_playback( - fname.get(), cbb.get(), window, demo_mode=True + fname.get(), brx.get(), window, demo_mode=True ), - ).pack() + ).pack(pady=0.2) except Exception: - tk.Button( + style.configure( + "PlaybackDemo.TButton", + foreground="blue", + font=("TkDefaultFont", 11, "bold"), + padding=(4, 3, 4, 1) + ) + ttk.Button( window, text="Playback (Demo Mode)", - fg="blue", + style="PlaybackDemo.TButton", command=lambda: do_playback( - fname.get(), cbb.get(), window, demo_mode=True + fname.get(), brx.get(), window, demo_mode=True ), - ).pack() + ).pack(pady=0.2) # Bring form window to front send_window_to_front(window) @@ -328,6 +484,13 @@ def create_tkinter_gui(): decoy.deiconify() decoy.destroy() # Start tkinter + for widget in window.winfo_children(): + if isinstance( + widget, (tk.Label, tk.Entry, tk.Checkbutton, tk.OptionMenu) + ): + widget.configure(font=my_font) + window.deiconify() # Force the OS to redraw it actively on top + window.focus_force() # Force keyboard focus into the entry fields window.mainloop() end_program() diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index b745a978280..f05a9e36e64 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -761,6 +761,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.goto = CDPM.goto cdp.reload = CDPM.reload cdp.refresh = CDPM.refresh + cdp.goto_if_not_url = CDPM.goto_if_not_url cdp.add_handler = CDPM.add_handler cdp.get_event_loop = CDPM.get_event_loop cdp.get_rd_host = CDPM.get_rd_host diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 29804756cc7..f66b0c728f8 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -180,6 +180,30 @@ def reload(self, ignore_cache=True, script_to_evaluate_on_load=None): def refresh(self, *args, **kwargs): self.reload(*args, **kwargs) + def goto_if_not_url(self, url): + """Opens the url in the browser if it's not the current url (*). + Parameters tagged on by search engines are ignored for this method. + Eg. If the current url is: + * https://www.bing.com/search?q=SeleniumBase&source=hp + And the url privided by this method call is: + * https://www.bing.com/search?q=SeleniumBase + Then the urls will be considered the same, + and no open() action will be performed. + This method is primarily used by Recorder Mode script generation, + where both clicks and opens are recorded. So if a click() action + leads to an goto() action, then the script generator will attempt + to convert the goto() action into goto_if_not_url() so that the + same page isn't opened again if the user is already on the page.""" + current_url = self.get_current_url() + if current_url != url: + if ( + "?q=" not in current_url + or "&" not in current_url + or current_url.find("?q=") >= current_url.find("&") + or current_url.split("&")[0] != url + ): + self.goto(url) + def get_event_loop(self): return self.loop diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index fd535e5869a..4b0d435dfb9 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1435,8 +1435,8 @@ def open_if_not_url(self, url): and no open() action will be performed. This method is primarily used by Recorder Mode script generation, where both clicks and opens are recorded. So if a click() action - leads to an open() action, then the script generator will attempt - to convert the open() action into open_if_not_url() so that the + leads to an goto() action, then the script generator will attempt + to convert the goto() action into goto_if_not_url() so that the same page isn't opened again if the user is already on the page.""" self.__check_scope() current_url = self.get_current_url() @@ -5929,120 +5929,323 @@ def __process_recorded_actions(self): out_file.close() sys.stdout.write("\nCreated recordings%s__init__.py" % os.sep) - data = [] - data.append("[pytest]") - data.append("addopts = --capture=no -p no:cacheprovider") - data.append("norecursedirs = .* build dist recordings temp assets") - data.append("filterwarnings =") - data.append(" ignore::pytest.PytestWarning") - data.append(" ignore:.*U.*mode is deprecated:DeprecationWarning") - data.append("junit_family = legacy") - data.append("python_files =") - data.append(" test_*.py ") - data.append(" *_test.py ") - data.append(" *_tests.py") - data.append(" *_suite.py") - data.append(" *_feature.py") - data.append(" *_test_rec.py") - data.append(" *_feature_rec.py") - data.append("python_classes = Test* *Test* *Test *Tests *Suite") - data.append("python_functions = test_*") - data.append("markers =") - data.append(" marker1: custom marker") - data.append(" marker2: custom marker") - data.append(" marker3: custom marker") - data.append(" marker_test_suite: custom marker") - data.append(" expected_failure: custom marker") - data.append(" local: custom marker") - data.append(" remote: custom marker") - data.append(" offline: custom marker") - data.append(" develop: custom marker") - data.append(" qa: custom marker") - data.append(" ci: custom marker") - data.append(" e2e: custom marker") - data.append(" ready: custom marker") - data.append(" smoke: custom marker") - data.append(" deploy: custom marker") - data.append(" active: custom marker") - data.append(" master: custom marker") - data.append(" release: custom marker") - data.append(" staging: custom marker") - data.append(" production: custom marker") - data.append("") - extra_file_name = "pytest.ini" - extra_file_path = os.path.join(recordings_folder, extra_file_name) - if not os.path.exists(extra_file_path): - out_file = open(extra_file_path, mode="w+", encoding="utf-8") + if ( + not getattr(self, "rec_sb_mgr", None) + and not getattr(self, "rec_sb_cdp", None) + ): + data = [] + data.append("[pytest]") + data.append("addopts = --capture=no -p no:cacheprovider") + data.append("norecursedirs = .* build dist recordings temp assets") + data.append("filterwarnings =") + data.append(" ignore::pytest.PytestWarning") + data.append( + " ignore:.*U.*mode is deprecated:DeprecationWarning" + ) + data.append("junit_family = legacy") + data.append("python_files =") + data.append(" test_*.py ") + data.append(" *_test.py ") + data.append(" *_tests.py") + data.append(" *_suite.py") + data.append(" *_feature.py") + data.append(" *_test_rec.py") + data.append(" *_feature_rec.py") + data.append("python_classes = Test* *Test* *Test *Tests *Suite") + data.append("python_functions = test_*") + data.append("markers =") + data.append(" marker1: custom marker") + data.append(" marker2: custom marker") + data.append(" marker3: custom marker") + data.append(" marker_test_suite: custom marker") + data.append(" expected_failure: custom marker") + data.append(" local: custom marker") + data.append(" remote: custom marker") + data.append(" offline: custom marker") + data.append(" develop: custom marker") + data.append(" qa: custom marker") + data.append(" ci: custom marker") + data.append(" e2e: custom marker") + data.append(" ready: custom marker") + data.append(" smoke: custom marker") + data.append(" deploy: custom marker") + data.append(" active: custom marker") + data.append(" master: custom marker") + data.append(" release: custom marker") + data.append(" staging: custom marker") + data.append(" production: custom marker") + data.append("") + extra_file_name = "pytest.ini" + extra_file_path = os.path.join(recordings_folder, extra_file_name) + if not os.path.exists(extra_file_path): + out_file = open(extra_file_path, mode="w+", encoding="utf-8") + out_file.writelines("\r\n".join(data)) + out_file.close() + sys.stdout.write("\nCreated recordings%spytest.ini" % os.sep) + + data = [] + data.append("[flake8]") + data.append("exclude=recordings,temp") + data.append("ignore=W503") + data.append("") + data.append("[nosetests]") + data.append("nocapture=1") + data.append("logging-level=INFO") + data.append("") + data.append("[behave]") + data.append("show_skipped=false") + data.append("show_timings=false") + data.append("") + extra_file_name = "setup.cfg" + extra_file_path = os.path.join(recordings_folder, extra_file_name) + if not os.path.exists(extra_file_path): + out_file = open(extra_file_path, mode="w+", encoding="utf-8") + out_file.writelines("\r\n".join(data)) + out_file.close() + sys.stdout.write("\nCreated recordings%ssetup.cfg" % os.sep) + + data = saved_data + file_name = self.__class__.__module__.split(".")[-1] + "_rec.py" + if hasattr(self, "_using_sb_fixture"): + test_id = sb_config._test_id + file_name = ( + test_id.split("::")[0].split("/")[-1].split("\\")[-1] + ) + file_name = file_name.split(".py")[0] + "_rec.py" + if getattr(self, "is_behave", None): + file_name = ( + sb_config.behave_scenario.filename.replace(".", "_") + ) + file_name = ( + file_name.split("/")[-1].split("\\")[-1] + "_rec.py" + ) + file_name = file_name + elif context_filename: + file_name = context_filename + file_path = os.path.join(recordings_folder, file_name) + out_file = open(file_path, mode="w+", encoding="utf-8") out_file.writelines("\r\n".join(data)) out_file.close() - sys.stdout.write("\nCreated recordings%spytest.ini" % os.sep) + rec_message = ">>> RECORDING SAVED as: " + if not new_file: + rec_message = ">>> RECORDING ADDED to: " + star_len = len(rec_message) + len(file_path) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and star_len > terminal_size: + star_len = terminal_size + spc = "\n\n" + if getattr(self, "rec_print", None): + spc = "" + sys.stdout.write( + "\nCreated recordings%s%s" % (os.sep, file_name) + ) + print() + if " " not in file_path: + os.system("sbase print %s -n" % file_path) + elif '"' not in file_path: + os.system('sbase print "%s" -n' % file_path) + else: + os.system("sbase print '%s' -n" % file_path) + stars = "*" * star_len + c1 = "" + c2 = "" + cr = "" + if not is_linux: + c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + rec_message = rec_message.replace(">>>", c2 + ">>>" + cr) + print( + "%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars) + ) + elif getattr(self, "rec_sb_mgr", None): + new_file = False + data = [] + if filename not in sb_config._sb_mgr_recorded_actions: + new_file = True + sb_config._sb_mgr_recorded_actions[filename] = [] + data.append("from seleniumbase import SB") + data.append("") + else: + data = sb_config._sb_mgr_recorded_actions[filename] + if "--uc" in sys.argv: + data.append('with SB(uc=True) as sb:') + else: + data.append("with SB() as sb:") + if len(sb_actions) > 0: + if "--uc" in sys.argv: + data.append(" sb.activate_cdp_mode()") + for action in sb_actions: + action = action.replace("self.", "sb.") + if "--uc" in sys.argv: + action = action.replace( + "sb.type(", "sb.press_keys(" + ) + data.append(" " + action) + else: + data.append(" pass") + data.append("") + sb_config._sb_mgr_recorded_actions[filename] = data + saved_data = data - data = [] - data.append("[flake8]") - data.append("exclude=recordings,temp") - data.append("ignore=W503") - data.append("") - data.append("[nosetests]") - data.append("nocapture=1") - data.append("logging-level=INFO") - data.append("") - data.append("[behave]") - data.append("show_skipped=false") - data.append("show_timings=false") - data.append("") - extra_file_name = "setup.cfg" - extra_file_path = os.path.join(recordings_folder, extra_file_name) - if not os.path.exists(extra_file_path): - out_file = open(extra_file_path, mode="w+", encoding="utf-8") + recordings_folder = constants.Recordings.SAVED_FOLDER + if recordings_folder.endswith("/"): + recordings_folder = recordings_folder[:-1] + if not os.path.exists(recordings_folder): + with suppress(Exception): + os.makedirs(recordings_folder) + sys.stdout.write("\nCreated recordings%s" % os.sep) + + data = saved_data + file_name = self.__class__.__module__.split(".")[-1] + "_rec.py" + if hasattr(self, "_using_sb_fixture"): + test_id = sb_config._test_id + file_name = ( + test_id.split("::")[0].split("/")[-1].split("\\")[-1] + ) + file_name = file_name.split(".py")[0] + "_rec.py" + if getattr(self, "is_behave", None): + file_name = ( + sb_config.behave_scenario.filename.replace(".", "_") + ) + file_name = ( + file_name.split("/")[-1].split("\\")[-1] + "_rec.py" + ) + file_name = file_name + elif context_filename: + file_name = context_filename + file_path = os.path.join(recordings_folder, file_name) + out_file = open(file_path, mode="w+", encoding="utf-8") out_file.writelines("\r\n".join(data)) out_file.close() - sys.stdout.write("\nCreated recordings%ssetup.cfg" % os.sep) - - data = saved_data - file_name = self.__class__.__module__.split(".")[-1] + "_rec.py" - if hasattr(self, "_using_sb_fixture"): - test_id = sb_config._test_id - file_name = test_id.split("::")[0].split("/")[-1].split("\\")[-1] - file_name = file_name.split(".py")[0] + "_rec.py" - if getattr(self, "is_behave", None): - file_name = sb_config.behave_scenario.filename.replace(".", "_") - file_name = file_name.split("/")[-1].split("\\")[-1] + "_rec.py" - file_name = file_name - elif context_filename: - file_name = context_filename - file_path = os.path.join(recordings_folder, file_name) - out_file = open(file_path, mode="w+", encoding="utf-8") - out_file.writelines("\r\n".join(data)) - out_file.close() - rec_message = ">>> RECORDING SAVED as: " - if not new_file: - rec_message = ">>> RECORDING ADDED to: " - star_len = len(rec_message) + len(file_path) - with suppress(Exception): - terminal_size = os.get_terminal_size().columns - if terminal_size > 30 and star_len > terminal_size: - star_len = terminal_size - spc = "\n\n" - if getattr(self, "rec_print", None): - spc = "" - sys.stdout.write("\nCreated recordings%s%s" % (os.sep, file_name)) - print() - if " " not in file_path: - os.system("sbase print %s -n" % file_path) - elif '"' not in file_path: - os.system('sbase print "%s" -n' % file_path) + rec_message = ">>> RECORDING SAVED as: " + if not new_file: + rec_message = ">>> RECORDING ADDED to: " + star_len = len(rec_message) + len(file_path) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and star_len > terminal_size: + star_len = terminal_size + spc = "\n\n" + if getattr(self, "rec_print", None): + spc = "" + sys.stdout.write( + "\nCreated recordings%s%s" % (os.sep, file_name) + ) + print() + if " " not in file_path: + os.system("sbase print %s -n" % file_path) + elif '"' not in file_path: + os.system('sbase print "%s" -n' % file_path) + else: + os.system("sbase print '%s' -n" % file_path) + stars = "*" * star_len + c1 = "" + c2 = "" + cr = "" + if not is_linux: + c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + rec_message = rec_message.replace(">>>", c2 + ">>>" + cr) + print( + "%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars) + ) + else: # rec_sb_cdp + from seleniumbase.core.sb_cdp import CDPMethods + new_file = False + data = [] + if filename not in sb_config._sb_mgr_recorded_actions: + new_file = True + sb_config._sb_mgr_recorded_actions[filename] = [] + data.append("from seleniumbase import sb_cdp") + data.append("") else: - os.system("sbase print '%s' -n" % file_path) - stars = "*" * star_len - c1 = "" - c2 = "" - cr = "" - if not is_linux: - c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX - c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX - cr = colorama.Style.RESET_ALL - rec_message = rec_message.replace(">>>", c2 + ">>>" + cr) - print("%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars)) + data = sb_config._sb_mgr_recorded_actions[filename] + data.append("sb = sb_cdp.Chrome()") + for action in sb_actions: + action = action.replace("self.", "sb.") + action = action.replace( + "sb.type(", "sb.press_keys(" + ) + action = action.replace( + "sb.drag_and_drop(", "sb.gui_drag_and_drop(" + ) + if "sb." in action: + method_name = action.split("sb.")[1].split("(")[0] + if hasattr(CDPMethods, method_name): + data.append(action) + else: + data.append("# %s" % action) + data.append("sb.quit()") + data.append("") + sb_config._sb_mgr_recorded_actions[filename] = data + saved_data = data + + recordings_folder = constants.Recordings.SAVED_FOLDER + if recordings_folder.endswith("/"): + recordings_folder = recordings_folder[:-1] + if not os.path.exists(recordings_folder): + with suppress(Exception): + os.makedirs(recordings_folder) + sys.stdout.write("\nCreated recordings%s" % os.sep) + + data = saved_data + file_name = self.__class__.__module__.split(".")[-1] + "_rec.py" + if hasattr(self, "_using_sb_fixture"): + test_id = sb_config._test_id + file_name = ( + test_id.split("::")[0].split("/")[-1].split("\\")[-1] + ) + file_name = file_name.split(".py")[0] + "_rec.py" + if getattr(self, "is_behave", None): + file_name = ( + sb_config.behave_scenario.filename.replace(".", "_") + ) + file_name = ( + file_name.split("/")[-1].split("\\")[-1] + "_rec.py" + ) + file_name = file_name + elif context_filename: + file_name = context_filename + file_path = os.path.join(recordings_folder, file_name) + out_file = open(file_path, mode="w+", encoding="utf-8") + out_file.writelines("\r\n".join(data)) + out_file.close() + rec_message = ">>> RECORDING SAVED as: " + if not new_file: + rec_message = ">>> RECORDING ADDED to: " + star_len = len(rec_message) + len(file_path) + with suppress(Exception): + terminal_size = os.get_terminal_size().columns + if terminal_size > 30 and star_len > terminal_size: + star_len = terminal_size + spc = "\n\n" + if getattr(self, "rec_print", None): + spc = "" + sys.stdout.write( + "\nCreated recordings%s%s" % (os.sep, file_name) + ) + print() + if " " not in file_path: + os.system("sbase print %s -n" % file_path) + elif '"' not in file_path: + os.system('sbase print "%s" -n' % file_path) + else: + os.system("sbase print '%s' -n" % file_path) + stars = "*" * star_len + c1 = "" + c2 = "" + cr = "" + if not is_linux: + c1 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c2 = colorama.Fore.LIGHTRED_EX + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + rec_message = rec_message.replace(">>>", c2 + ">>>" + cr) + print( + "%s%s%s%s%s\n%s" % (spc, rec_message, c1, file_path, cr, stars) + ) if getattr(self, "rec_behave", None): # Also generate necessary behave-gherkin files. @@ -15544,16 +15747,19 @@ def setUp(self, masterqa_mode=False): self.recorder_ext = sb_config.recorder_mode self.rec_print = sb_config.rec_print self.rec_behave = sb_config.rec_behave + self.rec_sb_mgr = sb_config.rec_sb_mgr + self.rec_sb_cdp = sb_config.rec_sb_cdp self.record_sleep = sb_config.record_sleep - if self.rec_print and not self.recorder_mode: - self.recorder_mode = True - self.recorder_ext = True - elif self.rec_behave and not self.recorder_mode: - self.recorder_mode = True - self.recorder_ext = True - elif self.record_sleep and not self.recorder_mode: - self.recorder_mode = True - self.recorder_ext = True + if not self.recorder_mode: + if ( + self.record_sleep + or self.rec_print + or self.rec_behave + or self.rec_sb_mgr + or self.rec_sb_cdp + ): + self.recorder_mode = True + self.recorder_ext = True self.disable_cookies = sb_config.disable_cookies self.disable_js = sb_config.disable_js self.disable_csp = sb_config.disable_csp @@ -15696,6 +15902,8 @@ def setUp(self, masterqa_mode=False): if not hasattr(sb_config, "_recorded_actions"): # Only filled when Recorder Mode is enabled sb_config._recorded_actions = {} + sb_config._sb_mgr_recorded_actions = {} + sb_config._sb_cdp_recorded_actions = {} sb_config._behave_recorded_actions = {} if not hasattr(settings, "SWITCH_TO_NEW_TABS_ON_CLICK"): diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 5a5f548b1d4..afca75d19b4 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -96,6 +96,8 @@ def pytest_addoption(parser): --verify-delay=SECONDS (The delay before MasterQA verification checks.) --ee / --esc-end (Lets the user end the current test via the ESC key.) --recorder (Enables the Recorder for turning browser actions into code.) + --rec-sb-mgr (A Recorder Mode that generates SB() context manager code.) + --rec-sb-cdp (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) --rec-behave (Same as Recorder Mode, but also generates behave-gherkin.) --rec-sleep (If the Recorder is enabled, also records self.sleep calls.) --rec-print (If the Recorder is enabled, prints output after tests end.) @@ -1009,6 +1011,24 @@ def pytest_addoption(parser): which records browser actions for converting into SeleniumBase scripts.""", ) + parser.addoption( + "--rec-sb-mgr", + action="store_true", + dest="rec_sb_mgr", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + context manager format.""", + ) + parser.addoption( + "--rec-sb-cdp", + action="store_true", + dest="rec_sb_cdp", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + Pure CDP Mode sb_cdp sync format.""", + ) parser.addoption( "--rec-behave", "--rec-gherkin", @@ -1831,18 +1851,21 @@ def pytest_configure(config): sb_config.esc_end = config.getoption("esc_end") sb_config.recorder_mode = config.getoption("recorder_mode") sb_config.recorder_ext = config.getoption("recorder_mode") # Again + sb_config.rec_sb_mgr = config.getoption("rec_sb_mgr") + sb_config.rec_sb_cdp = config.getoption("rec_sb_cdp") sb_config.rec_behave = config.getoption("rec_behave") sb_config.rec_print = config.getoption("rec_print") sb_config.record_sleep = config.getoption("record_sleep") - if sb_config.rec_print and not sb_config.recorder_mode: - sb_config.recorder_mode = True - sb_config.recorder_ext = True - elif sb_config.rec_behave and not sb_config.recorder_mode: - sb_config.recorder_mode = True - sb_config.recorder_ext = True - elif sb_config.record_sleep and not sb_config.recorder_mode: - sb_config.recorder_mode = True - sb_config.recorder_ext = True + if not sb_config.recorder_mode: + if ( + sb_config.record_sleep + or sb_config.rec_print + or sb_config.rec_behave + or sb_config.rec_sb_mgr + or sb_config.rec_sb_cdp + ): + sb_config.recorder_mode = True + sb_config.recorder_ext = True sb_config.disable_cookies = config.getoption("disable_cookies") sb_config.disable_js = config.getoption("disable_js") sb_config.disable_csp = config.getoption("disable_csp") diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py index d95bf6e2417..d36630636ef 100644 --- a/seleniumbase/plugins/sb_manager.py +++ b/seleniumbase/plugins/sb_manager.py @@ -92,6 +92,8 @@ def SB( xvfb_metrics=None, # Set Xvfb display size on Linux: "Width,Height". start_page=None, # The starting URL for the web browser when tests begin. rec_print=None, # If Recorder is enabled, prints output after tests end. + rec_sb_mgr=None, # Recorder Mode that generates SB() context manager code. + rec_sb_cdp=None, # Recorder Mode that generates Pure CDP Mode sb_cdp code. rec_behave=None, # Like Recorder Mode, but also generates behave-gherkin. record_sleep=None, # If Recorder enabled, also records self.sleep calls. data=None, # Extra test data. Access with "self.data" in tests. @@ -216,6 +218,8 @@ def SB( xvfb_metrics (w,h): Set Xvfb display size on Linux: "Width,Height". start_page (str): The starting URL for the web browser when tests begin. rec_print (bool): If Recorder is enabled, prints output after tests end. + rec_sb_mgr (bool): Recorder Mode that generates SB() context manager code. + rec_sb_cdp (bool): Recorder Mode that generates Pure CDP Mode sb_cdp code. rec_behave (bool): Like Recorder Mode, but also generates behave-gherkin. record_sleep (bool): If Recorder enabled, also records self.sleep calls. data (str): Extra test data. Access with "self.data" in tests. @@ -733,6 +737,16 @@ def SB( rec_print = True else: rec_print = False + if rec_sb_mgr is None: + if "--rec-sb-mgr" in sys_argv: + rec_sb_mgr = True + else: + rec_sb_mgr = False + if rec_sb_cdp is None: + if "--rec-sb-cdp" in sys_argv: + rec_sb_cdp = True + else: + rec_sb_cdp = False if rec_behave is None: if "--rec-behave" in sys_argv: rec_behave = True @@ -837,15 +851,16 @@ def SB( headless2 = False # Only for Chromium browsers if not headless and not headless2: headed = True - if rec_print and not recorder_mode: - recorder_mode = True - recorder_ext = True - elif rec_behave and not recorder_mode: - recorder_mode = True - recorder_ext = True - elif record_sleep and not recorder_mode: - recorder_mode = True - recorder_ext = True + if not recorder_mode: + if ( + record_sleep + or rec_print + or rec_behave + or rec_sb_mgr + or rec_sb_cdp + ): + recorder_mode = True + recorder_ext = True if recorder_mode and headless: headless = False headless1 = False @@ -1200,6 +1215,8 @@ def SB( sb_config.recorder_mode = recorder_mode sb_config.recorder_ext = recorder_ext sb_config.record_sleep = record_sleep + sb_config.rec_sb_mgr = rec_sb_mgr + sb_config.rec_sb_cdp = rec_sb_cdp sb_config.rec_behave = rec_behave sb_config.rec_print = rec_print sb_config.report_on = False @@ -1310,6 +1327,8 @@ def SB( sb.recorder_mode = sb_config.recorder_mode sb.recorder_ext = sb_config.recorder_ext sb.record_sleep = sb_config.record_sleep + sb.rec_sb_mgr = sb_config.rec_sb_mgr + sb.rec_sb_cdp = sb_config.rec_sb_cdp sb.rec_behave = sb_config.rec_behave sb.rec_print = sb_config.rec_print sb.report_on = sb_config.report_on diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index adbedf45254..2f680066a75 100644 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -75,6 +75,8 @@ class SeleniumBrowser(Plugin): --verify-delay=SECONDS (The delay before MasterQA verification checks.) --ee / --esc-end (Lets the user end the current test via the ESC key.) --recorder (Enables the Recorder for turning browser actions into code.) + --rec-sb-mgr (A Recorder Mode that generates SB() context manager code.) + --rec-sb-cdp (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) --rec-behave (Same as Recorder Mode, but also generates behave-gherkin.) --rec-sleep (If the Recorder is enabled, also records self.sleep calls.) --rec-print (If the Recorder is enabled, prints output after tests end.) @@ -731,6 +733,24 @@ def options(self, parser, env): which records browser actions for converting into SeleniumBase scripts.""", ) + parser.addoption( + "--rec-sb-mgr", + action="store_true", + dest="rec_sb_mgr", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + context manager format.""", + ) + parser.addoption( + "--rec-sb-cdp", + action="store_true", + dest="rec_sb_cdp", + default=False, + help="""Not only enables the SeleniumBase Recorder, + but also saves recorded actions into the + Pure CDP Mode sb_cdp sync format.""", + ) parser.addoption( "--rec-behave", "--rec-gherkin", @@ -1388,18 +1408,21 @@ def beforeTest(self, test): test.test.esc_end = self.options.esc_end test.test.recorder_mode = self.options.recorder_mode test.test.recorder_ext = self.options.recorder_mode # Again + test.test.rec_sb_mgr = self.options.rec_sb_mgr + test.test.rec_sb_cdp = self.options.rec_sb_cdp test.test.rec_behave = self.options.rec_behave test.test.rec_print = self.options.rec_print test.test.record_sleep = self.options.record_sleep - if self.options.rec_print: - test.test.recorder_mode = True - test.test.recorder_ext = True - elif self.options.rec_behave: - test.test.recorder_mode = True - test.test.recorder_ext = True - elif self.options.record_sleep: - test.test.recorder_mode = True - test.test.recorder_ext = True + if not self.options.recorder_mode: + if ( + self.options.record_sleep + or self.options.rec_print + or self.options.rec_behave + or self.options.rec_sb_mgr + or self.options.rec_sb_cdp + ): + test.test.recorder_mode = True + test.test.recorder_ext = True test.test.disable_cookies = self.options.disable_cookies test.test.disable_js = self.options.disable_js test.test.disable_csp = self.options.disable_csp From 8a9a8eb8dbfa6f90166564769fea5244eafaa32f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:42:06 -0400 Subject: [PATCH 2/8] Fix `sb.solve_captcha()` on Windows for Slider CAPTCHAs --- seleniumbase/core/sb_cdp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index f66b0c728f8..eb6991e045d 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -2300,6 +2300,10 @@ def __gui_slide_datadome_captcha(self): x2, y2 = self.get_gui_element_center("div.sliderTarget") self.close_active_tab() self.switch_to_tab(tab) + if shared_utils.is_windows(): + time.sleep(0.48) + self.loop.run_until_complete(self.page.wait(0.1)) + x2 = x2 + 22.5 # Overshoot drop to maximize compatibility self.gui_drag_drop_points(x1, y1, x2, y2, timeframe=0.55) time.sleep(0.25) self.loop.run_until_complete(self.page.wait(0.2)) From 8a39a469f0b09b7aff6e7b3e2dc0a7608dbdb027 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:44:52 -0400 Subject: [PATCH 3/8] Prevent cleanup warnings on Windows --- seleniumbase/undetected/cdp_driver/browser.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/seleniumbase/undetected/cdp_driver/browser.py b/seleniumbase/undetected/cdp_driver/browser.py index 0842d534290..0c24a3d73f1 100644 --- a/seleniumbase/undetected/cdp_driver/browser.py +++ b/seleniumbase/undetected/cdp_driver/browser.py @@ -12,6 +12,7 @@ import psutil import re import shutil +import sys import time import urllib.parse import urllib.request @@ -1018,6 +1019,24 @@ def stop(self, deconstruct=False): and connection_id in sb_config._closed_connection_ids ): sb_config._closed_connection_ids.remove(connection_id) + if shared_utils.is_windows(): + default_unraisablehook = sys.unraisablehook + + def silence_pipe_destruction_errors(unraisable): + exc_type = unraisable.exc_type + exc_value = unraisable.exc_value + if ( + exc_type is ValueError + and "I/O operation on closed pipe" in str(exc_value) + ): + return + default_unraisablehook(unraisable) + + sys.unraisablehook = silence_pipe_destruction_errors + # Automatically restore Python's default behavior at program exit + atexit.register( + lambda: setattr(sys, "unraisablehook", default_unraisablehook) + ) def quit(self): self.stop() From d72f80972432117bb23e9a37f2f1e6c8cdbac365 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:46:08 -0400 Subject: [PATCH 4/8] Fix issue with `get_title()` failing at random --- seleniumbase/undetected/cdp_driver/tab.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/seleniumbase/undetected/cdp_driver/tab.py b/seleniumbase/undetected/cdp_driver/tab.py index f6b26c58205..5c8a4e5fc0c 100644 --- a/seleniumbase/undetected/cdp_driver/tab.py +++ b/seleniumbase/undetected/cdp_driver/tab.py @@ -1626,7 +1626,11 @@ async def get_gui_element_rect(self, selector, timeout=5): return ({"height": e_height, "width": e_width, "x": x, "y": y}) async def get_title(self): - return await self.evaluate("document.title") + try: + return await self.evaluate("document.title") + except (Exception, TypeError): + await self.sleep(0.068) + return await self.evaluate("document.title") async def get_current_url(self): return await self.evaluate("window.location.href") From 13796feaaab122cfa8678644d95644d50cd5485f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:46:53 -0400 Subject: [PATCH 5/8] Update the docs --- README.md | 2 ++ examples/cdp_mode/ReadMe.md | 1 + help_docs/cdp_mode_methods.md | 1 + help_docs/customizing_test_runs.md | 2 ++ help_docs/recorder_mode.md | 12 ++++++------ seleniumbase/console_scripts/ReadMe.md | 2 ++ 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0013bd0f96a..ab5bd5d08d6 100755 --- a/README.md +++ b/README.md @@ -823,6 +823,8 @@ pytest test_coffee_cart.py --trace --verify-delay=SECONDS # (The delay before MasterQA verification checks.) --ee | --esc-end # (Lets the user end the current test via the ESC key.) --recorder # (Enables the Recorder for turning browser actions into code.) +--rec-sb-mgr # (A Recorder Mode that generates SB() context manager code.) +--rec-sb-cdp # (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) --rec-behave # (Same as Recorder Mode, but also generates behave-gherkin.) --rec-sleep # (If the Recorder is enabled, also records self.sleep calls.) --rec-print # (If the Recorder is enabled, prints output after tests end.) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 5eabe01a185..ad6f032fc52 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -367,6 +367,7 @@ sb.get(url, **kwargs) sb.open(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode sb.goto(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode sb.reload(ignore_cache=True, script_to_evaluate_on_load=None) +sb.goto_if_not_url(url) sb.refresh(*args, **kwargs) sb.get_event_loop() sb.get_rd_host() # Returns the remote-debugging host diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 695d7e16d69..c55b0934f33 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -12,6 +12,7 @@ sb.open(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode sb.goto(url, **kwargs) # Same as sb.get(url, **kwargs) in CDP Mode sb.reload(ignore_cache=True, script_to_evaluate_on_load=None) sb.refresh(*args, **kwargs) +sb.goto_if_not_url(url) sb.get_event_loop() sb.get_rd_host() # Returns the remote-debugging host sb.get_rd_port() # Returns the remote-debugging port diff --git a/help_docs/customizing_test_runs.md b/help_docs/customizing_test_runs.md index 05f7b11dc6e..da2aa902a91 100644 --- a/help_docs/customizing_test_runs.md +++ b/help_docs/customizing_test_runs.md @@ -192,6 +192,8 @@ pytest my_first_test.py --settings-file=custom_settings.py --verify-delay=SECONDS # (The delay before MasterQA verification checks.) --ee | --esc-end # (Lets the user end the current test via the ESC key.) --recorder # (Enables the Recorder for turning browser actions into code.) +--rec-sb-mgr # (A Recorder Mode that generates SB() context manager code.) +--rec-sb-cdp # (A Recorder Mode that generates Pure CDP Mode sb_cdp code.) --rec-behave # (Same as Recorder Mode, but also generates behave-gherkin.) --rec-sleep # (If the Recorder is enabled, also records self.sleep calls.) --rec-print # (If the Recorder is enabled, prints output after tests end.) diff --git a/help_docs/recorder_mode.md b/help_docs/recorder_mode.md index 7651203aa94..53f8129ed7b 100644 --- a/help_docs/recorder_mode.md +++ b/help_docs/recorder_mode.md @@ -7,17 +7,17 @@ 🔴 SeleniumBase Recorder Mode lets you record & export browser actions into test automation scripts.
- +⏺️ Recorder Mode can be activated from the command-line interface or the desktop Recorder App. To launch the desktop app, run: `sbase recorder`: -⏺️ Recorder Mode can be activated from the command-line interface or the Recorder Desktop App. + -⏺️ To make a new recording from the command-line interface, use `sbase mkrec`, `sbase codegen`, or `sbase record`: +⏺️ To make a new recording from the command-line interface, use `sbase mkrec`: ```zsh sbase mkrec TEST_NAME.py --url=URL ``` -If the file already exists, you'll get an error. If no URL is provided, you'll start on a blank page and will need to navigate somewhere for the Recorder to activate. (The Recorder captures events on URLs that start with `https`, `http`, or `file`.) The command above runs an empty test that stops at a breakpoint so that you can perform manual browser actions for the Recorder. When you have finished recording, type "`c`" on the command-line and press `[ENTER]` to continue from the breakpoint. The test will complete and a file called `TEST_NAME_rec.py` will be automatically created in the `./recordings` folder. That file will get copied back to the original folder with the name you gave it. (You can run with Edge instead of Chrome by adding `--edge` to the command above. For headed Linux machines, add `--gui` to prevent the default headless mode on Linux.) +If the file already exists, you'll get an error. If no URL is provided, you'll start on a blank page and will need to navigate somewhere for the Recorder to activate. (The Recorder captures events on URLs that start with `https`, `http`, or `file`.) The command above runs an empty test that stops at a breakpoint so that you can perform manual browser actions for the Recorder. When you have finished recording, type "`c`" on the command-line and press `[ENTER]` to continue from the breakpoint. The test will complete and a file called `TEST_NAME_rec.py` will be automatically created in the `./recordings` folder. That file will get copied back to the original folder with the name you gave it. (For headed Linux machines, add `--gui` to prevent the default headless mode on Linux.) Example: @@ -55,7 +55,7 @@ sbase recorder * Starting the SeleniumBase Recorder Desktop App... ``` - + ⏺️ While a recording is in progress, you can press the `[ESC]` key to pause the Recorder. To resume the recording, you can hit the `[~`]` key, which is located directly below the `[ESC]` key on most keyboards. @@ -115,7 +115,7 @@ pytest TEST_NAME.py --trace --rec -s ⏺️ Additionally, the SeleniumBase self.goto(URL) method will also open a new tab for you in Recorder Mode if the domain/origin is different from the current URL. If you need to navigate to a different domain/origin from within the same tab, call self.save_recorded_actions() first, which saves the recorded data for later. When a recorded test completes, SeleniumBase scans the sessionStorage data of all open browser tabs for generating the completed script. -⏺️ As an alternative to activating Recorder Mode with the --rec command-line arg, you can also call self.activate_recorder() from your tests. Using the Recorder this way is only useful for tests that stay on the same URL. This is because the standard Recorder Mode functions as a Chrome extension and persists wherever the browser goes. (This version only stays on the page where called.) +⏺️ As an alternative to activating Recorder Mode with the \-\-rec command-line arg, you can also call self.activate_recorder() from your tests. Using the Recorder this way is only useful for tests that stay on the same URL. This is because the standard Recorder Mode functions as a Chrome extension and persists wherever the browser goes. (This version only stays on the page where called.) ⏺️ (Note that same domain/origin is not the same as same URL. Example: https://xkcd.com/353 and https://xkcd.com/1537 are two different URLs with the same domain/origin. That means both URLs share the same sessionStorage, and that changes persist to different URLs of the same domain/origin. If you want to find out a website's origin during a test, just call: self.get_origin(), which returns the value of window.location.origin from the browser's console.) diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index 6a64218bd70..178747504d1 100644 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -153,6 +153,8 @@ that are available when using SeleniumBase. --help / -h (Display list of all available pytest options.) --final-debug (Enter Final Debug Mode after each test ends.) --recorder / --rec (Save browser actions as Python scripts.) +--rec-sb-mgr (Save Recorder actions as SB() context manager.) +--rec-sb-cdp (Save Recorder actions as Pure CDP Mode sb_cdp.) --rec-behave / --rec-gherkin (Save actions as Gherkin code.) --rec-print (Display recorded scripts when they are created.) --save-screenshot (Save a screenshot at the end of each test.) From 27b2fb0a38cdfb5d90aea74235cae8e655c2df1a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:47:10 -0400 Subject: [PATCH 6/8] Update examples --- examples/cdp_mode/raw_idealista.py | 2 +- examples/raw_parameter_script.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/cdp_mode/raw_idealista.py b/examples/cdp_mode/raw_idealista.py index d1940d6be0f..772bb83b5ca 100644 --- a/examples/cdp_mode/raw_idealista.py +++ b/examples/cdp_mode/raw_idealista.py @@ -8,7 +8,7 @@ sb.sleep(1.5) sb.solve_captcha() sb.sleep(2) - sb.click("button#didomi-notice-agree-button") + sb.click_if_visible("button#didomi-notice-agree-button", timeout=3) print("*** " + sb.get_text("h1")) items = sb.find_elements("div.item-info-container") for item in items: diff --git a/examples/raw_parameter_script.py b/examples/raw_parameter_script.py index ee12ea0e3e3..cafaf670fd0 100644 --- a/examples/raw_parameter_script.py +++ b/examples/raw_parameter_script.py @@ -103,6 +103,8 @@ sb.recorder_ext = False sb.record_sleep = False sb.rec_behave = False + sb.rec_sb_mgr = False + sb.rec_sb_cdp = False sb.rec_print = False sb.report_on = False sb.is_pytest = False From 221adbf8349cf0632290be5ec8fd1c135c4b9dac Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:48:24 -0400 Subject: [PATCH 7/8] Refresh Python dependencies --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index e452b9719df..74d65780168 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ setuptools~=70.2;python_version<"3.10" setuptools>=82.0.1;python_version>="3.10" wheel>=0.47.0 attrs>=26.1.0 -certifi>=2026.5.20 +certifi>=2026.6.17 exceptiongroup>=1.3.1 websockets~=15.0.1;python_version<"3.10" websockets>=16.0;python_version>="3.10" @@ -46,7 +46,7 @@ wsproto==1.2.0;python_version<"3.10" wsproto~=1.3.2;python_version>="3.10" websocket-client~=1.9.0 selenium==4.32.0;python_version<"3.10" -selenium==4.44.0;python_version>="3.10" +selenium==4.45.0;python_version>="3.10" cssselect==1.3.0;python_version<"3.10" cssselect>=1.4.0,<2;python_version>="3.10" sortedcontainers==2.4.0 diff --git a/setup.py b/setup.py index 54a8831d2d8..47aa6aaebe3 100755 --- a/setup.py +++ b/setup.py @@ -169,7 +169,7 @@ 'setuptools>=82.0.1;python_version>="3.10"', 'wheel>=0.47.0', 'attrs>=26.1.0', - 'certifi>=2026.5.20', + 'certifi>=2026.6.17', 'exceptiongroup>=1.3.1', 'websockets~=15.0.1;python_version<"3.10"', 'websockets>=16.0;python_version>="3.10"', @@ -210,7 +210,7 @@ 'wsproto~=1.3.2;python_version>="3.10"', 'websocket-client~=1.9.0', 'selenium==4.32.0;python_version<"3.10"', - 'selenium==4.44.0;python_version>="3.10"', + 'selenium==4.45.0;python_version>="3.10"', 'cssselect==1.3.0;python_version<"3.10"', 'cssselect>=1.4.0,<2;python_version>="3.10"', 'sortedcontainers==2.4.0', From a5ac4e8d5b8ae02a591d84c73494e4caaca2c83f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 17 Jun 2026 23:48:48 -0400 Subject: [PATCH 8/8] Version 4.50.0 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index c15c8970393..c869b41c1e1 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.49.14" +__version__ = "4.50.0"