diff --git a/examples/case_plans/basic_test.MyTestClass.test_basics.md b/examples/case_plans/basic_test.MyTestClass.test_basics.md
new file mode 100644
index 00000000..e210b558
--- /dev/null
+++ b/examples/case_plans/basic_test.MyTestClass.test_basics.md
@@ -0,0 +1,9 @@
+``basic_test.py::MyTestClass::test_basics``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. |
+| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. |
+| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. |
+| 4 | Remove the ``Backpack`` from the cart. | The ``Backpack`` is no longer in the cart. |
+| 5 | Log out from the website. | Logout was successful. |
diff --git a/examples/case_plans/list_assert_test.MyTestClass.test_assert_list_of_elements.md b/examples/case_plans/list_assert_test.MyTestClass.test_assert_list_of_elements.md
new file mode 100644
index 00000000..ec26b312
--- /dev/null
+++ b/examples/case_plans/list_assert_test.MyTestClass.test_assert_list_of_elements.md
@@ -0,0 +1,8 @@
+``list_assert_test.py::MyTestClass::test_assert_list_of_elements``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Open https://seleniumbase.io/demo_page. | |
+| 2 | Use ``self.assert_elements_present("head", "style", "script")`` to verify that multiple elements are present in the HTML. | The assertion is successful. |
+| 3 | Use ``self.assert_elements("h1", "h2", "h3")`` to verify that multiple elements are visible. | The assertion is successful. |
+| 4 | Use ``self.assert_elements(["#myDropdown", "#myButton", "#svgRect"])`` to verify that multiple elements are visible. | The assertion is successful. |
diff --git a/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md b/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md
new file mode 100644
index 00000000..42700153
--- /dev/null
+++ b/examples/case_plans/my_first_test.MyTestClass.test_swag_labs.md
@@ -0,0 +1,10 @@
+``my_first_test.py::MyTestClass::test_swag_labs``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. |
+| 2 | Click on the ``Backpack`` ``ADD TO CART`` button. | The button text changed to ``REMOVE``. |
+| 3 | Click on the cart icon. | The ``Backpack`` is seen in the cart. |
+| 4 | Click on the ``CHECKOUT`` button.
Enter user details and click ``CONTINUE``. | The ``Backpack`` is seen in the cart on the ``CHECKOUT: OVERVIEW`` page. |
+| 5 | Click on the ``FINISH`` button. | There is a ``Thank You`` message and a ``Pony Express`` delivery logo. |
+| 6 | Log out from the website. | Logout was successful. |
diff --git a/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md b/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md
new file mode 100644
index 00000000..ac674cb1
--- /dev/null
+++ b/examples/case_plans/shadow_root_test.ShadowRootTest.test_shadow_root.md
@@ -0,0 +1,5 @@
+``shadow_root_test.py::ShadowRootTest::test_shadow_root``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Open https://seleniumbase.io/other/shadow_dom.
Click each tab and verify the text contained within the Shadow Root sections. | Tab 1 text: ``Content Panel 1``
Tab 2 text: ``Content Panel 2``
Tab 3 text: ``Content Panel 3`` |
diff --git a/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md b/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md
new file mode 100644
index 00000000..7053790e
--- /dev/null
+++ b/examples/case_plans/test_calculator.CalculatorTests.test_6_times_7_plus_12_equals_54.md
@@ -0,0 +1,5 @@
+``test_calculator.py::CalculatorTests::test_6_times_7_plus_12_equals_54``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Open https://seleniumbase.io/apps/calculator.
Perform the following calculation: ``6 × 7 + 12`` | The output is ``54`` after pressing ``=`` |
diff --git a/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md b/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md
new file mode 100644
index 00000000..b3f19bde
--- /dev/null
+++ b/examples/case_plans/test_demo_site.DemoSiteTests.test_demo_site.md
@@ -0,0 +1,24 @@
+``test_demo_site.py::DemoSiteTests::test_demo_site``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Open https://seleniumbase.io/demo_page | |
+| 2 | Assert the title of the current web page.
Assert that a given element is visible on the page.
Assert that a text substring appears in an element's text. | The assertions were successful. |
+| 3 | Type text into various text fields and then verify. | The assertions were successful. |
+| 4 | Verify that a hover dropdown link changes page text. | The assertion was successful. |
+| 5 | Verify that a button click changes text on the page. | The assertion was successful. |
+| 6 | Verify that an SVG element is located on the page. | The assertion was successful. |
+| 7 | Verify that a slider control updates a progress bar. | The assertion was successful. |
+| 8 | Verify that a "select" option updates a meter bar. | The assertion was successful. |
+| 9 | Assert an element located inside an iFrame. | The assertion was successful. |
+| 10 | Assert text located inside an iFrame. | The assertion was successful. |
+| 11 | Verify that clicking a radio button selects it. | The assertion was successful. |
+| 12 | Verify that clicking an empty checkbox makes it selected. | The assertion was successful. |
+| 13 | Verify clicking on multiple elements with one call. | The assertions were successful. |
+| 14 | Verify that clicking an iFrame checkbox selects it. | The assertions were successful. |
+| 15 | Verify that Drag and Drop works. | The assertion was successful. |
+| 16 | Assert link text. | The assertion was successful. |
+| 17 | Verify clicking on link text. | The action was successful. |
+| 18 | Assert exact text in an element. | The assertion was successful. |
+| 19 | Highlight a page element. | The action was successful. |
+| 20 | Verify that Demo Mode works. | The assertion was successful. |
diff --git a/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md b/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md
new file mode 100644
index 00000000..5cb6c208
--- /dev/null
+++ b/examples/case_plans/test_login.SwagLabsLoginTests.test_swag_labs_login.md
@@ -0,0 +1,6 @@
+``test_login.py::SwagLabsLoginTests::test_swag_labs_login``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Log in to https://www.saucedemo.com with ``standard_user``. | Login was successful. |
+| 2 | Log out from the website. | Logout was successful. |
diff --git a/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md b/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md
new file mode 100644
index 00000000..b637f85f
--- /dev/null
+++ b/examples/case_plans/test_mfa_login.TestMFALogin.test_mfa_login.md
@@ -0,0 +1,7 @@
+``test_mfa_login.py::TestMFALogin::test_mfa_login``
+---
+| # | Step Description | Expected Result |
+| - | ---------------- | --------------- |
+| 1 | Open https://seleniumbase.io/realworld/login
Enter credentials and Sign In. | Sign In was successful. |
+| 2 | Click the ``This Page`` button.
Save a screenshot to the logs. | |
+| 3 | Click to Sign Out | Sign Out was successful. |
diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py
index f38eb352..d937525e 100644
--- a/seleniumbase/console_scripts/run.py
+++ b/seleniumbase/console_scripts/run.py
@@ -11,6 +11,7 @@ sbase methods
sbase options
sbase commander
sbase behave-gui
+sbase caseplans
sbase mkdir ui_tests
sbase mkfile new_test.py
sbase mkrec new_test.py
@@ -83,6 +84,7 @@ def show_basic_usage():
sc += " behave-options (List common behave options)\n"
sc += " gui / commander [OPTIONAL PATH or TEST FILE]\n"
sc += " behave-gui (SBase Commander for Behave)\n"
+ sc += " caseplans [OPTIONAL PATH or TEST FILE]\n"
sc += " mkdir [DIRECTORY] [OPTIONS]\n"
sc += " mkfile [FILE.py] [OPTIONS]\n"
sc += " mkrec / codegen [FILE.py] [OPTIONS]\n"
@@ -200,6 +202,27 @@ def show_behave_gui_usage():
print("")
+def show_caseplans_usage():
+ c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX
+ c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX
+ cr = colorama.Style.RESET_ALL
+ sc = " " + c2 + "** " + c3 + "caseplans" + c2 + " **" + cr
+ print(sc)
+ print("")
+ print(" Usage:")
+ print(" seleniumbase caseplans [OPTIONAL PATH or TEST FILE]")
+ print(" OR: sbase caseplans [OPTIONAL PATH or TEST FILE]")
+ print(" Examples:")
+ print(" sbase caseplans")
+ print(" sbase caseplans -k agent")
+ print(" sbase caseplans -m marker2")
+ print(" sbase caseplans test_suite.py")
+ print(" sbase caseplans offline_examples/")
+ print(" Output:")
+ print(" Launches the SeleniumBase Case Plans Generator.")
+ print("")
+
+
def show_mkdir_usage():
c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX
c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX
@@ -879,6 +902,7 @@ def show_detailed_help():
show_install_usage()
show_commander_usage()
show_behave_gui_usage()
+ show_caseplans_usage()
show_mkdir_usage()
show_mkfile_usage()
show_mkrec_usage()
@@ -932,6 +956,10 @@ def main():
from seleniumbase.console_scripts import sb_behave_gui
sb_behave_gui.main()
+ elif command == "caseplans" or command == "case-plans":
+ from seleniumbase.console_scripts import sb_caseplans
+
+ sb_caseplans.main()
elif (
command == "recorder"
or (command == "record" and len(command_args) == 0)
@@ -1135,6 +1163,14 @@ def main():
print("")
show_behave_gui_usage()
return
+ elif command_args[0] == "caseplans":
+ print("")
+ show_caseplans_usage()
+ return
+ elif command_args[0] == "case-plans":
+ print("")
+ show_caseplans_usage()
+ return
elif command_args[0] == "mkdir":
print("")
show_mkdir_usage()
diff --git a/seleniumbase/console_scripts/sb_caseplans.py b/seleniumbase/console_scripts/sb_caseplans.py
new file mode 100755
index 00000000..0f9c2b91
--- /dev/null
+++ b/seleniumbase/console_scripts/sb_caseplans.py
@@ -0,0 +1,526 @@
+# -*- coding: utf-8 -*-
+"""
+Launches the SeleniumBase Case Plans Generator.
+
+Usage:
+ seleniumbase caseplans [OPTIONAL PATH or TEST FILE]
+ sbase caseplans [OPTIONAL PATH or TEST FILE]
+
+Examples:
+ sbase caseplans
+ sbase caseplans -k agent
+ sbase caseplans -m marker2
+ sbase caseplans test_suite.py
+ sbase caseplans offline_examples/
+
+Output:
+ Launches the SeleniumBase Case Plans Generator.
+"""
+
+import codecs
+import colorama
+import os
+import subprocess
+import sys
+
+if sys.version_info <= (3, 7):
+ current_version = ".".join(str(ver) for ver in sys.version_info[:3])
+ raise Exception(
+ "\n* SBase Case Plans Generator requires Python 3.7 or newer!"
+ "\n** You are currently using Python %s" % current_version
+ )
+import tkinter as tk # noqa: E402
+from tkinter import messagebox # noqa: E402
+from tkinter.scrolledtext import ScrolledText # noqa: E402
+
+is_windows = False
+if sys.platform in ["win32", "win64", "x64"]:
+ is_windows = True
+
+
+def set_colors(use_colors):
+ c0 = ""
+ c1 = ""
+ c2 = ""
+ c3 = ""
+ c4 = ""
+ c5 = ""
+ cr = ""
+ if use_colors:
+ colorama.init(autoreset=True)
+ c0 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX
+ c1 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX
+ c2 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX
+ c3 = colorama.Fore.BLACK + colorama.Back.LIGHTCYAN_EX
+ c4 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX
+ c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX
+ cr = colorama.Style.RESET_ALL
+ return c0, c1, c2, c3, c4, c5, cr
+
+
+def send_window_to_front(root):
+ root.lift()
+ root.attributes("-topmost", True)
+ root.after_idle(root.attributes, "-topmost", False)
+
+
+def show_no_case_plans_warning():
+ messagebox.showwarning(
+ "No existing Case Plans found!",
+ "\nNo existing Case Plans found!!\n\nCreate some boilerplates first!",
+ )
+
+
+def get_test_id(display_id):
+ """The id used in various places such as the test log path."""
+ return display_id.replace(".py::", ".").replace("::", ".")
+
+
+def generate_case_plan_boilerplates(
+ root,
+ tests,
+ selected_tests,
+ tests_with_case_plan,
+ tests_without_case_plan,
+):
+ total_tests = len(tests)
+ total_selected_tests = 0
+ for selected_test in selected_tests:
+ if selected_tests[selected_test].get():
+ total_selected_tests += 1
+
+ test_cases = []
+ case_plans_to_create = []
+ if total_selected_tests == 0:
+ messagebox.showwarning(
+ "No tests were selected!",
+ "\nℹ️ No tests were selected!\nSelect tests for Case Plans!",
+ )
+ send_window_to_front(root)
+ return
+ elif total_tests == total_selected_tests:
+ for test in tests:
+ test_cases.append(test)
+ else:
+ for test_number, test in enumerate(tests):
+ if selected_tests[test_number].get():
+ test_cases.append(test)
+
+ for test_case in test_cases:
+ if (
+ test_case in tests_without_case_plan
+ and test_case not in tests_with_case_plan
+ ):
+ case_plans_to_create.append(test_case)
+
+ new_plans = 0
+ for case_plan in case_plans_to_create:
+ parts = case_plan.split("/")
+ test_address = None
+ folder_path = None
+ if len(parts) == 1:
+ test_address = parts[0]
+ if len(parts) > 1:
+ test_address = parts[-1]
+ folder_path = "/".join(parts[0:-1])
+ test_id = get_test_id(test_address)
+ case_id = test_id + ".md"
+ full_folder_path = None
+ if len(parts) == 1:
+ full_folder_path = "case_plans"
+ if not os.path.exists(full_folder_path):
+ os.makedirs(full_folder_path)
+ else:
+ full_folder_path = os.path.join(folder_path, "case_plans")
+ if not os.path.exists(full_folder_path):
+ os.makedirs(full_folder_path)
+
+ data = []
+ data.append("``%s``" % test_address)
+ data.append("---")
+ data.append("| # | Step Description | Expected Result |")
+ data.append("| - | ---------------- | --------------- |")
+ data.append("| 1 | Perform Action 1 | Verify Action 1 |")
+ data.append("| 2 | Perform Action 2 | Verify Action 2 |")
+ data.append("")
+ file_name = case_id
+ file_path = os.path.join(full_folder_path, file_name)
+ if not os.path.exists(file_path):
+ out_file = codecs.open(file_path, "w+", "utf-8")
+ out_file.writelines("\r\n".join(data))
+ out_file.close()
+ new_plans += 1
+ print("Created %s" % file_path)
+
+ if new_plans == 1:
+ messagebox.showinfo(
+ "A new Case Plan was generated!",
+ '\n✅ %s new boilerplate Case Plan was generated!' % new_plans,
+ )
+ elif new_plans >= 2:
+ messagebox.showinfo(
+ "New Case Plans were generated!",
+ '\n✅ %s new boilerplate Case Plans were generated!' % new_plans,
+ )
+ else:
+ messagebox.showwarning(
+ "No new Case Plans were generated!",
+ "\nℹ️ No new boilerplates were generated!\n\n"
+ "The selected tests already have Case Plans!",
+ )
+ send_window_to_front(root)
+
+
+def view_summary_of_existing_case_plans(root, tests):
+ case_data_storage = []
+ case_to_test_hash = {}
+ full_t = []
+ test_index = -1
+ for test in tests:
+ full_t.append(test)
+ test_index += 1
+ parts = test.strip().split("/")
+ test_address = None
+ folder_path = None
+ if len(parts) == 1:
+ test_address = parts[0]
+ folder_path = "."
+ if len(parts) > 1:
+ test_address = parts[-1]
+ folder_path = "/".join(parts[0:-1])
+ test_id = get_test_id(test_address)
+ case_id = test_id + ".md"
+ case_path = None
+ if len(parts) == 1:
+ case_path = os.path.join("case_plans", case_id)
+ else:
+ case_path = os.path.join(folder_path, "case_plans", case_id)
+ if os.path.exists(case_path):
+ f = open(case_path, "r")
+ case_data = f.read()
+ f.close()
+ case_data_storage.append(case_data)
+ case_to_test_hash[len(case_data_storage) - 1] = test_index
+
+ full_plan = []
+ if len(case_data_storage) > 0:
+ full_plan.append(
+ "
" % icon
+ + first_line[2:-2]
+ + "
%s
" % (icon, first_line)
+ lines.insert(0, first_line)
+ full_plan.append("\r\n".join(lines))
+ full_plan.append("")
+ if icon == "🔵":
+ num_ready_cases += 1
+ elif icon == "⭕":
+ num_boilerplate += 1
+ elif icon == "🚧":
+ num_in_progress += 1
+
+ msg_ready_cases = "%s Case Plans with customized tables" % num_ready_cases
+ if num_ready_cases == 1:
+ msg_ready_cases = "1 Case Plan with a customized table"
+ msg_boilerplate = "%s Case Plans using boilerplate code" % num_boilerplate
+ if num_boilerplate == 1:
+ msg_boilerplate = "1 Case Plan using boilerplate code"
+ msg_in_progress = "%s Case Plans that are missing tables" % num_in_progress
+ if num_in_progress == 1:
+ msg_in_progress = "1 Case Plan that is missing a table"
+
+ msg_r = " ".join(msg_ready_cases.split(" ")[1:])
+ msg_b = " ".join(msg_boilerplate.split(" ")[1:])
+ msg_i = " ".join(msg_in_progress.split(" ")[1:])
+
+ plan_head = []
+ if len(case_data_storage) > 0:
+ plan_head.append(
+ "