"""
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
)
from seleniumbase.fixtures import shared_utils
import tkinter as tk # noqa: E402
from tkinter import messagebox # noqa: E402
from tkinter.scrolledtext import ScrolledText # noqa: E402
def set_colors(use_colors):
c0 = ""
c1 = ""
c2 = ""
c3 = ""
c4 = ""
c5 = ""
cr = ""
if use_colors:
if (
shared_utils.is_windows()
and hasattr(colorama, "just_fix_windows_console")
):
colorama.just_fix_windows_console()
else:
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("::", ".").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 = []
for test_index, test in enumerate(tests):
full_t.append(test)
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(
"
Summary of existing Case Plans
"
)
full_plan.append("")
full_plan.append("| | |")
full_plan.append("| - | - |")
full_plan.append("| 🔵 | Plans with customized step definitions. |")
full_plan.append("| ⭕ | Plans using default boilerplate code. |")
full_plan.append("| 🚧 | Plans under construction with no table. |")
full_plan.append("")
full_plan.append("--------")
else:
show_no_case_plans_warning()
send_window_to_front(root)
return
full_plan.append("")
full_plan.append("🔎 (Click rows to expand) 🔍
")
full_plan.append("")
full_plan = []
num_ready_cases = 0
num_boilerplate = 0
num_in_progress = 0
for case_index, case_data in enumerate(case_data_storage):
icon = "🔵"
table_missing = False
if "| 1 | Perform Action 1 | Verify Action 1 |" in case_data:
# Still using raw boilerplate code. (Missing real test steps)
icon = "⭕"
if case_data.count("|") < 9 or case_data.count("-") < 3:
# Not enough characters for a minimal Markdown case plan file.
# The dash(es) on line 2, and the Markdown table are required.
# This is what a minimal case plan file might look like:
"""
TEST_ADDRESS
-
| Steps | Results |
| - | - |
| Step1 | Result1 |
"""
icon = "🚧"
table_missing = True
lines = case_data.split("\n")
if len(lines) >= 3 and not table_missing:
first_line = lines[0]
first_line = first_line.strip()
if not (first_line.startswith("``") and first_line.endswith("``")):
first_line = "``%s``" % tests[case_to_test_hash[case_index]]
lines.insert(0, first_line)
else:
first_line = "``%s``" % tests[case_to_test_hash[case_index]]
lines[0] = first_line
lines.insert(0, "")
lines[1] = (
" %s " % icon
+ first_line[2:-2]
+ "
"
)
if (
lines[2].strip().startswith("-")
and lines[2].strip().endswith("-")
):
lines[2] = ""
elif lines[2].strip() != "":
lines.insert(2, "")
if lines[-1].strip() != "":
lines.append("")
lines.append(" ")
full_plan.append("\r\n".join(lines))
else:
# No existing Case Plan found. / File is missing boilerplate.
icon = "🚧"
lines = []
first_line = tests[case_to_test_hash[case_index]]
first_line = "%s %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(
"Summary of existing Case Plans
"
)
plan_head.append("")
plan_head.append("| | | |")
plan_head.append("| - | -: | - |")
plan_head.append("| 🔵 | %s | %s |" % (num_ready_cases, msg_r))
plan_head.append("| ⭕ | %s | %s |" % (num_boilerplate, msg_b))
plan_head.append("| 🚧 | %s | %s |" % (num_in_progress, msg_i))
plan_head.append("")
plan_head.append("--------")
else:
show_no_case_plans_warning()
send_window_to_front(root)
return
plan_head.append("")
plan_head.append("🔎 (Click rows to expand) 🔍
")
plan_head.append("")
for row in full_plan:
plan_head.append(row)
full_plan = plan_head
file_path = "case_summary.md"
file = codecs.open(file_path, "w+", "utf-8")
file.writelines("\r\n".join(full_plan))
file.close()
if num_ready_cases < 10:
msg_ready_cases = " %s" % msg_ready_cases
if num_ready_cases < 100:
msg_ready_cases = " %s" % msg_ready_cases
if num_boilerplate < 10:
msg_boilerplate = " %s" % msg_boilerplate
if num_boilerplate < 100:
msg_boilerplate = " %s" % msg_boilerplate
if num_in_progress < 10:
msg_in_progress = " %s" % msg_in_progress
if num_in_progress < 100:
msg_in_progress = " %s" % msg_in_progress
gen_message = (
'🗂️ Summary generated at "case_summary.md":'
'\n🔵 %s'
'\n⭕ %s'
'\n🚧 %s'
% (msg_ready_cases, msg_boilerplate, msg_in_progress)
)
print(gen_message)
if num_ready_cases < 10:
msg_ready_cases = " %s" % msg_ready_cases
if num_ready_cases < 100:
msg_ready_cases = " %s" % msg_ready_cases
if num_boilerplate < 10:
msg_boilerplate = " %s" % msg_boilerplate
if num_boilerplate < 100:
msg_boilerplate = " %s" % msg_boilerplate
if num_in_progress < 10:
msg_in_progress = " %s" % msg_in_progress
if num_in_progress < 100:
msg_in_progress = " %s" % msg_in_progress
messagebox.showinfo(
"Case Plans Summary generated!",
'\nSummary generated at "case_summary.md"'
'\n🔵 %s'
'\n⭕ %s'
'\n🚧 %s'
% (msg_ready_cases, msg_boilerplate, msg_in_progress)
)
send_window_to_front(root)
def create_tkinter_gui(tests, command_string):
root = tk.Tk()
root.title("SeleniumBase Case Plans Generator")
if shared_utils.is_windows():
root.minsize(820, 618)
else:
root.minsize(820, 652)
tk.Label(root, text="").pack()
run_display = (
"Select from %s tests found: "
"(Boilerplate Case Plans will be generated as needed)"
% len(tests)
)
if len(tests) == 1:
run_display = (
"Select from 1 test found: "
"(Boilerplate Case Plans will be generated as needed)"
)
run_display_2 = "(Tests with existing Case Plans are already checked)"
tk.Label(root, text=run_display, bg="yellow", fg="green").pack()
tk.Label(root, text=run_display_2, bg="yellow", fg="magenta").pack()
text_area = ScrolledText(
root, width=100, height=12, wrap="word", state=tk.DISABLED
)
text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
count = 0
ara = {}
tests_with_case_plan = []
tests_without_case_plan = []
for row in tests:
row += " " * 200
ara[count] = tk.IntVar()
cb = None
if shared_utils.is_windows():
cb = tk.Checkbutton(
text_area,
text=(row),
bg="white",
fg="black",
anchor="w",
pady=0,
borderwidth=1,
highlightthickness=1,
variable=ara[count],
)
else:
cb = tk.Checkbutton(
text_area,
text=(row),
bg="white",
fg="black",
anchor="w",
pady=0,
variable=ara[count],
)
parts = row.strip().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"
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):
cb.select()
tests_with_case_plan.append(row.strip())
else:
tests_without_case_plan.append(row.strip())
text_area.window_create("end", window=cb)
text_area.insert("end", "\n")
count += 1
tk.Label(root, text="").pack()
tk.Button(
root,
text=(
"Generate boilerplate Case Plans "
"for selected tests missing them"),
fg="green",
command=lambda: generate_case_plan_boilerplates(
root,
tests,
ara,
tests_with_case_plan,
tests_without_case_plan,
),
).pack()
tk.Label(root, text="").pack()
try:
tk.Button(
root,
text=("Generate Summary of existing Case Plans"),
fg="teal",
command=lambda: view_summary_of_existing_case_plans(root, tests),
).pack()
except Exception:
tk.Button(
root,
text=("Generate Summary of existing Case Plans"),
fg="green",
command=lambda: view_summary_of_existing_case_plans(root, tests),
).pack()
tk.Label(root, text="\n").pack()
# Bring form window to front
send_window_to_front(root)
# Use decoy to set correct focus on main window
decoy = tk.Tk()
decoy.geometry("1x1")
decoy.iconify()
decoy.update()
decoy.deiconify()
decoy.destroy()
# Start tkinter
root.mainloop()
def main():
use_colors = True
if shared_utils.is_linux():
use_colors = False
c0, c1, c2, c3, c4, c5, cr = set_colors(use_colors)
command_args = sys.argv[2:]
command_string = " ".join(command_args)
message = ""
message += c2
message += "*"
message += c4
message += " Starting the "
message += c0
message += "Selenium"
message += c1
message += "Base"
message += c2
message += " "
message += c3
message += "Case Plans"
message += c4
message += " Generator"
message += c2
message += "..."
message += cr
print(message)
proc = subprocess.Popen(
'"%s" -m pytest --collect-only -q --rootdir="./" %s'
% (sys.executable, command_string),
stdout=subprocess.PIPE,
shell=True,
)
(output, error) = proc.communicate()
if error:
error_msg = "Error collecting tests: %s" % str(error)
error_msg = c5 + error_msg + cr
print(error_msg)
return
tests = []
if shared_utils.is_windows():
output = output.decode("latin1")
else:
output = output.decode("utf-8")
for row in output.replace("\r", "").split("\n"):
if ("::") in row:
tests.append(row)
if not tests:
error_msg = "No tests found! Exiting the Case Plans Generator..."
error_msg = c5 + "ERROR: " + error_msg + cr
print(error_msg)
return
create_tkinter_gui(tests, command_string)
if __name__ == "__main__":
print('To open the Case Plans Generator, type "sbase caseplans"')