diff --git a/examples/unit_tests/test_sb_mkdir.py b/examples/unit_tests/test_sb_mkdir.py new file mode 100644 index 00000000000..a6f7ee52fff --- /dev/null +++ b/examples/unit_tests/test_sb_mkdir.py @@ -0,0 +1,183 @@ +import os +import sys +import tempfile +import shutil +import yaml + + +def test_mkdir_with_gha_flag(): + from seleniumbase.console_scripts import sb_mkdir + import sys as sys_module + + original_argv = sys_module.argv + original_cwd = os.getcwd() + + temp_dir = tempfile.mkdtemp() + try: + os.chdir(temp_dir) + + test_dir = "test_gha_dir" + sys_module.argv = ["sbase", "mkdir", test_dir, "--gha"] + + sb_mkdir.main() + + workflow_file = os.path.join(test_dir, ".github", "workflows", "seleniumbase.yml") + assert os.path.exists(workflow_file), "Workflow file should be created" + + with open(workflow_file, "r") as f: + content = f.read() + + assert "name: SeleniumBase Tests" in content + assert "on:" in content + assert "push:" in content + assert "pull_request:" in content + assert "jobs:" in content + assert "test:" in content + assert "runs-on: ${{ matrix.os }}" in content + assert "strategy:" in content + assert "matrix:" in content + assert "python-version:" in content + assert "browser:" in content + assert "os:" in content + assert "actions/checkout@v3" in content + assert "actions/setup-python@v4" in content + assert "cache: 'pip'" in content + assert "pip install -r requirements.txt" in content + assert "pytest --browser=${{ matrix.browser }} --headless" in content + assert "actions/upload-artifact@v3" in content + assert "latest_logs/**" in content + + workflow_data = yaml.safe_load(content) + assert "on" in workflow_data + assert "jobs" in workflow_data + assert "test" in workflow_data["jobs"] + assert "steps" in workflow_data["jobs"]["test"] + + finally: + os.chdir(original_cwd) + sys_module.argv = original_argv + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + +def test_mkdir_with_gha_custom_params(): + from seleniumbase.console_scripts import sb_mkdir + import sys as sys_module + + original_argv = sys_module.argv + original_cwd = os.getcwd() + + temp_dir = tempfile.mkdtemp() + try: + os.chdir(temp_dir) + + test_dir = "test_gha_custom" + sys_module.argv = [ + "sbase", "mkdir", test_dir, "--gha", + "--gha-browsers=chrome,firefox", + "--gha-python=3.10,3.11", + "--gha-os=ubuntu-latest,windows-latest" + ] + + sb_mkdir.main() + + workflow_file = os.path.join(test_dir, ".github", "workflows", "seleniumbase.yml") + assert os.path.exists(workflow_file), "Workflow file should be created" + + with open(workflow_file, "r") as f: + content = f.read() + + assert '"chrome"' in content + assert '"firefox"' in content + assert '"3.10"' in content + assert '"3.11"' in content + assert '"ubuntu-latest"' in content + assert '"windows-latest"' in content + + workflow_data = yaml.safe_load(content) + matrix = workflow_data["jobs"]["test"]["strategy"]["matrix"] + assert "chrome" in matrix["browser"] + assert "firefox" in matrix["browser"] + assert "3.10" in matrix["python-version"] + assert "3.11" in matrix["python-version"] + assert "ubuntu-latest" in matrix["os"] + assert "windows-latest" in matrix["os"] + + finally: + os.chdir(original_cwd) + sys_module.argv = original_argv + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + +def test_mkdir_with_basic_and_gha(): + from seleniumbase.console_scripts import sb_mkdir + import sys as sys_module + + original_argv = sys_module.argv + original_cwd = os.getcwd() + + temp_dir = tempfile.mkdtemp() + try: + os.chdir(temp_dir) + + test_dir = "test_basic_gha" + sys_module.argv = ["sbase", "mkdir", test_dir, "--basic", "--gha"] + + sb_mkdir.main() + + workflow_file = os.path.join(test_dir, ".github", "workflows", "seleniumbase.yml") + assert os.path.exists(workflow_file), "Workflow file should be created with --basic --gha" + + requirements_file = os.path.join(test_dir, "requirements.txt") + assert os.path.exists(requirements_file), "requirements.txt should exist in basic mode" + + pytest_ini_file = os.path.join(test_dir, "pytest.ini") + assert os.path.exists(pytest_ini_file), "pytest.ini should exist in basic mode" + + test_files_exist = any( + f.endswith("_test.py") or f.startswith("test_") + for f in os.listdir(test_dir) + if os.path.isfile(os.path.join(test_dir, f)) + ) + assert not test_files_exist, "No test files should exist in basic mode" + + finally: + os.chdir(original_cwd) + sys_module.argv = original_argv + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + + +def test_mkdir_gha_workflow_already_exists_error(): + from seleniumbase.console_scripts import sb_mkdir + import sys as sys_module + + original_argv = sys_module.argv + original_cwd = os.getcwd() + + temp_dir = tempfile.mkdtemp() + try: + os.chdir(temp_dir) + + test_dir = "test_gha_exists" + os.makedirs(test_dir) + workflow_dir = os.path.join(test_dir, ".github", "workflows") + os.makedirs(workflow_dir, exist_ok=True) + workflow_file = os.path.join(workflow_dir, "seleniumbase.yml") + with open(workflow_file, "w") as f: + f.write("existing workflow") + + sys_module.argv = ["sbase", "mkdir", test_dir, "--gha"] + try: + sb_mkdir.main() + assert False, "Should raise an error when directory already exists" + except Exception as e: + assert "already exists" in str(e).lower() or "ERROR" in str(e) + + finally: + os.chdir(original_cwd) + sys_module.argv = original_argv + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index b5f41750d02..a926c549d9a 100644 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -50,14 +50,14 @@ COMMANDS:

