From fb4216508c6fd0daec2a8595e58cb072c4feb255 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Dec 2025 00:57:17 -0500 Subject: [PATCH 1/4] Add support for base "Chromium" --- .gitignore | 10 ++ pyproject.toml | 1 + seleniumbase/behave/behave_sb.py | 6 + seleniumbase/console_scripts/sb_install.py | 137 +++++++++++++++++- seleniumbase/console_scripts/sb_mkdir.py | 6 + seleniumbase/console_scripts/sb_mkrec.py | 5 + seleniumbase/console_scripts/sb_recorder.py | 2 + seleniumbase/core/browser_launcher.py | 69 +++++++++ .../drivers/chromium_drivers/__init__.py | 0 seleniumbase/plugins/driver_manager.py | 15 +- seleniumbase/plugins/pytest_plugin.py | 24 ++- seleniumbase/plugins/sb_manager.py | 13 +- seleniumbase/plugins/selenium_plugin.py | 18 ++- .../undetected/cdp_driver/cdp_util.py | 3 + seleniumbase/undetected/cdp_driver/config.py | 56 ++++++- setup.py | 1 + 16 files changed, 349 insertions(+), 17 deletions(-) create mode 100644 seleniumbase/drivers/chromium_drivers/__init__.py diff --git a/.gitignore b/.gitignore index 2892f773a87..af3cf531762 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,16 @@ msedgedriver.exe operadriver.exe uc_driver.exe +# Chromium Zip Files +chrome-mac.zip +chrome-linux.zip +chrome-win.zip + +# Chromium folders +chrome-mac +chrome-linux +chrome-win + # Chrome for Testing Zip Files chrome-mac-arm64.zip chrome-mac-x64.zip diff --git a/pyproject.toml b/pyproject.toml index b846942f3c5..c6dd5408a84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ packages = [ "seleniumbase.drivers.brave_drivers", "seleniumbase.drivers.comet_drivers", "seleniumbase.drivers.atlas_drivers", + "seleniumbase.drivers.chromium_drivers", "seleniumbase.extensions", "seleniumbase.fixtures", "seleniumbase.js_code", diff --git a/seleniumbase/behave/behave_sb.py b/seleniumbase/behave/behave_sb.py index c3f30699de3..240d1452ac3 100644 --- a/seleniumbase/behave/behave_sb.py +++ b/seleniumbase/behave/behave_sb.py @@ -494,6 +494,12 @@ def get_configured_sb(context): sb.binary_location = binary_location sb_config.binary_location = binary_location continue + # Handle: -D use-chromium + if low_key in ["use-chromium"] and not sb_config.binary_location: + binary_location = "_chromium_" + sb.binary_location = binary_location + sb_config.binary_location = binary_location + continue # Handle: -D cft if low_key in ["cft"] and not sb_config.binary_location: binary_location = "cft" diff --git a/seleniumbase/console_scripts/sb_install.py b/seleniumbase/console_scripts/sb_install.py index 4ee154e2411..ea00ce4ece8 100644 --- a/seleniumbase/console_scripts/sb_install.py +++ b/seleniumbase/console_scripts/sb_install.py @@ -19,6 +19,7 @@ sbase get chromedriver stable sbase get chromedriver beta sbase get chromedriver -p + sbase get chromium sbase get cft 131 sbase get chs Output: @@ -46,16 +47,21 @@ from seleniumbase import drivers # webdriver storage folder for SeleniumBase from seleniumbase.drivers import cft_drivers # chrome-for-testing from seleniumbase.drivers import chs_drivers # chrome-headless-shell +from seleniumbase.drivers import chromium_drivers # base chromium urllib3.disable_warnings() ARCH = platform.architecture()[0] IS_ARM_MAC = shared_utils.is_arm_mac() IS_MAC = shared_utils.is_mac() +IS_ARM_LINUX = shared_utils.is_arm_linux() IS_LINUX = shared_utils.is_linux() IS_WINDOWS = shared_utils.is_windows() DRIVER_DIR = os.path.dirname(os.path.realpath(drivers.__file__)) DRIVER_DIR_CFT = os.path.dirname(os.path.realpath(cft_drivers.__file__)) DRIVER_DIR_CHS = os.path.dirname(os.path.realpath(chs_drivers.__file__)) +DRIVER_DIR_CHROMIUM = os.path.dirname( + os.path.realpath(chromium_drivers.__file__) +) LOCAL_PATH = "/usr/local/bin/" # On Mac and Linux systems DEFAULT_CHROMEDRIVER_VERSION = "114.0.5735.90" # (If can't find LATEST_STABLE) DEFAULT_GECKODRIVER_VERSION = "v0.36.0" @@ -203,6 +209,59 @@ def get_cft_latest_version_from_milestone(milestone): return url_request.json()["milestones"][milestone]["version"] +def get_chromium_channel_revision(platform_code, channel): + """Snapshots only exist for revisions where a build occurred. + Therefore, not all found revisions will lead to snapshots.""" + platform_key = None + if platform_code in ["Mac_Arm", "Mac"]: + platform_key = "Mac" + elif platform_code in ["Linux_x64"]: + platform_key = "Linux" + elif platform_code in ["Win_x64"]: + platform_key = "Windows" + elif platform_code in ["Win"]: + platform_key = "Win32" + channel_key = None + if channel.lower() == "stable": + channel_key = "Stable" + elif channel.lower() == "beta": + channel_key = "Beta" + elif channel.lower() == "dev": + channel_key = "Dev" + elif channel.lower() == "canary": + channel_key = "Canary" + base_url = "https://chromiumdash.appspot.com/fetch_releases" + url = f"{base_url}?channel={channel_key}&platform={platform_key}&num=1" + url_request = requests_get_with_retry(url) + data = None + if url_request.ok: + data = url_request.text + else: + raise Exception("Could not determine Chromium revision!") + if data: + try: + import ast + + result = ast.literal_eval(data) + revision = result[0]["chromium_main_branch_position"] + return str(revision) + except Exception: + return get_latest_chromedriver_version(platform_code) + else: + return get_latest_chromedriver_version(platform_code) + + +def get_chromium_latest_revision(platform_code): + base_url = "https://storage.googleapis.com/chromium-browser-snapshots" + url = f"{base_url}/{platform_code}/LAST_CHANGE" + url_request = requests_get_with_retry(url) + if url_request.ok: + latest_revision = url_request.text + else: + raise Exception("Could not determine latest Chromium revision!") + return latest_revision + + def get_latest_chromedriver_version(channel="Stable"): try: if getattr(sb_config, "cft_lkgv_json", None): @@ -274,6 +333,11 @@ def main(override=None, intel_for_uc=None, force_uc=None): elif override.startswith("iedriver "): extra = override.split("iedriver ")[1] sys.argv = ["seleniumbase", "get", "iedriver", extra] + elif override == "chromium": + sys.argv = ["seleniumbase", "get", "chromium"] + elif override.startswith("chromium "): + extra = override.split("chromium ")[1] + sys.argv = ["seleniumbase", "get", "chromium", extra] elif override == "cft": sys.argv = ["seleniumbase", "get", "cft"] elif override.startswith("cft "): @@ -316,6 +380,8 @@ def main(override=None, intel_for_uc=None, force_uc=None): downloads_folder = DRIVER_DIR_CFT elif override == "chs" or name == "chs": downloads_folder = DRIVER_DIR_CHS + elif override == "chromium": + downloads_folder = DRIVER_DIR_CHROMIUM expected_contents = None platform_code = None copy_to_path = False @@ -643,6 +709,31 @@ def main(override=None, intel_for_uc=None, force_uc=None): "https://storage.googleapis.com/chrome-for-testing-public/" "%s/%s/%s" % (use_version, platform_code, file_name) ) + elif name == "chromium": + if IS_MAC: + if IS_ARM_MAC: + platform_code = "Mac_Arm" + else: + platform_code = "Mac" + file_name = "chrome-mac.zip" + elif IS_LINUX: + platform_code = "Linux_x64" + file_name = "chrome-linux.zip" + elif IS_WINDOWS: + if "64" in ARCH: + platform_code = "Win_x64" + else: + platform_code = "Win" + file_name = "chrome-win.zip" + revision = get_chromium_latest_revision(platform_code) + msg = c2 + "Chromium revision to download" + cr + p_version = c3 + revision + cr + log_d("\n*** %s = %s" % (msg, p_version)) + download_url = ( + "https://storage.googleapis.com/chromium-browser-snapshots/" + "%s/%s/%s" % (platform_code, revision, file_name) + ) + downloads_folder = DRIVER_DIR_CHROMIUM elif name == "chrome-headless-shell" or name == "chs": set_version = None found_version = None @@ -1003,12 +1094,12 @@ def main(override=None, intel_for_uc=None, force_uc=None): remote_file = requests_get_with_retry(download_url) with open(file_path, "wb") as file: file.write(remote_file.content) - log_d("%sDownload Complete!%s\n" % (c1, cr)) if file_name.endswith(".zip"): zip_file_path = file_path zip_ref = zipfile.ZipFile(zip_file_path, "r") contents = zip_ref.namelist() + log_d("%sDownload Complete!%s\n" % (c1, cr)) if ( len(contents) >= 1 and name in ["chromedriver", "uc_driver", "geckodriver"] @@ -1249,6 +1340,48 @@ def main(override=None, intel_for_uc=None, force_uc=None): "Chrome for Testing was saved inside:\n%s%s\n%s\n" % (pr_base_path, pr_sep, pr_folder_name) ) + elif name == "chromium": + # Zip file is valid. Proceed. + driver_path = None + driver_file = None + base_path = os.sep.join(zip_file_path.split(os.sep)[:-1]) + folder_name = contents[0].split("/")[0] + folder_path = os.path.join(base_path, folder_name) + if IS_MAC or IS_LINUX: + if ( + "chromium" in folder_path + and "drivers" in folder_path + and os.path.exists(folder_path) + ): + shutil.rmtree(folder_path) + subprocess.run( + ["unzip", zip_file_path, "-d", downloads_folder] + ) + elif IS_WINDOWS: + subprocess.run( + [ + "powershell", + "Expand-Archive", + "-Path", + zip_file_path, + "-DestinationPath", + downloads_folder, + "-Force", + ] + ) + else: + zip_ref.extractall(downloads_folder) + zip_ref.close() + with suppress(Exception): + os.remove(zip_file_path) + log_d("%sUnzip Complete!%s\n" % (c2, cr)) + pr_base_path = c3 + base_path + cr + pr_sep = c3 + os.sep + cr + pr_folder_name = c3 + folder_name + cr + log_d( + "Chromium was saved inside:\n%s%s\n%s\n" + % (pr_base_path, pr_sep, pr_folder_name) + ) elif name == "chrome-headless-shell" or name == "chs": # Zip file is valid. Proceed. driver_path = None @@ -1300,6 +1433,7 @@ def main(override=None, intel_for_uc=None, force_uc=None): tar = tarfile.open(file_path) contents = tar.getnames() if len(contents) == 1: + log_d("%sDownload Complete!%s\n" % (c1, cr)) for f_name in contents: # Remove existing version if exists new_file = os.path.join(downloads_folder, str(f_name)) @@ -1337,6 +1471,7 @@ def main(override=None, intel_for_uc=None, force_uc=None): else: # Not a .zip file or a .tar.gz file. Just a direct download. if "Driver" in file_name or "driver" in file_name: + log_d("%sDownload Complete!%s\n" % (c1, cr)) log_d("Making [%s] executable ..." % file_name) make_executable(file_path) log_d("%s[%s] is now ready for use!%s" % (c1, file_name, cr)) diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index b2e4379aa93..0caa4f01a35 100644 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -246,6 +246,12 @@ def main(): data.append("msedgedriver.exe") data.append("operadriver.exe") data.append("uc_driver.exe") + data.append("chrome-mac.zip") + data.append("chrome-linux.zip") + data.append("chrome-win.zip") + data.append("chrome-mac") + data.append("chrome-linux") + data.append("chrome-win") data.append("chrome-mac-arm64.zip") data.append("chrome-mac-x64.zip") data.append("chrome-linux64.zip") diff --git a/seleniumbase/console_scripts/sb_mkrec.py b/seleniumbase/console_scripts/sb_mkrec.py index d7554cc976e..b00070549f8 100644 --- a/seleniumbase/console_scripts/sb_mkrec.py +++ b/seleniumbase/console_scripts/sb_mkrec.py @@ -88,6 +88,7 @@ def main(): use_brave = False use_comet = False use_atlas = False + use_chromium = False use_uc = False esc_end = False start_page = None @@ -150,6 +151,8 @@ def main(): use_comet = True elif option.lower() == "--atlas": use_atlas = True + elif option.lower() == "--use-chromium": + use_chromium = True elif option.lower() == "--ee": esc_end = True elif option.lower() in ("--gui", "--headed"): @@ -295,6 +298,8 @@ def main(): run_cmd += " --comet" elif use_atlas: run_cmd += " --atlas" + elif use_chromium: + run_cmd += " --use-chromium" if force_gui: run_cmd += " --gui" if use_uc: diff --git a/seleniumbase/console_scripts/sb_recorder.py b/seleniumbase/console_scripts/sb_recorder.py index 3ec6e1d3edd..0cf91179554 100644 --- a/seleniumbase/console_scripts/sb_recorder.py +++ b/seleniumbase/console_scripts/sb_recorder.py @@ -152,6 +152,8 @@ def do_recording(file_name, url, overwrite_enabled, use_chrome, window): command += " --comet" elif "--atlas" in command_args: command += " --atlas" + elif "--use-chromium" in command_args: + command += " --use-chromium" if ( "--uc" in command_args or "--cdp" in command_args diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 24e931a8367..c3607b640e3 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -32,6 +32,7 @@ from seleniumbase.drivers import brave_drivers # still uses chromedriver from seleniumbase.drivers import comet_drivers # still uses chromedriver from seleniumbase.drivers import atlas_drivers # still uses chromedriver +from seleniumbase.drivers import chromium_drivers # still uses chromedriver from seleniumbase import extensions # browser extensions storage folder from seleniumbase.config import settings from seleniumbase.core import detect_b_ver @@ -52,6 +53,9 @@ DRIVER_DIR_BRAVE = os.path.dirname(os.path.realpath(brave_drivers.__file__)) DRIVER_DIR_COMET = os.path.dirname(os.path.realpath(comet_drivers.__file__)) DRIVER_DIR_ATLAS = os.path.dirname(os.path.realpath(atlas_drivers.__file__)) +DRIVER_DIR_CHROMIUM = os.path.dirname( + os.path.realpath(chromium_drivers.__file__) +) # Make sure that the SeleniumBase DRIVER_DIR is at the top of the System PATH # (Changes to the System PATH with os.environ only last during the test run) if not os.environ["PATH"].startswith(DRIVER_DIR): @@ -3057,6 +3061,8 @@ def get_driver( driver_dir = DRIVER_DIR_CFT if getattr(sb_config, "binary_location", None) == "chs": driver_dir = DRIVER_DIR_CHS + if getattr(sb_config, "binary_location", None) == "_chromium_": + driver_dir = DRIVER_DIR_CHROMIUM if _special_binary_exists(binary_location, "opera"): driver_dir = DRIVER_DIR_OPERA sb_config._cdp_browser = "opera" @@ -3120,6 +3126,51 @@ def get_driver( or browser_name == constants.Browser.EDGE ) ): + if ( + binary_location.lower() == "_chromium_" + and browser_name == constants.Browser.GOOGLE_CHROME + ): + binary_folder = None + if IS_MAC: + binary_folder = "chrome-mac" + elif IS_LINUX: + binary_folder = "chrome-linux" + elif IS_WINDOWS: + binary_folder = "chrome-win" + if binary_folder: + binary_location = os.path.join(driver_dir, binary_folder) + if not os.path.exists(binary_location): + from seleniumbase.console_scripts import sb_install + args = " ".join(sys.argv) + if not ( + "-n" in sys.argv or " -n=" in args or args == "-c" + ): + # (Not multithreaded) + sys_args = sys.argv # Save a copy of current sys args + log_d("\nWarning: Chromium binary not found...") + try: + sb_install.main(override="chromium") + except Exception as e: + log_d("\nWarning: Chrome download failed: %s" % e) + sys.argv = sys_args # Put back the original sys args + else: + chrome_fixing_lock = fasteners.InterProcessLock( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + with chrome_fixing_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not os.path.exists(binary_location): + sys_args = sys.argv # Save a copy of sys args + log_d( + "\nWarning: Chromium binary not found..." + ) + sb_install.main(override="chromium") + sys.argv = sys_args # Put back original args + else: + binary_location = None if ( binary_location.lower() == "cft" and browser_name == constants.Browser.GOOGLE_CHROME @@ -3257,12 +3308,25 @@ def get_driver( binary_name = "Google Chrome for Testing" binary_location += "/Google Chrome for Testing.app" binary_location += "/Contents/MacOS/Google Chrome for Testing" + elif binary_name == "Chromium.app": + binary_name = "Chromium" + binary_location += "/Contents/MacOS/Chromium" + elif binary_name in ["chrome-mac"]: + binary_name = "Chromium" + binary_location += "/Chromium.app" + binary_location += "/Contents/MacOS/Chromium" elif binary_name == "chrome-linux64": binary_name = "chrome" binary_location += "/chrome" + elif binary_name == "chrome-linux": + binary_name = "chrome" + binary_location += "/chrome" elif binary_name in ["chrome-win32", "chrome-win64"]: binary_name = "chrome.exe" binary_location += "\\chrome.exe" + elif binary_name in ["chrome-win"]: + binary_name = "chrome.exe" + binary_location += "\\chrome.exe" elif binary_name in [ "chrome-headless-shell-mac-arm64", "chrome-headless-shell-mac-x64", @@ -4034,6 +4098,9 @@ def get_local_driver( downloads_path = DOWNLOADS_FOLDER driver_dir = DRIVER_DIR special_chrome = False + if getattr(sb_config, "binary_location", None) == "_chromium_": + special_chrome = True + driver_dir = DRIVER_DIR_CHROMIUM if getattr(sb_config, "binary_location", None) == "cft": special_chrome = True driver_dir = DRIVER_DIR_CFT @@ -4969,6 +5036,8 @@ def get_local_driver( device_height, device_pixel_ratio, ) + if binary_location and "chromium_drivers" in binary_location: + chrome_options.add_argument("--use-mock-keychain") use_version = "latest" major_chrome_version = None saved_mcv = None diff --git a/seleniumbase/drivers/chromium_drivers/__init__.py b/seleniumbase/drivers/chromium_drivers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seleniumbase/plugins/driver_manager.py b/seleniumbase/plugins/driver_manager.py index fc1cb99781e..d24137f0573 100644 --- a/seleniumbase/plugins/driver_manager.py +++ b/seleniumbase/plugins/driver_manager.py @@ -140,6 +140,7 @@ def Driver( pls=None, # Shortcut / Duplicate of "page_load_strategy". cft=None, # Use "Chrome for Testing" chs=None, # Use "Chrome-Headless-Shell" + use_chromium=None, # Use base "Chromium" ) -> sb_driver.DriverMethods: """ * SeleniumBase Driver as a Python Context Manager or a returnable object. * @@ -665,11 +666,15 @@ def Driver( if arg.startswith("--bl="): binary_location = arg.split("--bl=")[1] break - if cft and not binary_location: + if use_chromium and not binary_location: + binary_location = "_chromium_" + elif cft and not binary_location: binary_location = "cft" elif chs and not binary_location: binary_location = "chs" - if "--cft" in sys_argv and not binary_location: + if "--use-chromium" in sys_argv and not binary_location: + binary_location = "_chromium_" + elif "--cft" in sys_argv and not binary_location: binary_location = "cft" elif "--chs" in sys_argv and not binary_location: binary_location = "chs" @@ -747,7 +752,7 @@ def Driver( and browser not in ["chrome", "opera", "brave", "comet", "atlas"] ): message = ( - '\n Undetected-Chromedriver Mode ONLY supports Chrome!' + '\n Undetected-Chromedriver Mode ONLY supports Chromium browsers!' '\n ("uc=True" / "undetectable=True" / "--uc")' '\n (Your browser choice was: "%s".)' '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) @@ -778,7 +783,9 @@ def Driver( if headless2 and browser == "firefox": headless2 = False # Only for Chromium browsers headless = True # Firefox has regular headless - elif browser not in ["chrome", "edge"]: + elif browser not in [ + "chrome", "edge", "opera", "brave", "comet", "atlas" + ]: headless2 = False # Only for Chromium browsers if disable_csp is None: if ( diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 43ae9890424..cc90f86d913 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -33,6 +33,7 @@ def pytest_addoption(parser): --brave (Shortcut for "--browser=brave".) --comet (Shortcut for "--browser=comet".) --atlas (Shortcut for "--browser=atlas".) + --use-chromium (Shortcut for using base `Chromium`) --cft (Shortcut for using `Chrome for Testing`) --chs (Shortcut for using `Chrome-Headless-Shell`) --settings-file=FILE (Override default SeleniumBase settings.) @@ -219,6 +220,13 @@ def pytest_addoption(parser): default=False, help="""Shortcut for --browser=atlas""", ) + parser.addoption( + "--use-chromium", + action="store_true", + dest="use_chromium", + default=False, + help="""Shortcut for using base `Chromium`""", + ) parser.addoption( "--cft", action="store_true", @@ -1623,7 +1631,7 @@ def pytest_addoption(parser): using_recorder and browser_changes == 1 and browser_text not in [ - "chrome", "edge", "opera", "brave", "comet", "atlas" + "chrome", "edge", "opera", "brave", "comet", "atlas", "chromium" ] ): message = ( @@ -1646,7 +1654,9 @@ def pytest_addoption(parser): undetectable = True if ( browser_changes == 1 - and browser_text not in ["chrome", "opera", "brave", "comet", "atlas"] + and browser_text not in [ + "chrome", "opera", "brave", "comet", "atlas", "chromium" + ] and undetectable ): message = ( @@ -1714,7 +1724,11 @@ def pytest_configure(config): if sb_config.headless2 and sb_config.browser == "firefox": sb_config.headless2 = False # Only for Chromium browsers sb_config.headless = True # Firefox has regular headless - elif sb_config.browser not in ["chrome", "edge"]: + elif ( + sb_config.browser not in [ + "chrome", "edge", "opera", "brave", "comet", "atlas", "chromium" + ] + ): sb_config.headless2 = False # Only for Chromium browsers sb_config.headed = config.getoption("headed") sb_config.xvfb = config.getoption("xvfb") @@ -1760,7 +1774,9 @@ def pytest_configure(config): bin_loc = detect_b_ver.get_binary_location("atlas") if bin_loc and os.path.exists(bin_loc): sb_config.binary_location = bin_loc - if config.getoption("use_cft") and not sb_config.binary_location: + if config.getoption("use_chromium") and not sb_config.binary_location: + sb_config.binary_location = "_chromium_" + elif config.getoption("use_cft") and not sb_config.binary_location: sb_config.binary_location = "cft" elif config.getoption("use_chs") and not sb_config.binary_location: sb_config.binary_location = "chs" diff --git a/seleniumbase/plugins/sb_manager.py b/seleniumbase/plugins/sb_manager.py index c1bb84d2214..aa1abc72806 100644 --- a/seleniumbase/plugins/sb_manager.py +++ b/seleniumbase/plugins/sb_manager.py @@ -123,6 +123,7 @@ def SB( wfa=None, # Shortcut / Duplicate of "wait_for_angularjs". cft=None, # Use "Chrome for Testing" chs=None, # Use "Chrome-Headless-Shell" + use_chromium=None, # Use base "Chromium" save_screenshot=None, # Save a screenshot at the end of each test. no_screenshot=None, # No screenshots saved unless tests directly ask it. page_load_strategy=None, # Set Chrome PLS to "normal", "eager", or "none". @@ -699,11 +700,15 @@ def SB( if arg.startswith("--bl="): binary_location = arg.split("--bl=")[1] break - if cft and not binary_location: + if use_chromium and not binary_location: + binary_location = "_chromium_" + elif cft and not binary_location: binary_location = "cft" elif chs and not binary_location: binary_location = "chs" - if "--cft" in sys_argv and not binary_location: + if "--use-chromium" in sys_argv and not binary_location: + binary_location = "_chromium_" + elif "--cft" in sys_argv and not binary_location: binary_location = "cft" elif "--chs" in sys_argv and not binary_location: binary_location = "chs" @@ -804,7 +809,7 @@ def SB( and browser not in ["chrome", "opera", "brave", "comet", "atlas"] ): message = ( - '\n Undetected-Chromedriver Mode ONLY supports Chrome!' + '\n Undetected-Chromedriver Mode ONLY supports Chromium browsers!' '\n ("uc=True" / "undetectable=True" / "--uc")' '\n (Your browser choice was: "%s".)' '\n (Will use "%s" without UC Mode.)\n' % (browser, browser) @@ -830,7 +835,7 @@ def SB( if headless2 and browser == "firefox": headless2 = False # Only for Chromium browsers headless = True # Firefox has regular headless - elif browser not in ["chrome", "edge"]: + elif browser not in ["chrome", "edge", "opera", "brave", "comet", "atlas"]: headless2 = False # Only for Chromium browsers if not headless and not headless2: headed = True diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index b25f3080262..3ecf7c0af2d 100644 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -22,6 +22,7 @@ class SeleniumBrowser(Plugin): --brave (Shortcut for "--browser=brave".) --comet (Shortcut for "--browser=comet".) --atlas (Shortcut for "--browser=atlas".) + --use-chromium (Shortcut for using base `Chromium`) --cft (Shortcut for using `Chrome for Testing`) --chs (Shortcut for using `Chrome-Headless-Shell`) --user-data-dir=DIR (Set the Chrome user data directory to use.) @@ -180,6 +181,13 @@ def options(self, parser, env): default=False, help="""Shortcut for --browser=atlas""", ) + parser.addoption( + "--use-chromium", + action="store_true", + dest="use_chromium", + default=False, + help="""Shortcut for using base `Chromium`""", + ) parser.addoption( "--cft", action="store_true", @@ -1237,9 +1245,11 @@ def beforeTest(self, test): raise Exception(message) if browser_text: browser = browser_text - if self.options.recorder_mode and browser not in ["chrome", "edge"]: + if self.options.recorder_mode and browser not in [ + "chrome", "edge", "opera", "brave", "comet", "atlas", "chromium" + ]: message = ( - "\n\n Recorder Mode ONLY supports Chrome and Edge!" + "\n\n Recorder Mode ONLY supports Chromium browsers!" '\n (Your browser choice was: "%s")\n' % browser ) raise Exception(message) @@ -1340,7 +1350,9 @@ def beforeTest(self, test): test.test.binary_location = self.options.binary_location if getattr(sb_config, "_cdp_bin_loc", None): test.test.binary_location = sb_config._cdp_bin_loc - if self.options.use_cft and not test.test.binary_location: + if self.options.use_chromium and not test.test.binary_location: + test.test.binary_location = "_chromium_" + elif self.options.use_cft and not test.test.binary_location: test.test.binary_location = "cft" elif self.options.use_chs and not test.test.binary_location: test.test.binary_location = "chs" diff --git a/seleniumbase/undetected/cdp_driver/cdp_util.py b/seleniumbase/undetected/cdp_driver/cdp_util.py index 8b75d26ae69..b13f03ffa19 100644 --- a/seleniumbase/undetected/cdp_driver/cdp_util.py +++ b/seleniumbase/undetected/cdp_driver/cdp_util.py @@ -284,6 +284,7 @@ async def start( mobile: Optional[bool] = None, # Use Mobile Mode with default args disable_csp: Optional[str] = None, # Disable content security policy extension_dir: Optional[str] = None, # Chrome extension directory + use_chromium: Optional[str] = None, # Use the base Chromium browser **kwargs: Optional[dict], ) -> Browser: """ @@ -499,6 +500,8 @@ async def start( print(" Using default Chrome browser instead!") bin_loc = None browser_executable_path = bin_loc + elif use_chromium or "--use-chromium" in arg_join: + browser_executable_path = "_chromium_" if proxy is None and "--proxy" in arg_join: proxy_string = None if "--proxy=" in arg_join: diff --git a/seleniumbase/undetected/cdp_driver/config.py b/seleniumbase/undetected/cdp_driver/config.py index 31a29a1bd13..e80d053e848 100644 --- a/seleniumbase/undetected/cdp_driver/config.py +++ b/seleniumbase/undetected/cdp_driver/config.py @@ -7,6 +7,9 @@ import zipfile from contextlib import suppress from seleniumbase.config import settings +from seleniumbase.drivers import chromium_drivers +from seleniumbase.fixtures import constants +from seleniumbase.fixtures import shared_utils from typing import Union, List, Optional __all__ = [ @@ -23,6 +26,12 @@ PathLike = Union[str, pathlib.Path] AUTO = None +IS_MAC = shared_utils.is_mac() +IS_LINUX = shared_utils.is_linux() +IS_WINDOWS = shared_utils.is_windows() +CHROMIUM_DIR = os.path.dirname( + os.path.realpath(chromium_drivers.__file__) +) class Config: @@ -92,8 +101,51 @@ def __init__( os.makedirs(profile) with open(preferences_file, "w") as f: f.write(preferences) + mock_keychain = False if not browser_executable_path: browser_executable_path = find_chrome_executable() + elif browser_executable_path == "_chromium_": + from filelock import FileLock + binary_folder = None + if IS_MAC: + binary_folder = "chrome-mac" + elif IS_LINUX: + binary_folder = "chrome-linux" + elif IS_WINDOWS: + binary_folder = "chrome-win" + binary_location = os.path.join(CHROMIUM_DIR, binary_folder) + gui_lock = FileLock(constants.MultiBrowser.DRIVER_FIXING_LOCK) + with gui_lock: + with suppress(Exception): + shared_utils.make_writable( + constants.MultiBrowser.DRIVER_FIXING_LOCK + ) + if not os.path.exists(binary_location): + from seleniumbase.console_scripts import sb_install + sys_args = sys.argv # Save a copy of sys args + sb_install.log_d("\nWarning: Chromium binary not found...") + sb_install.main(override="chromium") + sys.argv = sys_args # Put back original args + binary_name = binary_location.split("/")[-1].split("\\")[-1] + if binary_name in ["chrome-mac"]: + binary_name = "Chromium" + binary_location += "/Chromium.app" + binary_location += "/Contents/MacOS/Chromium" + elif binary_name == "chrome-linux": + binary_name = "chrome" + binary_location += "/chrome" + elif binary_name in ["chrome-win"]: + binary_name = "chrome.exe" + binary_location += "\\chrome.exe" + if os.path.exists(binary_location): + mock_keychain = True + browser_executable_path = binary_location + else: + print( + f"{binary_location} not found. " + f"Defaulting to regular Chrome!" + ) + browser_executable_path = find_chrome_executable() self._browser_args = browser_args self.browser_executable_path = browser_executable_path self.headless = headless @@ -135,10 +187,10 @@ def __init__( "--enable-privacy-sandbox-ads-apis", "--safebrowsing-disable-download-protection", '--simulate-outdated-no-au="Tue, 31 Dec 2099 23:59:59 GMT"', - "--password-store=basic", "--deny-permission-prompts", "--disable-application-cache", "--test-type", + "--ash-no-nudges", "--disable-breakpad", "--disable-setuid-sandbox", "--disable-prompt-on-repost", @@ -154,6 +206,8 @@ def __init__( "--disable-renderer-backgrounding", "--disable-dev-shm-usage", ] + if mock_keychain: + self._default_browser_args.append("--use-mock-keychain") @property def browser_args(self): diff --git a/setup.py b/setup.py index 1b6ec808fb4..5d4897de233 100755 --- a/setup.py +++ b/setup.py @@ -329,6 +329,7 @@ "seleniumbase.drivers.brave_drivers", "seleniumbase.drivers.comet_drivers", "seleniumbase.drivers.atlas_drivers", + "seleniumbase.drivers.chromium_drivers", "seleniumbase.extensions", "seleniumbase.fixtures", "seleniumbase.js_code", From add5b55299e1acb1d70556685676201fd266a406 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Dec 2025 00:58:08 -0500 Subject: [PATCH 2/4] Refresh Python dependencies --- mkdocs_build/requirements.txt | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 8e672b613e6..77ae6710435 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -2,7 +2,7 @@ # Minimum Python version: 3.10 (for generating docs only) regex>=2025.11.3 -pymdown-extensions>=10.19 +pymdown-extensions>=10.19.1 pipdeptree>=2.30.0 python-dateutil>=2.8.2 Markdown==3.10 diff --git a/requirements.txt b/requirements.txt index 60227ffd1f3..ea8ff4efc2f 100755 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ certifi>=2025.11.12 exceptiongroup>=1.3.1 websockets>=15.0.1 filelock~=3.19.1;python_version<"3.10" -filelock>=3.20.0;python_version>="3.10" +filelock>=3.20.1;python_version>="3.10" fasteners>=0.20 mycdp>=1.3.2 pynose>=1.5.5 diff --git a/setup.py b/setup.py index 5d4897de233..3bbca50b308 100755 --- a/setup.py +++ b/setup.py @@ -156,7 +156,7 @@ 'exceptiongroup>=1.3.1', 'websockets>=15.0.1', 'filelock~=3.19.1;python_version<"3.10"', - 'filelock>=3.20.0;python_version>="3.10"', + 'filelock>=3.20.1;python_version>="3.10"', 'fasteners>=0.20', 'mycdp>=1.3.2', 'pynose>=1.5.5', From b334f6dcbc717a7d6ea5773e1e29b33d240d5e1a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Dec 2025 00:58:51 -0500 Subject: [PATCH 3/4] Update the docs --- help_docs/customizing_test_runs.md | 16 ++++++++++++++++ seleniumbase/console_scripts/ReadMe.md | 1 + seleniumbase/drivers/ReadMe.md | 8 ++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/help_docs/customizing_test_runs.md b/help_docs/customizing_test_runs.md index dc8d7f56a58..64e89ceb15c 100644 --- a/help_docs/customizing_test_runs.md +++ b/help_docs/customizing_test_runs.md @@ -508,6 +508,20 @@ With the `SB()` and `Driver()` formats, the binary location is set via the `bina -------- +🎛️ To use the special `Chromium` binary: + +```zsh +sbase get chromium +``` + +Then, run scripts with `--use-chromium` / `use_chromium=True`: + +```zsh +pytest --use-chromium -n8 --dashboard --html=report.html -v --rs --headless +``` + +-------- + 🎛️ To use the special `Chrome for Testing` binary: ```zsh @@ -721,6 +735,7 @@ sjw=None # Shortcut / Duplicate of "skip_js_waits". wfa=None # Shortcut / Duplicate of "wait_for_angularjs". cft=None # Use "Chrome for Testing" chs=None # Use "Chrome-Headless-Shell" +use_chromium=None # Use base "Chromium" save_screenshot=None # Save a screenshot at the end of each test. no_screenshot=None # No screenshots saved unless tests directly ask it. page_load_strategy=None # Set Chrome PLS to "normal", "eager", or "none". @@ -816,6 +831,7 @@ server=None # Shortcut / Duplicate of "servername". guest=None # Shortcut / Duplicate of "guest_mode". wire=None # Shortcut / Duplicate of "use_wire". pls=None # Shortcut / Duplicate of "page_load_strategy". +use_chromium=None # Use base "Chromium" cft=None # Use "Chrome for Testing" chs=None # Use "Chrome-Headless-Shell" ``` diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index 8134102a8a1..53c89351c2e 100644 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -68,6 +68,7 @@ sbase get chromedriver 114.0.5735.90 sbase get chromedriver stable sbase get chromedriver beta sbase get chromedriver -p +sbase get chromium sbase get cft 131 sbase get chs ``` diff --git a/seleniumbase/drivers/ReadMe.md b/seleniumbase/drivers/ReadMe.md index afa81c0d230..01fc6a0661d 100644 --- a/seleniumbase/drivers/ReadMe.md +++ b/seleniumbase/drivers/ReadMe.md @@ -39,12 +39,16 @@ sbase get edgedriver 115.0.1901.183 🎛️ Use the `sbase get` command to download the `Chrome for Testing` and `Chrome-Headless-Shell` browser binaries. Example: ```zsh +sbase get chromium # (For base `Chromium`) sbase get cft # (For `Chrome for Testing`) sbase get chs # (For `Chrome-Headless-Shell`) ``` -Those commands download those binaries into the `seleniumbase/drivers` folder. -To use the binaries from there in SeleniumBase scripts, set the `binary_location` to `cft` or `chs`. +Those commands download those binaries into the `seleniumbase/drivers` folder. (There are subfolders, such as `cft_drivers`, `chs_drivers`, and `chromium_drivers`.) + +To use the base `Chromium` binary in SeleniumBase scripts, add `--use-chromium` on the command-line, or set `use_chromium=True` from within scripts. + +To use the `cft` or `chs` binaries in SeleniumBase scripts, set the `binary_location` to `cft` or `chs`, use `--cft` / `--chs` or set `cft=True` / `chs=True`. (Source: https://googlechromelabs.github.io/chrome-for-testing/) From a283dfa711c083581bd54793cc5d6d11035a96d0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 18 Dec 2025 00:59:15 -0500 Subject: [PATCH 4/4] Version 4.45.3 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 6c126ba3490..6e1c962f3c1 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.45.2" +__version__ = "4.45.3"