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/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
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/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/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"
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.)
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..eb6991e045d 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
@@ -2276,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))
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
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()
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")
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',