get / install

-* Usage: +- Usage: ```zsh sbase get [DRIVER] [OPTIONS] sbase install [DRIVER] [OPTIONS] ``` -* Examples: +- Examples: ```zsh sbase get chromedriver @@ -80,7 +80,7 @@ sbase get chs If not set, the driver version matches the browser. `-p` / `--path`: Also copy to "/usr/local/bin".) -* Output: +- Output: Downloads the webdriver to `seleniumbase/drivers/` @@ -90,25 +90,25 @@ Downloads the webdriver to `seleniumbase/drivers/`

methods

-* Usage: +- Usage: ```zsh sbase methods ``` -* Output: +- Output: Displays common SeleniumBase Python methods.

options

-* Usage: +- Usage: ```zsh sbase options ``` -* Output: +- Output: Displays common pytest command-line options that are available when using SeleniumBase. @@ -169,13 +169,13 @@ For the full list of command-line options, type: "pytest --help".

behave-options

-* Usage: +- Usage: ```zsh sbase behave-options ``` -* Output: +- Output: Displays common Behave command-line options that are available when using SeleniumBase. @@ -227,7 +227,7 @@ For the full list of command-line options, type: "behave --help".

gui / commander

-* Usage: +- Usage: ```zsh sbase gui [OPTIONAL PATH or TEST FILE] @@ -236,14 +236,14 @@ sbase commander [OPTIONAL PATH or TEST FILE]

behave-gui

-* Usage: +- Usage: ```zsh sbase behave-gui [OPTIONAL PATH or TEST FILE] sbase gui-behave [OPTIONAL PATH or TEST FILE] ``` -* Examples: +- Examples: ```zsh sbase behave-gui @@ -252,19 +252,19 @@ sbase behave-gui features/ sbase behave-gui features/calculator.feature ``` -* Output: +- Output: Launches SeleniumBase Commander / GUI for Behave.

caseplans

-* Usage: +- Usage: ```zsh sbase caseplans [OPTIONAL PATH or TEST FILE] ``` -* Examples: +- Examples: ```zsh sbase caseplans @@ -274,31 +274,44 @@ sbase caseplans test_suite.py sbase caseplans offline_examples/ ``` -* Output: +- Output: Launches the SeleniumBase Case Plans Generator.

mkdir

-* Usage: +- Usage: ```zsh sbase mkdir [DIRECTORY] [OPTIONS] ``` -* Example: +- Example: ```zsh sbase mkdir ui_tests ``` -* Options: +- Options: ```zsh -b / --basic (Only config files. No tests added.) +--gha / --github-actions (Generate GitHub Actions workflow) +--gha-browsers=BROWSERS (Comma-separated browsers, default: chrome) +--gha-python=VERSIONS (Comma-separated Python versions, default: 3.11) +--gha-os=OS_LIST (Comma-separated OS list, default: ubuntu-latest) ``` -* Output: +- Examples: + +```zsh +sbase mkdir ui_tests +sbase mkdir ui_tests --gha +sbase mkdir ui_tests --gha --gha-browsers=chrome,firefox --gha-python=3.10,3.11,3.12 +sbase mkdir ui_tests --basic --gha +``` + +- Output: Creates a new folder for running SBase scripts. The new folder contains default config files, @@ -342,19 +355,19 @@ ui_tests/

mkfile

-* Usage: +- Usage: ```zsh sbase mkfile [FILE.py] [OPTIONS] ``` -* Example: +- Example: ```zsh sbase mkfile new_test.py ``` -* Options: +- Options: ```zsh --uc (UC Mode boilerplate using SB context manager) @@ -363,7 +376,7 @@ sbase mkfile new_test.py --url=URL (Makes the test start on a specific page) ``` -* Language Options: +- Language Options: ```zsh --en / --English | --zh / --Chinese @@ -373,7 +386,7 @@ sbase mkfile new_test.py --ru / --Russian | --es / --Spanish ``` -* Syntax Formats: +- Syntax Formats: ```zsh --bc / --basecase (BaseCase class inheritance) @@ -384,7 +397,7 @@ sbase mkfile new_test.py --dm / --driver-manager (Driver manager) ``` -* Output: +- Output: Creates a new SBase test file with boilerplate code. If the file already exists, an error is raised. @@ -398,14 +411,14 @@ UC Mode automatically uses English with SB() format.

mkrec / record / codegen

-* Usage: +- Usage: ```zsh sbase mkrec [FILE.py] [OPTIONS] sbase codegen [FILE.py] [OPTIONS] ``` -* Examples: +- Examples: ```zsh sbase mkrec new_test.py @@ -414,7 +427,7 @@ sbase codegen new_test.py sbase codegen new_test.py --url=wikipedia.org ``` -* Options: +- Options: ```zsh --url=URL (Sets the initial start page URL.) @@ -426,45 +439,45 @@ sbase codegen new_test.py --url=wikipedia.org --behave (Also output Behave/Gherkin files.) ``` -* Output: +- Output: Creates a new SeleniumBase test using the Recorder. If the filename already exists, an error is raised.

recorder

-* Usage: +- Usage: ```zsh sbase recorder [OPTIONS] ``` -* Options: +- Options: ```zsh --uc / --undetected (Use undetectable mode.) --behave (Also output Behave/Gherkin files.) ``` -* Output: +- Output: Launches the SeleniumBase Recorder Desktop App.

mkpres

-* Usage: +- Usage: ```zsh sbase mkpres [FILE.py] [LANG] ``` -* Example: +- Example: ```zsh sbase mkpres new_presentation.py --en ``` -* Language Options: +- Language Options: ```zsh --en / --English | --zh / --Chinese @@ -474,7 +487,7 @@ sbase mkpres new_presentation.py --en --ru / --Russian | --es / --Spanish ``` -* Output: +- Output: Creates a new presentation with 3 example slides. If the file already exists, an error is raised. @@ -484,19 +497,19 @@ The slides can be used as a basic boilerplate.

mkchart

-* Usage: +- Usage: ```zsh sbase mkchart [FILE.py] [LANG] ``` -* Example: +- Example: ```zsh sbase mkchart new_chart.py --en ``` -* Language Options: +- Language Options: ```zsh --en / --English | --zh / --Chinese @@ -506,7 +519,7 @@ sbase mkchart new_chart.py --en --ru / --Russian | --es / --Spanish ``` -* Output: +- Output: Creates a new SeleniumBase chart presentation. If the file already exists, an error is raised. @@ -516,32 +529,32 @@ The chart can be used as a basic boilerplate.

print

-* Usage: +- Usage: ```zsh sbase print [FILE] [OPTIONS] ``` -* Options: +- Options: ```zsh -n (Add line Numbers to the rows) ``` -* Output: +- Output: Prints the code/text of any file with syntax-highlighting.

translate

-* Usage: +- Usage: ```zsh sbase translate [SB_FILE.py] [LANGUAGE] [ACTION] ``` -* Languages: +- Languages: ```zsh --en / --English | --zh / --Chinese @@ -551,7 +564,7 @@ sbase translate [SB_FILE.py] [LANGUAGE] [ACTION] --ru / --Russian | --es / --Spanish ``` -* Actions: +- Actions: ```zsh -p / --print (Print translation output to the screen) @@ -559,13 +572,13 @@ sbase translate [SB_FILE.py] [LANGUAGE] [ACTION] -c / --copy (Copy the translation to a new `.py` file) ``` -* Options: +- Options: ```zsh -n (include line Numbers when using the Print action) ``` -* Output: +- Output: Translates a SeleniumBase Python file into the language specified. Method calls and "import" lines get swapped. @@ -579,13 +592,13 @@ plus the 2-letter language code of the new language.

extract-objects

-* Usage: +- Usage: ```zsh sbase extract-objects [SB_FILE.py] ``` -* Output: +- Output: Creates page objects based on selectors found in a seleniumbase Python file and saves those objects to the @@ -593,19 +606,19 @@ seleniumbase Python file and saves those objects to the

inject-objects

-* Usage: +- Usage: ```zsh sbase inject-objects [SB_FILE.py] [OPTIONS] ``` -* Options: +- Options: ```zsh -c / --comments (Add object selectors to the comments.) ``` -* Output: +- Output: Takes the page objects found in the "page_objects.py" file and uses those to replace matching selectors in @@ -613,19 +626,19 @@ the selected seleniumbase Python file.

objectify

-* Usage: +- Usage: ```zsh sbase objectify [SB_FILE.py] [OPTIONS] ``` -* Options: +- Options: ```zsh -c / --comments (Add object selectors to the comments.) ``` -* Output: +- Output: A modified version of the file where the selectors have been replaced with variable names defined in @@ -635,19 +648,19 @@ have been replaced with variable names defined in

revert-objects

-* Usage: +- Usage: ```zsh sbase revert-objects [SB_FILE.py] [OPTIONS] ``` -* Options: +- Options: ```zsh -c / --comments (Keep existing comments for the lines.) ``` -* Output: +- Output: Reverts the changes made by `seleniumbase objectify ...` or `seleniumbase inject-objects ...` when run against a @@ -656,13 +669,13 @@ selectors stored in the "page_objects.py" file.

convert

-* Usage: +- Usage: ```zsh sbase convert [WEBDRIVER_UNITTEST_FILE.py] ``` -* Output: +- Output: Converts a Selenium IDE exported WebDriver unittest file into a SeleniumBase file. Adds `_SB` to the @@ -671,35 +684,35 @@ Works on both Selenium IDE & Katalon Recorder scripts.

encrypt / obfuscate

-* Usage: +- Usage: `sbase encrypt` / `sbase obfuscate` -* Output: +- Output: Runs the password encryption/obfuscation tool. (Where you can enter a password to encrypt/obfuscate.)

decrypt / unobfuscate

-* Usage: +- Usage: `sbase decrypt` / `sbase unobfuscate` -* Output: +- Output: Runs the password decryption/unobfuscation tool. (Where you can enter an encrypted password to decrypt.)

proxy

-* Usage: +- Usage: ```zsh sbase proxy [OPTIONS] ``` -* Options: +- Options: ```zsh --hostname=HOSTNAME (Set `hostname`) (Default: `127.0.0.1`) @@ -707,40 +720,40 @@ sbase proxy [OPTIONS] --help / -h (Display available `proxy` options.) ``` -* Output: +- Output: Launch a basic proxy server on the current machine. (Uses `127.0.0.1:8899` as the default address.)

download

-* Usage: +- Usage: ```zsh sbase download server ``` -* Output: +- Output: Downloads the Selenium Server JAR file for Grid usage. (That JAR file is required when using a Selenium Grid)

grid-hub

-* Usage: +- Usage: ```zsh sbase grid-hub [start|stop|restart] [OPTIONS] ``` -* Options: +- Options: ```zsh -v / --verbose (Increases verbosity of logging output.) --timeout=TIMEOUT (Close idle browser windows after TIMEOUT seconds.) ``` -* Output: +- Output: Controls the Selenium Grid Hub server, which allows for running tests on multiple machines in parallel @@ -750,25 +763,25 @@ You can start, restart, or stop the Grid Hub server.

grid-node

-* Usage: +- Usage: ```zsh sbase grid-node [start|stop|restart] [OPTIONS] ``` -* Options: +- Options: ```zsh --hub=HUB_IP (Grid Hub IP Address. Default: `127.0.0.1`) -v / --verbose (Increases verbosity of logging output.) ``` -* Output: +- Output: Controls the Selenium Grid node, which serves as a worker machine for your Selenium Grid Hub server. You can start, restart, or stop the Grid node. --------- +--- diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index b11305d9846..938f4ff93f2 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -246,8 +246,13 @@ def show_mkdir_usage(): print(" OR: sbase mkdir [DIRECTORY] [OPTIONS]") print(" Example:") print(" sbase mkdir ui_tests") + print(" sbase mkdir ui_tests --gha") print(" Options:") print(" -b / --basic (Only config files. No tests added.)") + print(" --gha / --github-actions (Generate GitHub Actions workflow)") + print(" --gha-browsers=BROWSERS (Comma-separated browsers, default: chrome)") + print(" --gha-python=VERSIONS (Comma-separated Python versions, default: 3.11)") + print(" --gha-os=OS_LIST (Comma-separated OS list, default: ubuntu-latest)") print(" Output:") print(" Creates a new folder for running SBase scripts.") print(" The new folder contains default config files,") diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index 0caa4f01a35..103a99a2a32 100644 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -23,6 +23,62 @@ import sys +def generate_github_actions_workflow(python_versions, browsers, os_versions): + python_list = [v.strip() for v in python_versions.split(",")] + browser_list = [b.strip() for b in browsers.split(",")] + os_list = [o.strip() for o in os_versions.split(",")] + + workflow_lines = [] + workflow_lines.append("name: SeleniumBase Tests") + workflow_lines.append("") + workflow_lines.append("on:") + workflow_lines.append(" push:") + workflow_lines.append(" pull_request:") + workflow_lines.append("") + workflow_lines.append("jobs:") + workflow_lines.append(" test:") + workflow_lines.append(" runs-on: ${{ matrix.os }}") + workflow_lines.append(" strategy:") + workflow_lines.append(" fail-fast: false") + workflow_lines.append(" matrix:") + workflow_lines.append(" python-version:") + for py_ver in python_list: + workflow_lines.append(' - "%s"' % py_ver) + workflow_lines.append(" browser:") + for browser in browser_list: + workflow_lines.append(' - "%s"' % browser) + workflow_lines.append(" os:") + for os_ver in os_list: + workflow_lines.append(' - "%s"' % os_ver) + workflow_lines.append("") + workflow_lines.append(" steps:") + workflow_lines.append(" - uses: actions/checkout@v3") + workflow_lines.append(" - name: Set up Python ${{ matrix.python-version }}") + workflow_lines.append(" uses: actions/setup-python@v4") + workflow_lines.append(" with:") + workflow_lines.append(" python-version: ${{ matrix.python-version }}") + workflow_lines.append(" cache: 'pip'") + workflow_lines.append(" - name: Install dependencies") + workflow_lines.append(" run: |") + workflow_lines.append(" python -m pip install --upgrade pip") + workflow_lines.append(" pip install -r requirements.txt") + workflow_lines.append(" - name: Run tests with ${{ matrix.browser }}") + workflow_lines.append(" run: pytest --browser=${{ matrix.browser }} --headless") + workflow_lines.append(" - name: Upload artifacts on failure") + workflow_lines.append(" if: failure()") + workflow_lines.append(" uses: actions/upload-artifact@v3") + workflow_lines.append(" with:") + workflow_lines.append(" name: test-artifacts-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.browser }}") + workflow_lines.append(" path: |") + workflow_lines.append(" latest_logs/**") + workflow_lines.append(" logs/**") + workflow_lines.append(" archived_logs/**") + workflow_lines.append(" screenshots/**") + workflow_lines.append("") + + return "\n".join(workflow_lines) + + def invalid_run_command(msg=None): exp = " ** mkdir **\n\n" exp += " Usage:\n" @@ -30,8 +86,13 @@ def invalid_run_command(msg=None): exp += " OR sbase mkdir [DIRECTORY] [OPTIONS]\n" exp += " Example:\n" exp += " sbase mkdir ui_tests\n" + exp += " sbase mkdir ui_tests --gha\n" exp += " Options:\n" exp += " -b / --basic (Only config files. No tests added.)\n" + exp += " --gha / --github-actions (Generate GitHub Actions workflow)\n" + exp += " --gha-browsers=BROWSERS (Comma-separated browsers, default: chrome)\n" + exp += " --gha-python=VERSIONS (Comma-separated Python versions, default: 3.11)\n" + exp += " --gha-os=OS_LIST (Comma-separated OS list, default: ubuntu-latest)\n" exp += " Output:\n" exp += " Creates a new folder for running SBase scripts.\n" exp += " The new folder contains default config files,\n" @@ -59,6 +120,10 @@ def main(): cr = colorama.Style.RESET_ALL basic = False + gha = False + gha_browsers = "chrome" + gha_python = "3.11" + gha_os = "ubuntu-latest" help_me = False error_msg = None invalid_cmd = None @@ -84,11 +149,19 @@ def main(): if len(command_args) >= 2: options = command_args[1:] for option in options: - option = option.lower() - if option == "-h" or option == "--help": + option_lower = option.lower() + if option_lower == "-h" or option_lower == "--help": help_me = True - elif option == "-b" or option == "--basic": + elif option_lower == "-b" or option_lower == "--basic": basic = True + elif option_lower == "--gha" or option_lower == "--github-actions": + gha = True + elif option_lower.startswith("--gha-browsers="): + gha_browsers = option[len("--gha-browsers="):] + elif option_lower.startswith("--gha-python="): + gha_python = option[len("--gha-python="):] + elif option_lower.startswith("--gha-os="): + gha_os = option[len("--gha-os="):] else: invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option invalid_cmd = invalid_cmd.replace(">> ", ">>" + c5 + " ") @@ -321,6 +394,23 @@ def main(): file.writelines("\r\n".join(data)) file.close() + if gha: + workflow_dir = "%s/.github/workflows" % dir_name + workflow_file = "%s/seleniumbase.yml" % workflow_dir + if os.path.exists(workflow_file): + error_msg = ( + 'Workflow file "%s" already exists!' % workflow_file + ) + error_msg = c5 + "ERROR: " + error_msg + cr + invalid_run_command(error_msg) + os.makedirs(workflow_dir, exist_ok=True) + workflow_content = generate_github_actions_workflow( + gha_python, gha_browsers, gha_os + ) + file = open(workflow_file, mode="w+", encoding="utf-8") + file.write(workflow_content) + file.close() + if basic: data = [] data.append(" %s/" % dir_name)