From a2b6bb77ed8b76faa65da6a2d582616a61b822c1 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 04:58:10 -0400 Subject: [PATCH 01/47] Add the new "sbase mkfile FILE_NAME.py" command --- seleniumbase/console_scripts/run.py | 65 +++++- seleniumbase/console_scripts/sb_mkfile.py | 244 ++++++++++++++++++++++ 2 files changed, 299 insertions(+), 10 deletions(-) create mode 100755 seleniumbase/console_scripts/sb_mkfile.py diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index 698f11abd3f..65fe947a6bf 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -8,6 +8,7 @@ Examples: sbase install chromedriver sbase mkdir browser_tests +sbase mkfile new_test.py sbase convert my_old_webdriver_unittest.py sbase print my_first_test.py -n sbase translate my_first_test.py --zh -p @@ -24,16 +25,6 @@ import colorama import sys -from seleniumbase.common import obfuscate -from seleniumbase.common import unobfuscate -from seleniumbase.console_scripts import logo_helper -from seleniumbase.console_scripts import objectify -from seleniumbase.console_scripts import sb_install -from seleniumbase.console_scripts import sb_mkdir -from seleniumbase.utilities.selenium_grid import download_selenium_server -from seleniumbase.utilities.selenium_grid import grid_hub -from seleniumbase.utilities.selenium_grid import grid_node -from seleniumbase.utilities.selenium_ide import convert_ide def show_usage(): @@ -56,6 +47,7 @@ def show_usage(): def show_basic_usage(): + from seleniumbase.console_scripts import logo_helper seleniumbase_logo = logo_helper.get_seleniumbase_logo() print(seleniumbase_logo) print("%s" % get_version()[0:1]) @@ -136,6 +128,34 @@ def show_mkdir_usage(): print("") +def show_mkfile_usage(): + print(" ** mkfile **") + print("") + print(" Usage:") + print(" seleniumbase mkfile [FILE_NAME.py]") + print(" OR: sbase mkfile [FILE_NAME.py]") + print(" Example:") + print(" seleniumbase mkfile new_test.py") + print(" Options:") + print(" -b / --basic (Basic boilerplate / single-line test)") + print(" Language Options:") + print(" --en / --English | --zh / --Chinese") + print(" --nl / --Dutch | --fr / --French") + print(" --it / --Italian | --ja / --Japanese") + print(" --ko / --Korean | --pt / --Portuguese") + print(" --ru / --Russian | --es / --Spanish") + print(" Output:") + print(" Creates a new SB test file with boilerplate code.") + print(" If the file already exists, an error is raised.") + print(" By default, uses English mode and creates a") + print(" boilerplate with the 5 most common SeleniumBase") + print(' methods, which are "open", "click", "update_text",') + print(' "assert_element", and "assert_text". If using the') + print(' basic boilerplate option, only the "open" method') + print(' is included.') + print("") + + def show_convert_usage(): print(" ** convert **") print("") @@ -359,6 +379,7 @@ def show_detailed_help(): print("") show_install_usage() show_mkdir_usage() + show_mkfile_usage() show_convert_usage() show_print_usage() show_translate_usage() @@ -393,18 +414,28 @@ def main(): if command == "install": if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_install sb_install.main() else: show_basic_usage() show_install_usage() elif command == "mkdir": if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkdir sb_mkdir.main() else: show_basic_usage() show_mkdir_usage() + elif command == "mkfile": + if len(command_args) >= 1: + from seleniumbase.console_scripts import sb_mkfile + sb_mkfile.main() + else: + show_basic_usage() + show_mkfile_usage() elif command == "convert": if len(command_args) == 1: + from seleniumbase.utilities.selenium_ide import convert_ide convert_ide.main() else: show_basic_usage() @@ -442,54 +473,64 @@ def main(): show_translate_usage() elif command == "extract-objects" or command == "extract_objects": if len(command_args) >= 1: + from seleniumbase.console_scripts import objectify objectify.extract_objects() else: show_basic_usage() show_extract_objects_usage() elif command == "inject-objects" or command == "inject_objects": if len(command_args) >= 1: + from seleniumbase.console_scripts import objectify objectify.inject_objects() else: show_basic_usage() show_inject_objects_usage() elif command == "objectify": if len(command_args) >= 1: + from seleniumbase.console_scripts import objectify objectify.objectify() else: show_basic_usage() show_objectify_usage() elif command == "revert-objects" or command == "revert_objects": if len(command_args) >= 1: + from seleniumbase.console_scripts import objectify objectify.revert_objects() else: show_basic_usage() show_revert_objects_usage() elif command == "encrypt" or command == "obfuscate": if len(command_args) >= 0: + from seleniumbase.common import obfuscate obfuscate.main() else: show_basic_usage() show_encrypt_usage() elif command == "decrypt" or command == "unobfuscate": if len(command_args) >= 0: + from seleniumbase.common import unobfuscate unobfuscate.main() else: show_basic_usage() show_decrypt_usage() elif command == "download": if len(command_args) >= 1 and command_args[0].lower() == "server": + from seleniumbase.utilities.selenium_grid import ( + download_selenium_server) download_selenium_server.main(force_download=True) else: show_basic_usage() show_download_usage() elif command == "grid-hub" or command == "grid_hub": if len(command_args) >= 1: + from seleniumbase.utilities.selenium_grid import grid_hub grid_hub.main() else: show_basic_usage() show_grid_hub_usage() elif command == "grid-node" or command == "grid_node": if len(command_args) >= 1: + from seleniumbase.utilities.selenium_grid import grid_node grid_node.main() else: show_basic_usage() @@ -509,6 +550,10 @@ def main(): print("") show_mkdir_usage() return + elif command_args[0] == "mkfile": + print("") + show_mkfile_usage() + return elif command_args[0] == "convert": print("") show_convert_usage() diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py new file mode 100755 index 00000000000..444f1ce43bd --- /dev/null +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" +Creates a new SeleniumBase test file with boilerplate code. + +Usage: +seleniumbase mkfile [FILE_NAME.py] [OPTIONS] +or sbase mkfile [FILE_NAME.py] [OPTIONS] +Output: +Creates a new SeleniumBase test file with boilerplate code. +If the file name already exists, an error will be raised. +""" + +import codecs +import colorama +import os +import sys + + +def invalid_run_command(msg=None): + exp = (" ** mkfile **\n\n") + exp += " Usage:\n" + exp += " seleniumbase mkfile [FILE_NAME.py] [OPTIONS]\n" + exp += " OR sbase mkfile [FILE_NAME.py] [OPTIONS]\n" + exp += " Example:\n" + exp += " sbase mkfile new_test.py\n" + exp += " Options:\n" + exp += " -b / --basic (Basic boilerplate / single-line test)\n" + exp += " Language Options:\n" + exp += " --en / --English | --zh / --Chinese\n" + exp += " --nl / --Dutch | --fr / --French\n" + exp += " --it / --Italian | --ja / --Japanese\n" + exp += " --ko / --Korean | --pt / --Portuguese\n" + exp += " --ru / --Russian | --es / --Spanish\n" + exp += " Output:\n" + exp += " Creates a new SB test file with boilerplate code.\n" + exp += " If the file already exists, an error is raised.\n" + exp += " By default, uses English mode and creates a\n" + exp += " boilerplate with the 5 most common SeleniumBase\n" + exp += ' methods, which are "open", "click", "update_text",\n' + exp += ' "assert_element", and "assert_text". If using the\n' + exp += ' basic boilerplate option, only the "open" method\n' + exp += ' is included.\n' + if not msg: + raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + else: + raise Exception('INVALID RUN COMMAND!\n\n%s\n%s\n' % (exp, msg)) + + +def main(): + colorama.init(autoreset=True) + c1 = colorama.Fore.BLUE + colorama.Back.LIGHTCYAN_EX + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + c7 = colorama.Fore.BLACK + colorama.Back.MAGENTA + cr = colorama.Style.RESET_ALL + basic = False + help_me = False + error_msg = None + invalid_cmd = None + language = "English" + + command_args = sys.argv[2:] + file_name = command_args[0] + if not file_name.endswith(".py"): + error_msg = 'File Name must end with ".py"!' + elif "*" in file_name or len(str(file_name)) < 4: + error_msg = 'Invalid File Name!' + elif "/" in str(file_name) or "\\" in str(file_name): + error_msg = 'File must be created in the current directory!' + elif os.path.exists(os.getcwd() + '/' + file_name): + error_msg = ( + 'File "%s" already exists in the current path!' % file_name) + if error_msg: + error_msg = c5 + error_msg + cr + invalid_run_command(error_msg) + + if len(command_args) >= 2: + options = command_args[1:] + for option in options: + option = option.lower() + if option == "help" or option == "--help": + help_me = True + elif option == "-b" or option == "--basic": + basic = True + elif option == "--en" or option == "--english": + language = "English" + elif option == "--zh" or option == "--chinese": + language = "Chinese" + elif option == "--nl" or option == "--dutch": + language = "Dutch" + elif option == "--fr" or option == "--french": + language = "French" + elif option == "--it" or option == "--italian": + language = "Italian" + elif option == "--ja" or option == "--japanese": + language = "Japanese" + elif option == "--ko" or option == "--korean": + language = "Korean" + elif option == "--pt" or option == "--portuguese": + language = "Portuguese" + elif option == "--ru" or option == "--russian": + language = "Russian" + elif option == "--es" or option == "--spanish": + language = "Spanish" + else: + invalid_cmd = "\n===> INVALID OPTION: >> %s <<\n" % option + invalid_cmd = invalid_cmd.replace('>> ', ">>" + c5 + " ") + invalid_cmd = invalid_cmd.replace(' <<', " " + cr + "<<") + invalid_cmd = invalid_cmd.replace('>>', c7 + ">>" + cr) + invalid_cmd = invalid_cmd.replace('<<', c7 + "<<" + cr) + help_me = True + break + if help_me: + invalid_run_command(invalid_cmd) + + if language != "English" and sys.version_info[0] == 2: + print("") + msg = 'Multi-language support for "sbase mkfile" ' + msg += 'is not available on Python 2!' + msg = "\n" + c5 + msg + cr + msg += '\nPlease run in "English" mode or upgrade to Python 3!\n' + raise Exception(msg) + + dir_name = os.getcwd() + file_path = "%s/%s" % (dir_name, file_name) + + meta = "" + if language != "English": + meta = "" + body = "html > body" + para = "body p" + hello = "Hello" + goodbye = "Goodbye" + class_name = "MyTestClass" + if language == "Chinese": + hello = "你好" + goodbye = "再见" + class_name = "我的测试类" + elif language == "Dutch": + hello = "Hallo" + goodbye = "Dag" + class_name = "MijnTestklasse" + elif language == "French": + hello = "Bonjour" + goodbye = "Au revoir" + class_name = "MaClasseDeTest" + elif language == "Italian": + hello = "Ciao" + goodbye = "Addio" + class_name = "MiaClasseDiTest" + elif language == "Japanese": + hello = "こんにちは" + goodbye = "さようなら" + class_name = "私のテストクラス" + elif language == "Korean": + hello = "여보세요" + goodbye = "안녕" + class_name = "테스트_클래스" + elif language == "Portuguese": + hello = "Olá" + goodbye = "Tchau" + class_name = "MinhaClasseDeTeste" + elif language == "Russian": + hello = "Привет" + goodbye = "До свидания" + class_name = "МойТестовыйКласс" + elif language == "Spanish": + hello = "Hola" + goodbye = "Adiós" + class_name = "MiClaseDePrueba" + url = "" + if basic: + url = "about:blank" + else: + url = "data:text/html,%s

%s
" % (meta, hello) + + import_line = "from seleniumbase import BaseCase" + parent_class = "BaseCase" + class_line = "class MyTestClass(BaseCase):" + if language != "English": + from seleniumbase.translate.master_dict import MD_F + import_line = MD_F.get_import_line(language) + parent_class = MD_F.get_lang_parent_class(language) + class_line = "class %s(%s):" % (class_name, parent_class) + + data = [] + data.append("%s" % import_line) + data.append("") + data.append("") + data.append("%s" % class_line) + data.append("") + data.append(" def test_base(self):") + data.append(' self.open("%s")' % url) + if not basic: + data.append(' self.assert_element("%s") # selector' % body) + data.append(' self.assert_text("%s", "%s")' + ' # text, selector' % (hello, para)) + data.append(' self.update_text("input", "%s")' + ' # selector, text' % goodbye) + data.append(' self.click("%s") # selector' % para) + data.append("") + + new_data = [] + if language == "English": + new_data = data + else: + from seleniumbase.translate.master_dict import MD + from seleniumbase.translate.master_dict import MD_L_Codes + md = MD.md + lang_codes = MD_L_Codes.lang + nl_code = lang_codes[language] + dl_code = lang_codes["English"] + for line in data: + found_swap = False + replace_count = line.count("self.") # Total possible replacements + for key in md.keys(): + original = "self." + md[key][dl_code] + "(" + if original in line: + replacement = "self." + md[key][nl_code] + "(" + new_line = line.replace(original, replacement) + found_swap = True + replace_count -= 1 + if replace_count == 0: + break # Done making replacements + else: + # There might be another method to replace in the line. + # Example: self.assert_true("Name" in self.get_title()) + line = new_line + continue + if found_swap: + if new_line.endswith(" # noqa"): # Remove flake8 skip + new_line = new_line[0:-len(" # noqa")] + new_data.append(new_line) + continue + new_data.append(line) + data = new_data + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() + success = '\n' + c1 + '* File "%s" was created! *' % file_name + cr + '\n' + print(success) + + +if __name__ == "__main__": + invalid_run_command() From 6d7d452636aacf19297d22d755215acabcbb1a30 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 04:59:00 -0400 Subject: [PATCH 02/47] Update the "sbase mkdir DIR_NAME" command --- seleniumbase/console_scripts/sb_mkdir.py | 619 +++++++++++------------ 1 file changed, 308 insertions(+), 311 deletions(-) diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index 0b2c98877cc..4b080e2ed27 100755 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -3,6 +3,7 @@ Usage: seleniumbase mkdir [DIRECTORY_NAME] +OR sbase mkdir [DIRECTORY_NAME] Output: Creates a new folder for running SeleniumBase scripts. The new folder contains default config files, @@ -12,350 +13,346 @@ """ import codecs +import colorama import os import sys -def invalid_run_command(): +def invalid_run_command(msg=None): exp = (" ** mkdir **\n\n") exp += " Usage:\n" exp += " seleniumbase mkdir [DIRECTORY_NAME]\n" + exp += " OR sbase mkdir [DIRECTORY_NAME]\n" exp += " Example:\n" - exp += " seleniumbase mkdir browser_tests\n" + exp += " sbase mkdir browser_tests\n" exp += " Output:\n" exp += " Creates a new folder for running SeleniumBase scripts.\n" exp += " The new folder contains default config files,\n" exp += " sample tests for helping new users get started, and\n" exp += " Python boilerplates for setting up customized\n" exp += " test frameworks.\n" - print("") - raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + if not msg: + raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) + else: + raise Exception('INVALID RUN COMMAND!\n\n%s\n%s\n' % (exp, msg)) def main(): - num_args = len(sys.argv) - if sys.argv[0].split('/')[-1].lower() == "seleniumbase" or ( - sys.argv[0].split('\\')[-1].lower() == "seleniumbase") or ( - sys.argv[0].split('/')[-1].lower() == "sbase") or ( - sys.argv[0].split('\\')[-1].lower() == "sbase"): - if num_args < 3 or num_args > 3: - invalid_run_command() - else: + colorama.init(autoreset=True) + c5 = colorama.Fore.RED + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + error_msg = None + command_args = sys.argv[2:] + if len(command_args) != 1: invalid_run_command() - dir_name = sys.argv[num_args - 1] + dir_name = command_args[0] if len(str(dir_name)) < 2: - raise Exception('Directory name length must be at least 2 ' - 'characters long!') - if os.path.exists(os.getcwd() + '/' + dir_name): - raise Exception('Directory "%s" already exists ' - 'in the current path!\n' % dir_name) - else: - os.mkdir(dir_name) + error_msg = ( + 'Directory name length must be at least 2 characters long!') + elif "/" in str(dir_name) or "\\" in str(dir_name): + error_msg = ( + 'Directory name must not include slashes ("/", "\\")!') + elif os.path.exists(os.getcwd() + '/' + dir_name): + error_msg = ( + 'Directory "%s" already exists in the current path!\n' + '' % dir_name) + if error_msg: + error_msg = c5 + error_msg + cr + invalid_run_command(error_msg) + + os.mkdir(dir_name) - data = [] - data.append("[pytest]") - data.append("addopts = --capture=no --ignore conftest.py " - "-p no:cacheprovider") - 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 = test_*.py *_test.py *_tests.py *_suite.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(" ready: custom marker") - data.append(" master: custom marker") - data.append(" release: custom marker") - data.append(" staging: custom marker") - data.append(" production: custom marker") - data.append("") - file_path = "%s/%s" % (dir_name, "pytest.ini") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("[pytest]") + data.append("addopts = --capture=no --ignore conftest.py " + "-p no:cacheprovider") + 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 = test_*.py *_test.py *_tests.py *_suite.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(" ready: custom marker") + data.append(" master: custom marker") + data.append(" release: custom marker") + data.append(" staging: custom marker") + data.append(" production: custom marker") + data.append("") + file_path = "%s/%s" % (dir_name, "pytest.ini") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("[nosetests]") - data.append("nocapture=1") - data.append("logging-level=INFO") - data.append("") - data.append("[bdist_wheel]") - data.append("universal=1") - file_path = "%s/%s" % (dir_name, "setup.cfg") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("[nosetests]") + data.append("nocapture=1") + data.append("logging-level=INFO") + data.append("") + data.append("[bdist_wheel]") + data.append("universal=1") + file_path = "%s/%s" % (dir_name, "setup.cfg") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("") - data.append("") - data.append("class MyTestClass(BaseCase):") - data.append("") - data.append(" def test_basic(self):") - data.append(' self.open("https://xkcd.com/353/")') - data.append(' self.assert_title("xkcd: Python")') - data.append(" self.assert_element('img[alt=\"Python\"]')") - data.append(" self.click('a[rel=\"license\"]')") - data.append(' self.assert_text("free to copy and reuse")') - data.append(' self.go_back()') - data.append(' self.click("link=About")') - data.append(' self.assert_text("xkcd.com", "h2")') - data.append(' self.open(' - '"://store.xkcd.com/collections/everything")') - data.append( - ' self.update_text("input.search-input", "xkcd book\\n")') - data.append(' self.assert_text("xkcd: volume 0", "h3")') - data.append("") - file_path = "%s/%s" % (dir_name, "my_first_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from seleniumbase import BaseCase") + data.append("") + data.append("") + data.append("class MyTestClass(BaseCase):") + data.append("") + data.append(" def test_basic(self):") + data.append(' self.open("https://xkcd.com/353/")') + data.append(' self.assert_title("xkcd: Python")') + data.append(" self.assert_element('img[alt=\"Python\"]')") + data.append(" self.click('a[rel=\"license\"]')") + data.append(' self.assert_text("free to copy and reuse")') + data.append(' self.go_back()') + data.append(' self.click("link=About")') + data.append(' self.assert_text("xkcd.com", "h2")') + data.append(' self.open(' + '"://store.xkcd.com/collections/everything")') + data.append( + ' self.update_text("input.search-input", "xkcd book\\n")') + data.append(' self.assert_text("xkcd: volume 0", "h3")') + data.append("") + file_path = "%s/%s" % (dir_name, "my_first_test.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("") - data.append("") - data.append("class MyTestClass(BaseCase):") - data.append("") - data.append(" def test_demo_site(self):") - data.append(' self.open(' - '"https://seleniumbase.io/demo_page.html")') - data.append(' self.assert_title("Web Testing Page")') - data.append(' self.assert_element("tbody#tbodyId")') - data.append(' self.assert_text("Demo Page", "h1")') - data.append(' self.update_text("#myTextInput", ' - '"This is Automated")') - data.append(' self.update_text("textarea.area1", ' - '"Testing Time!\\n")') - data.append(" self.update_text('[name=\"preText2\"]', " - "\"Typing Text!\")") - data.append(' self.assert_text("Automation Practice", "h3")') - data.append(' self.hover_and_click("#myDropdown", ' - '"#dropOption2")') - data.append(' self.assert_text("Link Two Selected", "h3")') - data.append(' self.assert_text("This Text is Green", "#pText")') - data.append(' self.click("#myButton")') - data.append(' self.assert_text("This Text is Purple", ' - '"#pText")') - data.append(" self.assert_element('svg[name=\"svgName\"]')") - data.append(" self.assert_element('progress[value=\"50\"]')") - data.append(' self.press_right_arrow("#myslider", times=5)') - data.append(" self.assert_element('progress[value=\"100\"]')") - data.append(" self.assert_element('meter[value=\"0.25\"]')") - data.append(' self.select_option_by_text("#mySelect", ' - '"Set to 75%")') - data.append(" self.assert_element('meter[value=\"0.75\"]')") - data.append(' self.assert_false(self.is_element_visible(' - '"img"))') - data.append(' self.switch_to_frame("#myFrame1")') - data.append(' self.assert_true(self.is_element_visible("img"))') - data.append(' self.switch_to_default_content()') - data.append(' self.assert_false(self.is_text_visible(' - '"iFrame Text"))') - data.append(' self.switch_to_frame("#myFrame2")') - data.append(' self.assert_true(self.is_text_visible(' - '"iFrame Text"))') - data.append(' self.switch_to_default_content()') - data.append(' self.assert_false(self.is_selected(' - '"#radioButton2"))') - data.append(' self.click("#radioButton2")') - data.append(' self.assert_true(self.is_selected(' - '"#radioButton2"))') - data.append(' self.assert_false(self.is_selected(' - '"#checkBox1"))') - data.append(' self.click("#checkBox1")') - data.append(' self.assert_true(self.is_selected("#checkBox1"))') - data.append(' self.assert_false(self.is_selected(' - '"#checkBox2"))') - data.append(' self.assert_false(self.is_selected(' - '"#checkBox3"))') - data.append(' self.assert_false(self.is_selected(' - '"#checkBox4"))') - data.append(' self.click_visible_elements(' - '"input.checkBoxClassB")') - data.append(' self.assert_true(self.is_selected("#checkBox2"))') - data.append(' self.assert_true(self.is_selected("#checkBox3"))') - data.append(' self.assert_true(self.is_selected("#checkBox4"))') - data.append(' self.assert_false(self.is_element_visible(' - '".fBox"))') - data.append(' self.switch_to_frame("#myFrame3")') - data.append(' self.assert_true(self.is_element_visible(' - '".fBox"))') - data.append(' self.assert_false(self.is_selected(".fBox"))') - data.append(' self.click(".fBox")') - data.append(' self.assert_true(self.is_selected(".fBox"))') - data.append(' self.switch_to_default_content()') - data.append(' self.assert_link_text("seleniumbase.com")') - data.append(' self.assert_link_text("SeleniumBase on GitHub")') - data.append(' self.assert_link_text("seleniumbase.io")') - data.append(' self.click_link_text("SeleniumBase Demo Page")') - data.append(' self.assert_exact_text("Demo Page", "h1")') - data.append("") - file_path = "%s/%s" % (dir_name, "test_demo_site.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from seleniumbase import BaseCase") + data.append("") + data.append("") + data.append("class MyTestClass(BaseCase):") + data.append("") + data.append(" def test_demo_site(self):") + data.append(' self.open("https://seleniumbase.io/demo_page.html")') + data.append(' self.assert_title("Web Testing Page")') + data.append(' self.assert_element("tbody#tbodyId")') + data.append(' self.assert_text("Demo Page", "h1")') + data.append(' self.update_text("#myTextInput", ' + '"This is Automated")') + data.append(' self.update_text("textarea.area1", ' + '"Testing Time!\\n")') + data.append(" self.update_text('[name=\"preText2\"]', " + "\"Typing Text!\")") + data.append(' self.assert_text("Automation Practice", "h3")') + data.append(' self.hover_and_click("#myDropdown", "#dropOption2")') + data.append(' self.assert_text("Link Two Selected", "h3")') + data.append(' self.assert_text("This Text is Green", "#pText")') + data.append(' self.click("#myButton")') + data.append(' self.assert_text("This Text is Purple", "#pText")') + data.append(" self.assert_element('svg[name=\"svgName\"]')") + data.append(" self.assert_element('progress[value=\"50\"]')") + data.append(' self.press_right_arrow("#myslider", times=5)') + data.append(" self.assert_element('progress[value=\"100\"]')") + data.append(" self.assert_element('meter[value=\"0.25\"]')") + data.append(' self.select_option_by_text("#mySelect", ' + '"Set to 75%")') + data.append(" self.assert_element('meter[value=\"0.75\"]')") + data.append(' self.assert_false(self.is_element_visible("img"))') + data.append(' self.switch_to_frame("#myFrame1")') + data.append(' self.assert_true(self.is_element_visible("img"))') + data.append(' self.switch_to_default_content()') + data.append(' self.assert_false(self.is_text_visible(' + '"iFrame Text"))') + data.append(' self.switch_to_frame("#myFrame2")') + data.append(' self.assert_true(self.is_text_visible(' + '"iFrame Text"))') + data.append(' self.switch_to_default_content()') + data.append(' self.assert_false(self.is_selected("#radioButton2"))') + data.append(' self.click("#radioButton2")') + data.append(' self.assert_true(self.is_selected("#radioButton2"))') + data.append(' self.assert_false(self.is_selected("#checkBox1"))') + data.append(' self.click("#checkBox1")') + data.append(' self.assert_true(self.is_selected("#checkBox1"))') + data.append(' self.assert_false(self.is_selected("#checkBox2"))') + data.append(' self.assert_false(self.is_selected("#checkBox3"))') + data.append(' self.assert_false(self.is_selected("#checkBox4"))') + data.append(' self.click_visible_elements("input.checkBoxClassB")') + data.append(' self.assert_true(self.is_selected("#checkBox2"))') + data.append(' self.assert_true(self.is_selected("#checkBox3"))') + data.append(' self.assert_true(self.is_selected("#checkBox4"))') + data.append(' self.assert_false(self.is_element_visible(".fBox"))') + data.append(' self.switch_to_frame("#myFrame3")') + data.append(' self.assert_true(self.is_element_visible(".fBox"))') + data.append(' self.assert_false(self.is_selected(".fBox"))') + data.append(' self.click(".fBox")') + data.append(' self.assert_true(self.is_selected(".fBox"))') + data.append(' self.switch_to_default_content()') + data.append(' self.assert_link_text("seleniumbase.com")') + data.append(' self.assert_link_text("SeleniumBase on GitHub")') + data.append(' self.assert_link_text("seleniumbase.io")') + data.append(' self.click_link_text("SeleniumBase Demo Page")') + data.append(' self.assert_exact_text("Demo Page", "h1")') + data.append("") + file_path = "%s/%s" % (dir_name, "test_demo_site.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("from parameterized import parameterized") - data.append("") - data.append("") - data.append("class GoogleTestClass(BaseCase):") - data.append("") - data.append(" @parameterized.expand([") - data.append(' ["pypi", "pypi.org"],') - data.append(' ["wikipedia", "wikipedia.org"],') - data.append(' ["seleniumbase", "seleniumbase/SeleniumBase"],') - data.append(" ])") - data.append(" def test_parameterized_google_search(" - "self, search_term, expected_text):") - data.append(" self.open('https://google.com/ncr')") - data.append(" self.update_text('input[title=\"Search\"]', " - "search_term + '\\n')") - data.append(" self.assert_element('#result-stats')") - data.append(" self.assert_text(expected_text, '#search')") - data.append("") - file_path = "%s/%s" % (dir_name, "parameterized_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from seleniumbase import BaseCase") + data.append("from parameterized import parameterized") + data.append("") + data.append("") + data.append("class GoogleTestClass(BaseCase):") + data.append("") + data.append(" @parameterized.expand([") + data.append(' ["pypi", "pypi.org"],') + data.append(' ["wikipedia", "wikipedia.org"],') + data.append(' ["seleniumbase", "seleniumbase/SeleniumBase"],') + data.append(" ])") + data.append(" def test_parameterized_google_search(" + "self, search_term, expected_text):") + data.append(" self.open('https://google.com/ncr')") + data.append(" self.update_text('input[title=\"Search\"]', " + "search_term + '\\n')") + data.append(" self.assert_element('#result-stats')") + data.append(" self.assert_text(expected_text, '#search')") + data.append("") + file_path = "%s/%s" % (dir_name, "parameterized_test.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - dir_name_2 = dir_name + "/" + "boilerplates" - os.mkdir(dir_name_2) + dir_name_2 = dir_name + "/" + "boilerplates" + os.mkdir(dir_name_2) - data = [] - data.append("") - file_path = "%s/%s" % (dir_name_2, "__init__.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("") + file_path = "%s/%s" % (dir_name_2, "__init__.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("") - data.append("") - data.append("class BaseTestCase(BaseCase):") - data.append("") - data.append(" def setUp(self):") - data.append(" super(BaseTestCase, self).setUp()") - data.append(" # << Add custom code AFTER the super() line >>") - data.append("") - data.append(" def tearDown(self):") - data.append(" self.save_teardown_screenshot()") - data.append(" # << Add custom code BEFORE the super() line >>") - data.append(" super(BaseTestCase, self).tearDown()") - data.append("") - data.append(" def login(self):") - data.append(" # <<< Placeholder. Add your code here. >>>") - data.append(" pass") - data.append("") - data.append(" def example_method(self):") - data.append(" # <<< Placeholder. Add your code here. >>>") - data.append(" pass") - data.append("") - file_path = "%s/%s" % (dir_name_2, "base_test_case.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from seleniumbase import BaseCase") + data.append("") + data.append("") + data.append("class BaseTestCase(BaseCase):") + data.append("") + data.append(" def setUp(self):") + data.append(" super(BaseTestCase, self).setUp()") + data.append(" # << Add custom code AFTER the super() line >>") + data.append("") + data.append(" def tearDown(self):") + data.append(" self.save_teardown_screenshot()") + data.append(" # << Add custom code BEFORE the super() line >>") + data.append(" super(BaseTestCase, self).tearDown()") + data.append("") + data.append(" def login(self):") + data.append(" # <<< Placeholder. Add your code here. >>>") + data.append(" pass") + data.append("") + data.append(" def example_method(self):") + data.append(" # <<< Placeholder. Add your code here. >>>") + data.append(" pass") + data.append("") + file_path = "%s/%s" % (dir_name_2, "base_test_case.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("class Page(object):") - data.append(' html = "html"') - data.append("") - file_path = "%s/%s" % (dir_name_2, "page_objects.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("class Page(object):") + data.append(' html = "html"') + data.append("") + file_path = "%s/%s" % (dir_name_2, "page_objects.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from .base_test_case import BaseTestCase") - data.append("from .page_objects import Page") - data.append("") - data.append("") - data.append("class MyTestClass(BaseTestCase):") - data.append("") - data.append(" def test_boilerplate(self):") - data.append(" self.login()") - data.append(" self.example_method()") - data.append(" self.assert_element(Page.html)") - data.append("") - file_path = "%s/%s" % (dir_name_2, "boilerplate_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from .base_test_case import BaseTestCase") + data.append("from .page_objects import Page") + data.append("") + data.append("") + data.append("class MyTestClass(BaseTestCase):") + data.append("") + data.append(" def test_boilerplate(self):") + data.append(" self.login()") + data.append(" self.example_method()") + data.append(" self.assert_element(Page.html)") + data.append("") + file_path = "%s/%s" % (dir_name_2, "boilerplate_test.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - dir_name_3 = dir_name_2 + "/" + "samples" - os.mkdir(dir_name_3) + dir_name_3 = dir_name_2 + "/" + "samples" + os.mkdir(dir_name_3) - data = [] - data.append("") - file_path = "%s/%s" % (dir_name_3, "__init__.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("") + file_path = "%s/%s" % (dir_name_3, "__init__.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("from seleniumbase import BaseCase") - data.append("from .google_objects import HomePage, ResultsPage") - data.append("") - data.append("") - data.append("class GoogleTests(BaseCase):") - data.append("") - data.append(" def test_google_dot_com(self):") - data.append(" self.open('https://google.com/ncr')") - data.append( - " self.update_text(HomePage.search_box, 'github')") - data.append(" self.assert_element(HomePage.list_box)") - data.append(" self.assert_element(HomePage.search_button)") - data.append( - " self.assert_element(HomePage.feeling_lucky_button)") - data.append(" self.click(HomePage.search_button)") - data.append( - " self.assert_text('github.com', " - "ResultsPage.search_results)") - data.append(" self.assert_element(ResultsPage.images_link)") - data.append("") - file_path = "%s/%s" % (dir_name_3, "google_test.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() + data = [] + data.append("from seleniumbase import BaseCase") + data.append("from .google_objects import HomePage, ResultsPage") + data.append("") + data.append("") + data.append("class GoogleTests(BaseCase):") + data.append("") + data.append(" def test_google_dot_com(self):") + data.append(" self.open('https://google.com/ncr')") + data.append( + " self.update_text(HomePage.search_box, 'github')") + data.append(" self.assert_element(HomePage.list_box)") + data.append(" self.assert_element(HomePage.search_button)") + data.append( + " self.assert_element(HomePage.feeling_lucky_button)") + data.append(" self.click(HomePage.search_button)") + data.append( + " self.assert_text('github.com', " + "ResultsPage.search_results)") + data.append(" self.assert_element(ResultsPage.images_link)") + data.append("") + file_path = "%s/%s" % (dir_name_3, "google_test.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() - data = [] - data.append("class HomePage(object):") - data.append(" dialog_box = '[role=\"dialog\"] div'") - data.append(" search_box = 'input[title=\"Search\"]'") - data.append(" list_box = '[role=\"listbox\"]'") - data.append(" search_button = 'input[value=\"Google Search\"]'") - data.append( - " feeling_lucky_button = " - "'''input[value=\"I'm Feeling Lucky\"]'''") - data.append("") - data.append("") - data.append("class ResultsPage(object):") - data.append(" google_logo = 'img[alt=\"Google\"]'") - data.append(" images_link = 'link=Images'") - data.append(" search_results = 'div#center_col'") - data.append("") - file_path = "%s/%s" % (dir_name_3, "google_objects.py") - file = codecs.open(file_path, "w+", "utf-8") - file.writelines("\r\n".join(data)) - file.close() - print('''\n* Directory "%s" was created with config files ''' - '''and sample tests! *\n''' % dir_name) + data = [] + data.append("class HomePage(object):") + data.append(" dialog_box = '[role=\"dialog\"] div'") + data.append(" search_box = 'input[title=\"Search\"]'") + data.append(" list_box = '[role=\"listbox\"]'") + data.append(" search_button = 'input[value=\"Google Search\"]'") + data.append( + " feeling_lucky_button = " + "'''input[value=\"I'm Feeling Lucky\"]'''") + data.append("") + data.append("") + data.append("class ResultsPage(object):") + data.append(" google_logo = 'img[alt=\"Google\"]'") + data.append(" images_link = 'link=Images'") + data.append(" search_results = 'div#center_col'") + data.append("") + file_path = "%s/%s" % (dir_name_3, "google_objects.py") + file = codecs.open(file_path, "w+", "utf-8") + file.writelines("\r\n".join(data)) + file.close() + print('''\n* Directory "%s" was created with config files ''' + '''and sample tests! *\n''' % dir_name) if __name__ == "__main__": - main() + invalid_run_command() From efdd38bd6e7fc65171ef12b09695e776f8fdeab9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:00:52 -0400 Subject: [PATCH 03/47] Update translations --- seleniumbase/translate/chinese.py | 4 +-- seleniumbase/translate/master_dict.py | 36 ++++++++++++++------------- seleniumbase/translate/translator.py | 1 + 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/seleniumbase/translate/chinese.py b/seleniumbase/translate/chinese.py index 81796d7b0dc..c84e592e161 100755 --- a/seleniumbase/translate/chinese.py +++ b/seleniumbase/translate/chinese.py @@ -362,11 +362,11 @@ def 设置所有属性(self, *args, **kwargs): # set_attributes(selector, attribute, value) return self.set_attributes(*args, **kwargs) - def 输入文字(self, *args, **kwargs): + def 输入文本(self, *args, **kwargs): # input(selector, new_value) # Same as update_text() return self.input(*args, **kwargs) - def 写文字(self, *args, **kwargs): + def 写文本(self, *args, **kwargs): # write(selector, new_value) # Same as update_text() return self.write(*args, **kwargs) diff --git a/seleniumbase/translate/master_dict.py b/seleniumbase/translate/master_dict.py index 0daa8ee63b3..f62a4fc6144 100755 --- a/seleniumbase/translate/master_dict.py +++ b/seleniumbase/translate/master_dict.py @@ -1,17 +1,19 @@ - -# Master Dictionary - -# Translations -# 0: English -# 1: Chinese -# 2: Dutch -# 3: French -# 4: Italian -# 5: Japanese -# 6: Korean -# 7: Portuguese -# 8: Russian -# 9: Spanish +# -*- coding: utf-8 -*- +""" +Master Dictionary + +Translations +0: English +1: Chinese +2: Dutch +3: French +4: Italian +5: Japanese +6: Korean +7: Portuguese +8: Russian +9: Spanish +""" class MD_F: @@ -1308,7 +1310,7 @@ class MD: md["input"] = ["*"] * num_langs md["input"][0] = "input" - md["input"][1] = "输入文字" + md["input"][1] = "输入文本" md["input"][2] = "voer" md["input"][3] = "taper" md["input"][4] = "digitare" @@ -1320,7 +1322,7 @@ class MD: md["write"] = ["*"] * num_langs md["write"][0] = "write" - md["write"][1] = "写文字" + md["write"][1] = "写文本" md["write"][2] = "schrijven" md["write"][3] = "écriver" md["write"][4] = "scrivere" @@ -1480,7 +1482,7 @@ class MD: # "type" -> duplicate of "input" md["type"] = ["*"] * num_langs md["type"][0] = "type" - md["type"][1] = "输入文字" + md["type"][1] = "输入文本" md["type"][2] = "voer" md["type"][3] = "taper" md["type"][4] = "digitare" diff --git a/seleniumbase/translate/translator.py b/seleniumbase/translate/translator.py index 77738a790f2..b31f82147cb 100755 --- a/seleniumbase/translate/translator.py +++ b/seleniumbase/translate/translator.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Translates a SeleniumBase Python file into a different language From 041952ba351c0939ac67652a1ab9e429ab3c7e2c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:01:40 -0400 Subject: [PATCH 04/47] Update Messenger asserts --- seleniumbase/fixtures/base_case.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index e76fb1cf3c1..b186b757958 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -3823,7 +3823,7 @@ def assert_element(self, selector, by=By.CSS_SELECTOR, timeout=None): if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = "ASSERT %s: %s" % (by, selector) + messenger_post = "ASSERT %s: {%s}" % (by, selector) self.__highlight_with_assert_success(messenger_post, selector, by) return True @@ -3903,7 +3903,7 @@ def assert_text(self, text, selector="html", by=By.CSS_SELECTOR, if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = ("ASSERT TEXT {%s} in %s: %s" + messenger_post = ("ASSERT TEXT {%s} in %s: {%s}" % (text, by, selector)) self.__highlight_with_assert_success(messenger_post, selector, by) return True @@ -3924,7 +3924,7 @@ def assert_exact_text(self, text, selector="html", by=By.CSS_SELECTOR, if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = ("ASSERT EXACT TEXT {%s} in %s: %s" + messenger_post = ("ASSERT EXACT TEXT {%s} in %s: {%s}" % (text, by, selector)) self.__highlight_with_assert_success(messenger_post, selector, by) return True From 45c48f9e26f1b171ea28ab15cd3906d9f7bde097 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:03:22 -0400 Subject: [PATCH 05/47] Add "--rs" as a simplified command arg for "--reuse-session" --- seleniumbase/plugins/pytest_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 2d9c0d7f5c2..d7112f91ef3 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -385,7 +385,7 @@ def pytest_addoption(parser): dest='devtools', default=False, help="""Using this opens Chrome's DevTools.""") - parser.addoption('--reuse_session', '--reuse-session', + parser.addoption('--rs', '--reuse_session', '--reuse-session', action="store_true", dest='reuse_session', default=False, From f6f4463ca9b4081766711715d8316a55ae13b858 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:03:52 -0400 Subject: [PATCH 06/47] Update the docs --- README.md | 10 +++++----- docs/img/sb_logo_10.png | Bin 0 -> 60100 bytes 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 docs/img/sb_logo_10.png diff --git a/README.md b/README.md index 964ae2d5ef7..fc167708c51 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ - - - + + +

-SeleniumBase +SeleniumBase

Everything you need to test websites. @@ -68,7 +68,7 @@ python setup.py install ``` If multiple versions of Python are installed, be specific (E.g. use ``python3`` instead of ``python``). -* You can also install ``seleniumbase`` from [pypi](https://pypi.python.org/pypi/seleniumbase): +* You can also install ``seleniumbase`` from [pypi](https://pypi.python.org/pypi/seleniumbase). ```bash pip install seleniumbase ``` diff --git a/docs/img/sb_logo_10.png b/docs/img/sb_logo_10.png new file mode 100644 index 0000000000000000000000000000000000000000..8e8d92f7237d22b0ff1c3db07e8e22f763a4872f GIT binary patch literal 60100 zcmd43IDsaf-W^wn&g-#Y^!PN?Y8$xI=Mu4`k=ha_^8zhiGgwAhqE1X)@AUF9g*7-NF5Iq39^hN(I>S3-#@g8{XhRz_dSF7 zs+X#%Z$zdzo?4J%{D*eKRzl$VI4!?K2eomh#xx!$!By)_>cwR~h#Ws9L+(TBm-wi8 z3Cj>@_h=RmWUhX<`d5Yh(rc5D7(w&9QJ-PhjW`*0F zZm)&%lS!J)b$ZLg*Q(?kxBp@OpXp}-eiz3VD6wS!7EQGPf)AbBt3T)eekp_@JE)G_ zuUhk8XDYjf=u`XrT#w3MjeniAZFK(-eC+}2y}A~78hBr7xCx#ii0_exI=U>grnEWJ z5hZ*8G*PB7f&S_FMXGG~+3hb6%mkWB9;Fr2f~OOubr0qlwwH$C==b*R%Kq1q{((sT z$ZTLtaLZY8bD{J{jsiupxexi0Gr@0)c) ztyY{(xXZ0gsgD|enR0bBe7FN~tnVM}y~3rv0!(*0Yx=tmnwuorzE0~&dNXD`>B`M? zoaB3@;Vs5aAY1Q}g^A?fa(`L?^4xPrNXZ@xQ@!n)EeR?C=}9)a*Hm*{=Qera_Y!_o zSIWDYxx)(Y%BtHG+y4!Z0^wXRQgf^fZd$!1@E0lNGiPchH0=PU(^sjRIrEDVVnvD+ znc9gDd;g*g!+2ldE$j8jW8%Z3({hYU0kti<%58x=g5>vM4NV^}Ke-pH5gu=?O8CH! zZe_eCL+ifBTou4R^BVXVJoehb4j~u3CJ$T8tnkpX-RXw-0*_Ysh5MFZ#iOK8^QJme zHifz|?~Jgu0Y_TS+Q>H@n9pCH$73!NaR;S6y`!k^cjuEKeryyq3dKK}xP`FJF2P^h zMTC0(9I8JY;?JXgb?>5Lr@@q17Bnz|vcV>_38Z{(261GrL6w-Ebb3;!?^Jyn1y*_F{<5>y5&BIo8Fyyt0t1_Df8k?u|@bb~K{Pl2$=9D{Q1V5*{5EZdjq^ADrJ`S5MB4-@-%j*2;5u0T&6x$e9rLD`*LX<@&;$z- z>KdO29;GR2KHnOi@kFxQjSSFd?7)Rc+;B?YNS<7RS5u&QQ(R4LE#4}PY9w0@ZpW9s z(mXkXdSiOaWR+zMEe)5y?@M$lkpsCMy$Af>xhNdVu*Br_=ljvy?f8118FkZu3VGxr z`;#u^7hUvcMH>kN=Q82KjcQL?m3{xDWTrjIV%#z-ZJ_SEZ3PpuGJv7CYL^8A95?L5 zSHc+jaVr5zz0%CR^HZK7oQ`iUSwk0Vtcj_A;@dpa@v^RnS_X8?53+8N$%Py4^-k_g zu!|=veC;@kfU3%!K01OK{N{r^z;|Ke=m^DYRs^35r>FJf_fj14Xu-`?=SX47s;2ee zFVUQRC4d3CGpf_PH!4qB>AxxC2LYes8-Hn^f8PUFBTx7snxSbkcURMWcMx-X;pYLA zngy5Vnu>cg9bku_-(0Padwr-oh;G#zzjdRl5#buF^fimsmiS1yMk)qw_u@p}{9$_i z;)6^rk*~7`bq9HB=oU4c+@9AQrTK6@n0>f1?`_!!_&>MWH#szn^yAur0D?fr<5{J6 z@~c1BY}>O3D#;PoY7)L0eRo9iN3(3|R+WJ>ikosOuPBS&%Q=(yw@GI|M@tlI65Dp& zatTT=NXn~y*-IKh-zXXT()l7(i#_uCfyCtDYOc8*E-CqheI3b7)L z(sd*ccyfZWl%1EOGO`P)rx)fz>pjs@GI;-3S{fECTp+=bRDg-JoAX4N7r)5m`qm}u z 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 B?J)oV literal 0 HcmV?d00001 From dbdc6b0ac3bbec707adf30f63133a8ddfc8b1196 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:04:27 -0400 Subject: [PATCH 07/47] Update flake8 version --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index cae97abfad7..8543638e08d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ boto==2.49.0 cffi==1.14.0 rich==2.0.0;python_version>="3.6" and python_version<"4.0" flake8==3.7.9;python_version<"3.5" -flake8==3.8.2;python_version>="3.5" +flake8==3.8.3;python_version>="3.5" pyflakes==2.1.1;python_version<"3.5" pyflakes==2.2.0;python_version>="3.5" certifi>=2020.4.5.2 diff --git a/setup.py b/setup.py index 0de852c6579..7cb462caf1d 100755 --- a/setup.py +++ b/setup.py @@ -135,7 +135,7 @@ 'cffi==1.14.0', 'rich==2.0.0;python_version>="3.6" and python_version<"4.0"', 'flake8==3.7.9;python_version<"3.5"', - 'flake8==3.8.2;python_version>="3.5"', + 'flake8==3.8.3;python_version>="3.5"', 'pyflakes==2.1.1;python_version<"3.5"', 'pyflakes==2.2.0;python_version>="3.5"', 'certifi>=2020.4.5.2', From 9d08988ceaabb09bc9f843d78a02ba188f3e4b60 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 05:05:59 -0400 Subject: [PATCH 08/47] Version 1.40.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7cb462caf1d..3d9d60a49e9 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ setup( name='seleniumbase', - version='1.39.5', + version='1.40.0', description='Fast, Easy, and Reliable Browser Automation & Testing.', long_description=long_description, long_description_content_type='text/markdown', From 74d26ba91719f236e2aad96bd7fb3468fe63def3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 11:29:49 -0400 Subject: [PATCH 09/47] Update "rich" dependency to v2.0.1 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8543638e08d..43c256138c1 100755 --- a/requirements.txt +++ b/requirements.txt @@ -41,7 +41,7 @@ coverage==5.1 pyotp==2.3.0 boto==2.49.0 cffi==1.14.0 -rich==2.0.0;python_version>="3.6" and python_version<"4.0" +rich==2.0.1;python_version>="3.6" and python_version<"4.0" flake8==3.7.9;python_version<"3.5" flake8==3.8.3;python_version>="3.5" pyflakes==2.1.1;python_version<"3.5" diff --git a/setup.py b/setup.py index 3d9d60a49e9..47ba6e11546 100755 --- a/setup.py +++ b/setup.py @@ -133,7 +133,7 @@ 'pyotp==2.3.0', 'boto==2.49.0', 'cffi==1.14.0', - 'rich==2.0.0;python_version>="3.6" and python_version<"4.0"', + 'rich==2.0.1;python_version>="3.6" and python_version<"4.0"', 'flake8==3.7.9;python_version<"3.5"', 'flake8==3.8.3;python_version>="3.5"', 'pyflakes==2.1.1;python_version<"3.5"', From 5b1889cef2c8d8be9deb0559f2f4ad5cddefd91f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 12:51:01 -0400 Subject: [PATCH 10/47] Update sb_mkfile --- seleniumbase/console_scripts/sb_mkfile.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py index 444f1ce43bd..0fa107c1459 100755 --- a/seleniumbase/console_scripts/sb_mkfile.py +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -123,9 +123,6 @@ def main(): dir_name = os.getcwd() file_path = "%s/%s" % (dir_name, file_name) - meta = "" - if language != "English": - meta = "" body = "html > body" para = "body p" hello = "Hello" @@ -170,8 +167,10 @@ def main(): url = "" if basic: url = "about:blank" + elif language not in ["English", "Dutch", "French", "Italian"]: + url = "data:text/html,

%s " % hello else: - url = "data:text/html,%s

%s
" % (meta, hello) + url = "data:text/html,

%s

" % hello import_line = "from seleniumbase import BaseCase" parent_class = "BaseCase" From 9be50da12c15f09a4f90da864df27044c50f79e7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 13:58:30 -0400 Subject: [PATCH 11/47] Refresh proxy list --- seleniumbase/config/proxy_list.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/seleniumbase/config/proxy_list.py b/seleniumbase/config/proxy_list.py index 213c0f97707..3835f52538c 100755 --- a/seleniumbase/config/proxy_list.py +++ b/seleniumbase/config/proxy_list.py @@ -20,13 +20,10 @@ """ PROXY_LIST = { - "example1": "142.93.130.169:8118", # (Example) - set your own proxy here - "example2": "51.91.212.159:3128", # (Example) - set your own proxy here - "example3": "149.129.238.254:3128", # (Example) - set your own proxy here - "example4": "82.200.233.4:3128", # (Example) - set your own proxy here - "example5": "46.218.155.194:3128", # (Example) - set your own proxy here - "example6": "45.77.222.251:3128", # (Example) - set your own proxy here - "example7": "51.178.220.168:3128", # (Example) - set your own proxy here + "example1": "104.154.143.77:3128", # (Example) - set your own proxy here + "example2": "105.112.8.53:3128", # (Example) - set your own proxy here + "example3": "82.200.233.4:3128", # (Example) - set your own proxy here + "example4": "176.53.40.222:3128", # (Example) - set your own proxy here "proxy1": None, "proxy2": None, "proxy3": None, From 1788d1648ea34ee1ffb3dd14438f59890e790409 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 23:57:24 -0400 Subject: [PATCH 12/47] Update sb_mkfile --- seleniumbase/console_scripts/sb_mkfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py index 0fa107c1459..c650da95d0c 100755 --- a/seleniumbase/console_scripts/sb_mkfile.py +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -235,7 +235,9 @@ def main(): file = codecs.open(file_path, "w+", "utf-8") file.writelines("\r\n".join(data)) file.close() - success = '\n' + c1 + '* File "%s" was created! *' % file_name + cr + '\n' + success = ( + '\n' + c1 + '* Test file: "' + file_name + '" was created! *' + '' + cr + '\n') print(success) From 3b7a6a790175ac9b21f6eb0e5ebeba7d6b3ecfce Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 23:58:39 -0400 Subject: [PATCH 13/47] Set the language in the __init__ --- seleniumbase/translate/chinese.py | 4 ++++ seleniumbase/translate/dutch.py | 4 ++++ seleniumbase/translate/french.py | 4 ++++ seleniumbase/translate/italian.py | 4 ++++ seleniumbase/translate/japanese.py | 4 ++++ seleniumbase/translate/korean.py | 4 ++++ seleniumbase/translate/portuguese.py | 4 ++++ seleniumbase/translate/russian.py | 4 ++++ seleniumbase/translate/spanish.py | 4 ++++ 9 files changed, 36 insertions(+) diff --git a/seleniumbase/translate/chinese.py b/seleniumbase/translate/chinese.py index c84e592e161..039b26c95d9 100755 --- a/seleniumbase/translate/chinese.py +++ b/seleniumbase/translate/chinese.py @@ -5,6 +5,10 @@ class 硒测试用例(BaseCase): # noqa + def __init__(self, *args, **kwargs): + super(硒测试用例, self).__init__(*args, **kwargs) + self._language = "Chinese" + def 开启(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/dutch.py b/seleniumbase/translate/dutch.py index 9245d10f704..d4dfdbbd1ad 100755 --- a/seleniumbase/translate/dutch.py +++ b/seleniumbase/translate/dutch.py @@ -5,6 +5,10 @@ class Testgeval(BaseCase): + def __init__(self, *args, **kwargs): + super(Testgeval, self).__init__(*args, **kwargs) + self._language = "Dutch" + def openen(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/french.py b/seleniumbase/translate/french.py index f959edbf3e0..cd8012125be 100755 --- a/seleniumbase/translate/french.py +++ b/seleniumbase/translate/french.py @@ -5,6 +5,10 @@ class CasDeBase(BaseCase): + def __init__(self, *args, **kwargs): + super(CasDeBase, self).__init__(*args, **kwargs) + self._language = "French" + def ouvrir(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/italian.py b/seleniumbase/translate/italian.py index 6f692e9f925..6bc474d524a 100755 --- a/seleniumbase/translate/italian.py +++ b/seleniumbase/translate/italian.py @@ -5,6 +5,10 @@ class CasoDiProva(BaseCase): + def __init__(self, *args, **kwargs): + super(CasoDiProva, self).__init__(*args, **kwargs) + self._language = "Italian" + def apri(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/japanese.py b/seleniumbase/translate/japanese.py index af04a5f177c..c563e5e88e6 100755 --- a/seleniumbase/translate/japanese.py +++ b/seleniumbase/translate/japanese.py @@ -5,6 +5,10 @@ class セレニウムテストケース(BaseCase): # noqa + def __init__(self, *args, **kwargs): + super(セレニウムテストケース, self).__init__(*args, **kwargs) + self._language = "Japanese" + def を開く(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/korean.py b/seleniumbase/translate/korean.py index f0366d91cfc..a8be5ab50cf 100755 --- a/seleniumbase/translate/korean.py +++ b/seleniumbase/translate/korean.py @@ -5,6 +5,10 @@ class 셀레늄_테스트_케이스(BaseCase): # noqa + def __init__(self, *args, **kwargs): + super(셀레늄_테스트_케이스, self).__init__(*args, **kwargs) + self._language = "Korean" + def 열기(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/portuguese.py b/seleniumbase/translate/portuguese.py index 31cf37cde9b..8a1bcf6ff2d 100755 --- a/seleniumbase/translate/portuguese.py +++ b/seleniumbase/translate/portuguese.py @@ -5,6 +5,10 @@ class CasoDeTeste(BaseCase): + def __init__(self, *args, **kwargs): + super(CasoDeTeste, self).__init__(*args, **kwargs) + self._language = "Portuguese" + def abrir(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/russian.py b/seleniumbase/translate/russian.py index 842b4fbc998..494213b5266 100755 --- a/seleniumbase/translate/russian.py +++ b/seleniumbase/translate/russian.py @@ -5,6 +5,10 @@ class ТестНаСелен(BaseCase): # noqa + def __init__(self, *args, **kwargs): + super(ТестНаСелен, self).__init__(*args, **kwargs) + self._language = "Russian" + def открыть(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) diff --git a/seleniumbase/translate/spanish.py b/seleniumbase/translate/spanish.py index e9dbdc17bd6..9a7fcf8c341 100755 --- a/seleniumbase/translate/spanish.py +++ b/seleniumbase/translate/spanish.py @@ -5,6 +5,10 @@ class CasoDePrueba(BaseCase): + def __init__(self, *args, **kwargs): + super(CasoDePrueba, self).__init__(*args, **kwargs) + self._language = "Spanish" + def abrir(self, *args, **kwargs): # open(url) return self.open(*args, **kwargs) From 63e8cc686a005592c29425adb7c38b83bf1b2d8c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 10 Jun 2020 23:59:27 -0400 Subject: [PATCH 14/47] Set "English" as the default language --- seleniumbase/fixtures/base_case.py | 1 + 1 file changed, 1 insertion(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index b186b757958..953466019fa 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs): self.__device_height = None self.__device_pixel_ratio = None # Requires self._* instead of self.__* for external class use + self._language = "English" self._html_report_extra = [] # (Used by pytest_plugin.py) self._default_driver = None self._drivers_list = [] From 8b8c2699841eb3ccc99bcd7c0d9b15456ebd801c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 11 Jun 2020 20:13:39 -0400 Subject: [PATCH 15/47] self.type(selector, text) exists. Don't confuse it with Python type(obj) --- seleniumbase/fixtures/base_case.py | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 953466019fa..473af080c58 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -383,6 +383,31 @@ def add_text(self, selector, text, by=By.CSS_SELECTOR, timeout=None): elif self.slow_mode: self.__slow_mode_pause_if_active() + def type(self, selector, text, by=By.CSS_SELECTOR, + timeout=None, retry=False): + """ Same as update_text() + This method updates an element's text field with new text. + Has multiple parts: + * Waits for the element to be visible. + * Waits for the element to be interactive. + * Clears the text field. + * Types in the new text. + * Hits Enter/Submit (if the text ends in "\n"). + @Params + selector - the selector of the text field + new_value - the new value to type into the text field + by - the type of selector to search by (Default: CSS Selector) + timeout - how long to wait for the selector to be visible + retry - if True, use JS if the Selenium text update fails + DO NOT confuse self.type() with Python type()! They are different! + """ + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + self.update_text(selector, text, by=by, timeout=timeout, retry=retry) + def submit(self, selector, by=By.CSS_SELECTOR): """ Alternative to self.driver.find_element_by_*(SELECTOR).submit() """ selector, by = self.__recalculate_selector(selector, by) @@ -2964,16 +2989,6 @@ def reload_page(self): """ Same as refresh_page() """ self.refresh_page() - def type(self, selector, text, by=By.CSS_SELECTOR, - timeout=None, retry=False): - """ Same as update_text() """ - if not timeout: - timeout = settings.LARGE_TIMEOUT - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - selector, by = self.__recalculate_selector(selector, by) - self.update_text(selector, text, by=by, timeout=timeout, retry=retry) - def input(self, selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False): """ Same as update_text() """ From 2d04bcfa1f7e8b0dcd0e167d2bc0a266f3f1e8c0 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Thu, 11 Jun 2020 20:51:24 -0400 Subject: [PATCH 16/47] Refactor master dictionary --- seleniumbase/translate/master_dict.py | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/seleniumbase/translate/master_dict.py b/seleniumbase/translate/master_dict.py index f62a4fc6144..a6bffcc9c47 100755 --- a/seleniumbase/translate/master_dict.py +++ b/seleniumbase/translate/master_dict.py @@ -1308,17 +1308,17 @@ class MD: md["set_attributes"][8] = "набор_атрибутов" md["set_attributes"][9] = "establecer_atributos" - md["input"] = ["*"] * num_langs - md["input"][0] = "input" - md["input"][1] = "输入文本" - md["input"][2] = "voer" - md["input"][3] = "taper" - md["input"][4] = "digitare" - md["input"][5] = "入力" - md["input"][6] = "입력" - md["input"][7] = "entrada" - md["input"][8] = "введите" - md["input"][9] = "entrada" + md["type"] = ["*"] * num_langs + md["type"][0] = "type" + md["type"][1] = "输入文本" + md["type"][2] = "voer" + md["type"][3] = "taper" + md["type"][4] = "digitare" + md["type"][5] = "入力" + md["type"][6] = "입력" + md["type"][7] = "entrada" + md["type"][8] = "введите" + md["type"][9] = "entrada" md["write"] = ["*"] * num_langs md["write"][0] = "write" @@ -1479,18 +1479,18 @@ class MD: ################ # Duplicates - # "type" -> duplicate of "input" - md["type"] = ["*"] * num_langs - md["type"][0] = "type" - md["type"][1] = "输入文本" - md["type"][2] = "voer" - md["type"][3] = "taper" - md["type"][4] = "digitare" - md["type"][5] = "入力" - md["type"][6] = "입력" - md["type"][7] = "entrada" - md["type"][8] = "введите" - md["type"][9] = "entrada" + # "input" -> duplicate of "type" + md["input"] = ["*"] * num_langs + md["input"][0] = "input" + md["input"][1] = "输入文本" + md["input"][2] = "voer" + md["input"][3] = "taper" + md["input"][4] = "digitare" + md["input"][5] = "入力" + md["input"][6] = "입력" + md["input"][7] = "entrada" + md["input"][8] = "введите" + md["input"][9] = "entrada" # "goto" -> duplicate of "visit" md["goto"] = ["*"] * num_langs From 6291e1d14a7234d73639ea7c007e4802b110caa9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 00:51:14 -0400 Subject: [PATCH 17/47] Simplify detection of Chinese, Japanese, and Korean characters --- seleniumbase/console_scripts/sb_print.py | 16 +++------------- seleniumbase/translate/translator.py | 16 +++------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/seleniumbase/console_scripts/sb_print.py b/seleniumbase/console_scripts/sb_print.py index a9fb85771b6..d120c9f8f04 100755 --- a/seleniumbase/console_scripts/sb_print.py +++ b/seleniumbase/console_scripts/sb_print.py @@ -37,19 +37,9 @@ def invalid_run_command(msg=None): def sc_ranges(): # Get the ranges of special characters of Chinese, Japanese, and Korean. special_char_ranges = ([ - {"from": ord(u"\u3300"), "to": ord(u"\u33ff")}, - {"from": ord(u"\ufe30"), "to": ord(u"\ufe4f")}, - {"from": ord(u"\uf900"), "to": ord(u"\ufaff")}, - {"from": ord(u"\U0002F800"), "to": ord(u"\U0002fa1f")}, - {'from': ord(u'\u3040'), 'to': ord(u'\u309f')}, - {"from": ord(u"\u30a0"), "to": ord(u"\u30ff")}, - {"from": ord(u"\u2e80"), "to": ord(u"\u2eff")}, - {"from": ord(u"\u4e00"), "to": ord(u"\u9fff")}, - {"from": ord(u"\u3400"), "to": ord(u"\u4dbf")}, - {"from": ord(u"\U00020000"), "to": ord(u"\U0002a6df")}, - {"from": ord(u"\U0002a700"), "to": ord(u"\U0002b73f")}, - {"from": ord(u"\U0002b740"), "to": ord(u"\U0002b81f")}, - {"from": ord(u"\U0002b820"), "to": ord(u"\U0002ceaf")} + {"from": ord(u"\u4e00"), "to": ord(u"\u9FFF")}, + {"from": ord(u"\u3040"), "to": ord(u"\u30ff")}, + {"from": ord(u"\uac00"), "to": ord(u"\ud7a3")} ]) return special_char_ranges diff --git a/seleniumbase/translate/translator.py b/seleniumbase/translate/translator.py index b31f82147cb..72424308d91 100755 --- a/seleniumbase/translate/translator.py +++ b/seleniumbase/translate/translator.py @@ -76,19 +76,9 @@ def invalid_run_command(msg=None): def sc_ranges(): # Get the ranges of special characters of Chinese, Japanese, and Korean. special_char_ranges = ([ - {"from": ord(u"\u3300"), "to": ord(u"\u33ff")}, - {"from": ord(u"\ufe30"), "to": ord(u"\ufe4f")}, - {"from": ord(u"\uf900"), "to": ord(u"\ufaff")}, - {"from": ord(u"\U0002F800"), "to": ord(u"\U0002fa1f")}, - {'from': ord(u'\u3040'), 'to': ord(u'\u309f')}, - {"from": ord(u"\u30a0"), "to": ord(u"\u30ff")}, - {"from": ord(u"\u2e80"), "to": ord(u"\u2eff")}, - {"from": ord(u"\u4e00"), "to": ord(u"\u9fff")}, - {"from": ord(u"\u3400"), "to": ord(u"\u4dbf")}, - {"from": ord(u"\U00020000"), "to": ord(u"\U0002a6df")}, - {"from": ord(u"\U0002a700"), "to": ord(u"\U0002b73f")}, - {"from": ord(u"\U0002b740"), "to": ord(u"\U0002b81f")}, - {"from": ord(u"\U0002b820"), "to": ord(u"\U0002ceaf")} + {"from": ord(u"\u4e00"), "to": ord(u"\u9FFF")}, + {"from": ord(u"\u3040"), "to": ord(u"\u30ff")}, + {"from": ord(u"\uac00"), "to": ord(u"\ud7a3")} ]) return special_char_ranges From 91155814e09c751fac832d3b0aee4c436afcd762 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 00:52:41 -0400 Subject: [PATCH 18/47] Update "rich" Python version --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 43c256138c1..b937ac33cf2 100755 --- a/requirements.txt +++ b/requirements.txt @@ -41,7 +41,7 @@ coverage==5.1 pyotp==2.3.0 boto==2.49.0 cffi==1.14.0 -rich==2.0.1;python_version>="3.6" and python_version<"4.0" +rich==2.1.0;python_version>="3.6" and python_version<"4.0" flake8==3.7.9;python_version<"3.5" flake8==3.8.3;python_version>="3.5" pyflakes==2.1.1;python_version<"3.5" diff --git a/setup.py b/setup.py index 47ba6e11546..54ae583164f 100755 --- a/setup.py +++ b/setup.py @@ -133,7 +133,7 @@ 'pyotp==2.3.0', 'boto==2.49.0', 'cffi==1.14.0', - 'rich==2.0.1;python_version>="3.6" and python_version<"4.0"', + 'rich==2.1.0;python_version>="3.6" and python_version<"4.0"', 'flake8==3.7.9;python_version<"3.5"', 'flake8==3.8.3;python_version>="3.5"', 'pyflakes==2.1.1;python_version<"3.5"', From eac54118b33f193dcc55ececc37ac01a76205ec5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 00:56:09 -0400 Subject: [PATCH 19/47] Replace " # noqa" with "" when printing Python test files --- seleniumbase/console_scripts/sb_print.py | 2 ++ seleniumbase/translate/translator.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/seleniumbase/console_scripts/sb_print.py b/seleniumbase/console_scripts/sb_print.py index d120c9f8f04..3458982806e 100755 --- a/seleniumbase/console_scripts/sb_print.py +++ b/seleniumbase/console_scripts/sb_print.py @@ -155,6 +155,8 @@ def main(): if is_python_file: new_sb_lines = [] for line in code_lines: + if line.endswith(" # noqa") and line.count(" # noqa") == 1: + line = line.replace(" # noqa", "") line_length2 = len(line) # Normal Python string length used line_length = get_width(line) # Special characters count 2X if line_length > code_width: diff --git a/seleniumbase/translate/translator.py b/seleniumbase/translate/translator.py index 72424308d91..b8587255a62 100755 --- a/seleniumbase/translate/translator.py +++ b/seleniumbase/translate/translator.py @@ -496,6 +496,8 @@ def main(): new_sb_lines = [] for line in seleniumbase_lines: + if line.endswith(" # noqa") and line.count(" # noqa") == 1: + line = line.replace(" # noqa", "") line_length2 = len(line) # Normal Python string length used line_length = get_width(line) # Special characters count 2X if line_length > code_width: From c36dd662512de45a6b2d4db36d6aafe4f0f4fea9 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 00:57:46 -0400 Subject: [PATCH 20/47] Update the SeleniumBase smart-word-wrap function --- seleniumbase/console_scripts/sb_print.py | 48 ++++++++++++++++++++++++ seleniumbase/translate/translator.py | 48 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/seleniumbase/console_scripts/sb_print.py b/seleniumbase/console_scripts/sb_print.py index 3458982806e..80476ffbbde 100755 --- a/seleniumbase/console_scripts/sb_print.py +++ b/seleniumbase/console_scripts/sb_print.py @@ -246,6 +246,54 @@ def main(): else: new_sb_lines.append(line) continue + elif line.count('("') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('("')[0] + '(' + line2 = new_ws + '"' + line.split('("')[1] + if not ('):') in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('" in self.') == 1: + line2a = line2.split( + '" in self.')[0] + '" in' + line2b = new_ws + "self." + ( + line2.split('" in self.')[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + elif line.count("('") == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("('")[0] + '(' + line2 = new_ws + "'" + line.split("('")[1] + if not ('):') in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count("' in self.") == 1: + line2a = line2.split( + "' in self.")[0] + "' in" + line2b = new_ws + "self." + ( + line2.split("' in self.")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue elif line.count('= "') == 1 and line.count('://') == 1: whitespace = line_length2 - len(line.lstrip()) new_ws = line[0:whitespace] + " " diff --git a/seleniumbase/translate/translator.py b/seleniumbase/translate/translator.py index b8587255a62..42266e2fd96 100755 --- a/seleniumbase/translate/translator.py +++ b/seleniumbase/translate/translator.py @@ -587,6 +587,54 @@ def main(): else: new_sb_lines.append(line) continue + elif line.count('("') == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split('("')[0] + '(' + line2 = new_ws + '"' + line.split('("')[1] + if not ('):') in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count('" in self.') == 1: + line2a = line2.split( + '" in self.')[0] + '" in' + line2b = new_ws + "self." + ( + line2.split('" in self.')[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue + elif line.count("('") == 1: + whitespace = line_length2 - len(line.lstrip()) + new_ws = line[0:whitespace] + " " + line1 = line.split("('")[0] + '(' + line2 = new_ws + "'" + line.split("('")[1] + if not ('):') in line2: + new_sb_lines.append(line1) + if get_width(line2) + w > console_width: + if line2.count("' in self.") == 1: + line2a = line2.split( + "' in self.")[0] + "' in" + line2b = new_ws + "self." + ( + line2.split("' in self.")[1]) + new_sb_lines.append(line2a) + new_sb_lines.append(line2b) + continue + new_sb_lines.append(line2) + elif get_width(line2) + 4 + w <= console_width: + line2 = " " + line2 + new_sb_lines.append(line1) + new_sb_lines.append(line2) + else: + new_sb_lines.append(line) + continue elif line.count('= "') == 1 and line.count('://') == 1: whitespace = line_length2 - len(line.lstrip()) new_ws = line[0:whitespace] + " " From cbd50474f9388d14527db3bce08f76ac34607450 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 01:06:18 -0400 Subject: [PATCH 21/47] Add translations for messages posted by Demo Mode --- seleniumbase/fixtures/base_case.py | 57 +++++++++++--- seleniumbase/fixtures/words.py | 117 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 11 deletions(-) create mode 100755 seleniumbase/fixtures/words.py diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 473af080c58..7bacfe6f572 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -2373,7 +2373,11 @@ def assert_no_404_errors(self, multithreaded=True): for link in links: self.assert_link_status_code_is_not_404(link) if self.demo_mode: - messenger_post = ("ASSERT NO 404 ERRORS") + a_t = "ASSERT NO 404 ERRORS" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_no_404_errors(self._language) + messenger_post = ("%s" % a_t) self.__highlight_with_assert_success(messenger_post, "html") def print_unique_links_with_status_codes(self): @@ -2673,7 +2677,11 @@ def assert_title(self, title): "does not match the actual page title [%s]!" "" % (expected, actual)) if self.demo_mode: - messenger_post = ("ASSERT TITLE = {%s}" % title) + a_t = "ASSERT TITLE" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_title(self._language) + messenger_post = ("%s: {%s}" % (a_t, title)) self.__highlight_with_assert_success(messenger_post, "html") def assert_no_js_errors(self): @@ -2704,7 +2712,11 @@ def assert_no_js_errors(self): "JavaScript errors found on %s => %s" % (current_url, errors)) if self.demo_mode: if (self.browser == 'chrome' or self.browser == 'edge'): - messenger_post = ("ASSERT NO JS ERRORS") + a_t = "ASSERT NO JS ERRORS" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_no_js_errors(self._language) + messenger_post = ("%s" % a_t) self.__highlight_with_assert_success(messenger_post, "html") def __activate_html_inspector(self): @@ -3839,7 +3851,11 @@ def assert_element(self, selector, by=By.CSS_SELECTOR, timeout=None): if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = "ASSERT %s: {%s}" % (by, selector) + a_t = "ASSERT" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert(self._language) + messenger_post = "%s %s: {%s}" % (a_t, by.upper(), selector) self.__highlight_with_assert_success(messenger_post, selector, by) return True @@ -3919,8 +3935,14 @@ def assert_text(self, text, selector="html", by=By.CSS_SELECTOR, if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = ("ASSERT TEXT {%s} in %s: {%s}" - % (text, by, selector)) + a_t = "ASSERT TEXT" + i_n = "in" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = ("%s {%s} %s %s: {%s}" + % (a_t, text, i_n, by.upper(), selector)) self.__highlight_with_assert_success(messenger_post, selector, by) return True @@ -3940,8 +3962,14 @@ def assert_exact_text(self, text, selector="html", by=By.CSS_SELECTOR, if self.demo_mode: selector, by = self.__recalculate_selector(selector, by) - messenger_post = ("ASSERT EXACT TEXT {%s} in %s: {%s}" - % (text, by, selector)) + a_t = "ASSERT EXACT TEXT" + i_n = "in" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_exact_text(self._language) + i_n = SD.translate_in(self._language) + messenger_post = ("%s {%s} %s %s: {%s}" + % (a_t, text, i_n, by.upper(), selector)) self.__highlight_with_assert_success(messenger_post, selector, by) return True @@ -4025,7 +4053,11 @@ def assert_link_text(self, link_text, timeout=None): timeout = self.__get_new_timeout(timeout) self.wait_for_link_text_visible(link_text, timeout=timeout) if self.demo_mode: - messenger_post = ("ASSERT LINK TEXT {%s}." % link_text) + a_t = "ASSERT LINK TEXT" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_link_text(self._language) + messenger_post = ("%s: {%s}" % (a_t, link_text)) self.__highlight_with_assert_success( messenger_post, link_text, by=By.LINK_TEXT) return True @@ -4057,8 +4089,11 @@ def assert_partial_link_text(self, partial_link_text, timeout=None): timeout = self.__get_new_timeout(timeout) self.wait_for_partial_link_text(partial_link_text, timeout=timeout) if self.demo_mode: - messenger_post = ( - "ASSERT PARTIAL LINK TEXT {%s}." % partial_link_text) + a_t = "ASSERT PARTIAL LINK TEXT" + if self._language != "English": + from seleniumbase.fixtures.words import SD + a_t = SD.translate_assert_link_text(self._language) + messenger_post = ("%s: {%s}" % (a_t, partial_link_text)) self.__highlight_with_assert_success( messenger_post, partial_link_text, by=By.PARTIAL_LINK_TEXT) return True diff --git a/seleniumbase/fixtures/words.py b/seleniumbase/fixtures/words.py new file mode 100755 index 00000000000..549163fab0d --- /dev/null +++ b/seleniumbase/fixtures/words.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +''' Small Dictionary ''' + + +class SD: + + def translate_in(language): + words = {} + words["English"] = "in" + words["Chinese"] = "在" + words["Dutch"] = "in" + words["French"] = "dans" + words["Italian"] = "nel" + words["Japanese"] = "に" + words["Korean"] = "에" + words["Portuguese"] = "no" + words["Russian"] = "в" + words["Spanish"] = "en" + return words[language] + + def translate_assert(language): + words = {} + words["English"] = "ASSERT" + words["Chinese"] = "断言" + words["Dutch"] = "CONTROLEREN" + words["French"] = "VÉRIFIER" + words["Italian"] = "VERIFICARE" + words["Japanese"] = "検証" + words["Korean"] = "확인" + words["Portuguese"] = "VERIFICAR" + words["Russian"] = "ПОДТВЕРДИТЬ" + words["Spanish"] = "VERIFICAR" + return words[language] + + def translate_assert_text(language): + words = {} + words["English"] = "ASSERT TEXT" + words["Chinese"] = "断言文本" + words["Dutch"] = "CONTROLEREN TEKST" + words["French"] = "VÉRIFIER TEXTE" + words["Italian"] = "VERIFICARE TESTO" + words["Japanese"] = "テキストを確認する" + words["Korean"] = "텍스트 확인" + words["Portuguese"] = "VERIFICAR TEXTO" + words["Russian"] = "ПОДТВЕРДИТЬ ТЕКСТ" + words["Spanish"] = "VERIFICAR TEXTO" + return words[language] + + def translate_assert_exact_text(language): + words = {} + words["English"] = "ASSERT EXACT TEXT" + words["Chinese"] = "确切断言文本" + words["Dutch"] = "CONTROLEREN EXACTE TEKST" + words["French"] = "VÉRIFIER EXACTEMENT TEXTE" + words["Italian"] = "VERIFICARE TESTO ESATTO" + words["Japanese"] = "正確なテキストを確認する" + words["Korean"] = "정확한 텍스트를 확인하는" + words["Portuguese"] = "VERIFICAR TEXTO EXATO" + words["Russian"] = "ПОДТВЕРДИТЬ ТЕКСТ ТОЧНО" + words["Spanish"] = "VERIFICAR TEXTO EXACTO" + return words[language] + + def translate_assert_link_text(language): + words = {} + words["English"] = "ASSERT LINK TEXT" + words["Chinese"] = "断言链接文本" + words["Dutch"] = "CONTROLEREN LINKTEKST" + words["French"] = "VÉRIFIER TEXTE DU LIEN" + words["Italian"] = "VERIFICARE TESTO DEL COLLEGAMENTO" + words["Japanese"] = "リンクテキストを確認する" + words["Korean"] = "링크 텍스트 확인" + words["Portuguese"] = "VERIFICAR TEXTO DO LINK" + words["Russian"] = "ПОДТВЕРДИТЬ ССЫЛКУ" + words["Spanish"] = "VERIFICAR TEXTO DEL ENLACE" + return words[language] + + def translate_assert_title(language): + words = {} + words["English"] = "ASSERT TITLE" + words["Chinese"] = "断言标题" + words["Dutch"] = "CONTROLEREN TITEL" + words["French"] = "VÉRIFIER TITRE" + words["Italian"] = "VERIFICARE TITOLO" + words["Japanese"] = "タイトルを確認" + words["Korean"] = "제목 확인" + words["Portuguese"] = "VERIFICAR TÍTULO" + words["Russian"] = "ПОДТВЕРДИТЬ НАЗВАНИЕ" + words["Spanish"] = "VERIFICAR TÍTULO" + return words[language] + + def translate_assert_no_404_errors(language): + words = {} + words["English"] = "ASSERT NO 404 ERRORS" + words["Chinese"] = "检查断开的链接" + words["Dutch"] = "CONTROLEREN OP GEBROKEN LINKS" + words["French"] = "VÉRIFIER LES LIENS ROMPUS" + words["Italian"] = "VERIFICARE I COLLEGAMENTI" + words["Japanese"] = "リンク切れを確認する" + words["Korean"] = "끊어진 링크 확인" + words["Portuguese"] = "VERIFICAR SE HÁ LINKS QUEBRADOS" + words["Russian"] = "ПРОВЕРИТЬ ОШИБКИ 404" + words["Spanish"] = "VERIFICAR SI HAY ENLACES ROTOS" + return words[language] + + def translate_assert_no_js_errors(language): + words = {} + words["English"] = "ASSERT NO JS ERRORS" + words["Chinese"] = "检查JS错误" + words["Dutch"] = "CONTROLEREN OP JS FOUTEN" + words["French"] = "VÉRIFIER LES ERREURS JS" + words["Italian"] = "CONTROLLA ERRORI JS" + words["Japanese"] = "JSエラーを確認する" + words["Korean"] = "JS 오류 확인" + words["Portuguese"] = "VERIFICAR SE HÁ ERROS JS" + words["Russian"] = "ПРОВЕРИТЬ ОШИБКИ JS" + words["Spanish"] = "VERIFICAR SI HAY ERRORES JS" + return words[language] From 79ca6d6ef3c824febb164c88dc359170e8f66919 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 01:32:17 -0400 Subject: [PATCH 22/47] Update the docs --- seleniumbase/console_scripts/ReadMe.md | 28 +++++++++++++++++++++++ seleniumbase/console_scripts/sb_mkdir.py | 1 + seleniumbase/console_scripts/sb_mkfile.py | 11 +++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index 14e921067ca..678e43de66c 100755 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -42,6 +42,34 @@ sample tests for helping new users get started, and Python boilerplates for setting up customized test frameworks. +### mkfile + +* Usage: +``seleniumbase mkfile [FILE_NAME.py] [OPTIONS]`` + +* Example: +``seleniumbase mkfile new_test.py`` + +* Options: +``-b`` / ``--basic`` (Basic boilerplate / single-line test) + +* Language Options: +``--en`` / ``--English`` | ``--zh`` / ``--Chinese`` +``--nl`` / ``--Dutch`` | ``--fr`` / ``--French`` +``--it`` / ``--Italian`` | ``--ja`` / ``--Japanese`` +``--ko`` / ``--Korean`` | ``--pt`` / ``--Portuguese`` +``--ru`` / ``--Russian`` | ``--es`` / ``--Spanish`` + +* Output: +Creates a new SB test file with boilerplate code. +If the file already exists, an error is raised. +By default, uses English mode and creates a +boilerplate with the 5 most common SeleniumBase +methods, which are "open", "click", "update_text", +"assert_element", and "assert_text". If using the +basic boilerplate option, only the "open" method +is included. + ### convert * Usage: diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index 4b080e2ed27..af288c082a3 100755 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -4,6 +4,7 @@ Usage: seleniumbase mkdir [DIRECTORY_NAME] OR sbase mkdir [DIRECTORY_NAME] + Output: Creates a new folder for running SeleniumBase scripts. The new folder contains default config files, diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py index c650da95d0c..7bde4620ade 100755 --- a/seleniumbase/console_scripts/sb_mkfile.py +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -5,9 +5,16 @@ Usage: seleniumbase mkfile [FILE_NAME.py] [OPTIONS] or sbase mkfile [FILE_NAME.py] [OPTIONS] + Output: -Creates a new SeleniumBase test file with boilerplate code. -If the file name already exists, an error will be raised. +Creates a new SB test file with boilerplate code. +If the file already exists, an error is raised. +By default, uses English mode and creates a +boilerplate with the 5 most common SeleniumBase +methods, which are "open", "click", "update_text", +"assert_element", and "assert_text". If using the +basic boilerplate option, only the "open" method +is included. """ import codecs From 45904428be4a42ef4142e30fb2da19cba5b4376a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 01:33:11 -0400 Subject: [PATCH 23/47] Improve reliability of Demo Mode --- seleniumbase/fixtures/js_utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/seleniumbase/fixtures/js_utils.py b/seleniumbase/fixtures/js_utils.py index 28a4014b4ef..aa2dba76208 100755 --- a/seleniumbase/fixtures/js_utils.py +++ b/seleniumbase/fixtures/js_utils.py @@ -233,7 +233,10 @@ def highlight_with_js(driver, selector, loops, o_bs): script = ("""document.querySelector('%s').style.boxShadow = '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" % selector) - driver.execute_script(script) + try: + driver.execute_script(script) + except Exception: + return for n in range(loops): script = ("""document.querySelector('%s').style.boxShadow = '0px 0px 6px 6px rgba(255, 0, 0, 1)';""" @@ -609,7 +612,10 @@ def highlight_with_js_2(driver, message, selector, o_bs, msg_dur): script = ("""document.querySelector('%s').style.boxShadow = '0px 0px 6px 6px rgba(128, 128, 128, 0.5)';""" % selector) - driver.execute_script(script) + try: + driver.execute_script(script) + except Exception: + return time.sleep(0.0181) script = ("""document.querySelector('%s').style.boxShadow = '0px 0px 6px 6px rgba(205, 30, 0, 1)';""" @@ -644,7 +650,10 @@ def highlight_with_jquery_2(driver, message, selector, o_bs, msg_dur): selector = "body" script = """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(128, 128, 128, 0.5)');""" % selector - safe_execute_script(driver, script) + try: + safe_execute_script(driver, script) + except Exception: + return time.sleep(0.0181) script = """jQuery('%s').css('box-shadow', '0px 0px 6px 6px rgba(205, 30, 0, 1)');""" % selector From 38fa1d03248283a2f46acac1697563044e34ff6b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 12:06:54 -0400 Subject: [PATCH 24/47] Update the docs --- seleniumbase/console_scripts/ReadMe.md | 56 +++++---- seleniumbase/console_scripts/run.py | 131 ++++++++++++++++------ seleniumbase/console_scripts/sb_mkdir.py | 23 ++-- seleniumbase/console_scripts/sb_mkfile.py | 35 ++++-- 4 files changed, 167 insertions(+), 78 deletions(-) diff --git a/seleniumbase/console_scripts/ReadMe.md b/seleniumbase/console_scripts/ReadMe.md index 678e43de66c..05706822236 100755 --- a/seleniumbase/console_scripts/ReadMe.md +++ b/seleniumbase/console_scripts/ReadMe.md @@ -4,20 +4,28 @@ SeleniumBase console scripts help you get things done more easily, such as installing web drivers, creating a test directory with necessary configuration files, converting old WebDriver unittest scripts into SeleniumBase code, translating tests into multiple languages, and using the Selenium Grid. -Type ``seleniumbase`` on the command line to use console scripts. -You can also use the simplified name: ``sbase`` instead. +* Usage: ``seleniumbase [COMMAND] [PARAMETERS]`` + +* (simplified): ``sbase [COMMAND] [PARAMETERS]`` + +* To list all commands: ``seleniumbase --help`` (For running tests, [use pytest with SeleniumBase](https://github.com/seleniumbase/SeleniumBase/blob/master/help_docs/customizing_test_runs.md).) ### install * Usage: -``seleniumbase install [DRIVER_NAME]`` - (Drivers: ``chromedriver``, ``geckodriver``, ``edgedriver``, - ``iedriver``, ``operadriver``) +``sbase install [DRIVER_NAME] [VERSION]`` + (Drivers: ``chromedriver``, ``geckodriver``, ``edgedriver``, + ``iedriver``, ``operadriver``) + (Versions: ``latest`` or a specific driver version. + If none specified, installs the default version.) -* Example: -``seleniumbase install chromedriver`` +* Examples: +``sbase install chromedriver`` + +* Options: + ``latest``: * Output: Installs the specified webdriver. @@ -30,25 +38,25 @@ Installs the specified webdriver. ### mkdir * Usage: -``seleniumbase mkdir [DIRECTORY_NAME]`` +``sbase mkdir [DIRECTORY_NAME]`` * Example: -``seleniumbase mkdir browser_tests`` +``sbase mkdir browser_tests`` * Output: Creates a new folder for running SeleniumBase scripts. The new folder contains default config files, -sample tests for helping new users get started, and -Python boilerplates for setting up customized +sample tests for helping new users get started, +and Python boilerplates for setting up customized test frameworks. ### mkfile * Usage: -``seleniumbase mkfile [FILE_NAME.py] [OPTIONS]`` +``sbase mkfile [FILE_NAME.py] [OPTIONS]`` * Example: -``seleniumbase mkfile new_test.py`` +``sbase mkfile new_test.py`` * Options: ``-b`` / ``--basic`` (Basic boilerplate / single-line test) @@ -61,7 +69,7 @@ test frameworks. ``--ru`` / ``--Russian`` | ``--es`` / ``--Spanish`` * Output: -Creates a new SB test file with boilerplate code. +Creates a new SeleniumBase test file with boilerplate code. If the file already exists, an error is raised. By default, uses English mode and creates a boilerplate with the 5 most common SeleniumBase @@ -73,7 +81,7 @@ is included. ### convert * Usage: -``seleniumbase convert [PYTHON_WEBDRIVER_UNITTEST_FILE]`` +``sbase convert [PYTHON_WEBDRIVER_UNITTEST_FILE]`` * Output: Converts a Selenium IDE exported WebDriver unittest file @@ -85,7 +93,7 @@ See: http://www.katalon.com/automation-recorder ### translate * Usage: -``seleniumbase translate [SB_FILE].py [LANGUAGE] [ACTION]`` +``sbase translate [SB_FILE].py [LANGUAGE] [ACTION]`` * Languages: ``--en`` / ``--English`` | ``--zh`` / ``--Chinese`` @@ -117,7 +125,7 @@ plus the 2-letter language code of the new language. ### extract-objects * Usage: -``seleniumbase extract-objects [SB_PYTHON_FILE]`` +``sbase extract-objects [SB_PYTHON_FILE]`` * Output: Creates page objects based on selectors found in a @@ -127,7 +135,7 @@ seleniumbase Python file and saves those objects to the ### inject-objects * Usage: -``seleniumbase inject-objects [SB_PYTHON_FILE] [OPTIONS]`` +``sbase inject-objects [SB_PYTHON_FILE] [OPTIONS]`` * Options: ``-c``, ``--comments`` (Add object selectors to the comments.) @@ -140,7 +148,7 @@ the selected seleniumbase Python file. ### objectify * Usage: -``seleniumbase objectify [SB_PYTHON_FILE] [OPTIONS]`` +``sbase objectify [SB_PYTHON_FILE] [OPTIONS]`` * Options: ``-c``, ``--comments`` (Add object selectors to the comments.) @@ -155,7 +163,7 @@ have been replaced with variable names defined in ### revert-objects * Usage: -``seleniumbase revert-objects [SB_PYTHON_FILE] [OPTIONS]`` +``sbase revert-objects [SB_PYTHON_FILE] [OPTIONS]`` * Options: ``-c``, ``--comments`` (Keep existing comments for the lines.) @@ -169,11 +177,11 @@ selectors stored in the "page_objects.py" file. ### download * Usage: -``seleniumbase download [ITEM]`` +``sbase download [ITEM]`` (Options: server) * Example: -``seleniumbase download server`` +``sbase download server`` * Output: Downloads the specified item. @@ -182,7 +190,7 @@ Downloads the specified item. ### grid-hub * Usage: -``seleniumbase grid-hub {start|stop}`` +``sbase grid-hub {start|stop}`` * Options: ``-v``, ``--verbose`` (Increases verbosity of logging output.) @@ -197,7 +205,7 @@ You can start, restart, or stop the Grid Hub server. ### grid-node * Usage: -``seleniumbase grid-node {start|stop} [OPTIONS]`` +``sbase grid-node {start|stop} [OPTIONS]`` * Options: ``--hub=HUB_IP`` (The Grid Hub IP Address to connect to.) (Default: ``127.0.0.1``) diff --git a/seleniumbase/console_scripts/run.py b/seleniumbase/console_scripts/run.py index 65fe947a6bf..c05c1585d41 100644 --- a/seleniumbase/console_scripts/run.py +++ b/seleniumbase/console_scripts/run.py @@ -54,11 +54,12 @@ def show_basic_usage(): print("") sc = ("") sc += ('Usage: "seleniumbase [COMMAND] [PARAMETERS]"\n') - sc += ('(short name): "sbase [COMMAND] [PARAMETERS]"\n') + sc += ('(simplified): "sbase [COMMAND] [PARAMETERS]"\n') sc += ("\n") sc += ("Commands:\n") sc += (" install [DRIVER_NAME] [OPTIONS]\n") - sc += (" mkdir [NEW_TEST_DIRECTORY_NAME]\n") + sc += (" mkdir [DIRECTORY_NAME]\n") + sc += (" mkfile [FILE_NAME.py]\n") sc += (" convert [PYTHON_WEBDRIVER_UNITTEST_FILE]\n") sc += (" print [FILE] [OPTIONS]\n") sc += (" translate [SB_PYTHON_FILE] [LANGUAGE] [ACTION]\n") @@ -82,7 +83,11 @@ def show_basic_usage(): def show_install_usage(): - print(" ** install **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "install" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase install [DRIVER_NAME] [OPTIONS]") @@ -95,12 +100,12 @@ def show_install_usage(): print(' Use "latest" for the latest version.') print(" -p OR --path Also copy the driver to /usr/local/bin") print(" Example:") - print(" seleniumbase install chromedriver") - print(" seleniumbase install geckodriver") - print(" seleniumbase install chromedriver 83.0.4103.39") - print(" seleniumbase install chromedriver latest") - print(" seleniumbase install chromedriver -p") - print(" seleniumbase install chromedriver latest -p") + print(" sbase install chromedriver") + print(" sbase install geckodriver") + print(" sbase install chromedriver 83.0.4103.39") + print(" sbase install chromedriver latest") + print(" sbase install chromedriver -p") + print(" sbase install chromedriver latest -p") print(" Output:") print(" Installs the chosen webdriver to seleniumbase/drivers/") print(" (chromedriver is required for Chrome automation)") @@ -112,30 +117,38 @@ def show_install_usage(): def show_mkdir_usage(): - print(" ** mkdir **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "mkdir" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase mkdir [DIRECTORY_NAME]") print(" OR: sbase mkdir [DIRECTORY_NAME]") print(" Example:") - print(" seleniumbase mkdir browser_tests") + print(" sbase mkdir browser_tests") print(" Output:") - print(" Creates a new folder for running SeleniumBase scripts.") + print(" Creates a new folder for running SBase scripts.") print(" The new folder contains default config files,") - print(" sample tests for helping new users get started, and") - print(" Python boilerplates for setting up customized") + print(" sample tests for helping new users get started,") + print(" and Python boilerplates for setting up customized") print(" test frameworks.") print("") def show_mkfile_usage(): - print(" ** mkfile **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "mkfile" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase mkfile [FILE_NAME.py]") print(" OR: sbase mkfile [FILE_NAME.py]") print(" Example:") - print(" seleniumbase mkfile new_test.py") + print(" sbase mkfile new_test.py") print(" Options:") print(" -b / --basic (Basic boilerplate / single-line test)") print(" Language Options:") @@ -145,7 +158,7 @@ def show_mkfile_usage(): print(" --ko / --Korean | --pt / --Portuguese") print(" --ru / --Russian | --es / --Spanish") print(" Output:") - print(" Creates a new SB test file with boilerplate code.") + print(" Creates a new SBase test file with boilerplate code.") print(" If the file already exists, an error is raised.") print(" By default, uses English mode and creates a") print(" boilerplate with the 5 most common SeleniumBase") @@ -157,7 +170,11 @@ def show_mkfile_usage(): def show_convert_usage(): - print(" ** convert **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "convert" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase convert [PYTHON_WEBDRIVER_UNITTEST_FILE]") @@ -172,7 +189,12 @@ def show_convert_usage(): def show_print_usage(): - print(" ** print **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "print" + c2 + " **" + cr) + print(sc) + print("") print(" Usage:") print(" seleniumbase print [FILE] [OPTIONS]") print(" OR: sbase print [FILE] [OPTIONS]") @@ -186,7 +208,12 @@ def show_print_usage(): def show_translate_usage(): - print(" ** translate **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "translate" + c2 + " **" + cr) + print(sc) + print("") print(" Usage:") print(" seleniumbase translate [SB_FILE.py] [LANGUAGE] [ACTION]") print(" OR: sbase translate [SB_FILE.py] [LANGUAGE] [ACTION]") @@ -216,7 +243,11 @@ def show_translate_usage(): def show_extract_objects_usage(): - print(" ** extract-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "extract-objects" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase extract-objects [SELENIUMBASE_PYTHON_FILE]") @@ -229,7 +260,11 @@ def show_extract_objects_usage(): def show_inject_objects_usage(): - print(" ** inject-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "inject-objects" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase inject-objects [SELENIUMBASE_PYTHON_FILE]") @@ -245,7 +280,11 @@ def show_inject_objects_usage(): def show_objectify_usage(): - print(" ** objectify **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "objectify" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase objectify [SELENIUMBASE_PYTHON_FILE]") @@ -264,7 +303,11 @@ def show_objectify_usage(): def show_revert_objects_usage(): - print(" ** revert-objects **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "revert-objects" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase revert-objects [SELENIUMBASE_PYTHON_FILE]") @@ -281,7 +324,11 @@ def show_revert_objects_usage(): def show_encrypt_usage(): - print(" ** encrypt OR obfuscate **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "encrypt OR obfuscate" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase encrypt || seleniumbase obfuscate") @@ -294,7 +341,11 @@ def show_encrypt_usage(): def show_decrypt_usage(): - print(" ** decrypt OR unobfuscate **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "decrypt OR unobfuscate" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase decrypt || seleniumbase unobfuscate") @@ -307,7 +358,11 @@ def show_decrypt_usage(): def show_download_usage(): - print(" ** download **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "download" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase download server") @@ -319,7 +374,11 @@ def show_download_usage(): def show_grid_hub_usage(): - print(" ** grid-hub **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "grid-hub" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase grid-hub {start|stop}") @@ -339,7 +398,11 @@ def show_grid_hub_usage(): def show_grid_node_usage(): - print(" ** grid-node **") + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + cr = colorama.Style.RESET_ALL + sc = (" " + c2 + "** " + c3 + "grid-node" + c2 + " **" + cr) + print(sc) print("") print(" Usage:") print(" seleniumbase grid-node {start|stop} [OPTIONS]") @@ -374,9 +437,13 @@ def show_version_info(): def show_detailed_help(): + c2 = colorama.Fore.BLUE + colorama.Back.LIGHTGREEN_EX + c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX + c6 = colorama.Back.CYAN + cr = colorama.Style.RESET_ALL show_basic_usage() - print("More Info:") - print("") + print(c6 + " " + c2 + " Commands: " + c6 + " ") + print(cr) show_install_usage() show_mkdir_usage() show_mkfile_usage() @@ -392,8 +459,6 @@ def show_detailed_help(): show_download_usage() show_grid_hub_usage() show_grid_node_usage() - c3 = colorama.Fore.BLUE + colorama.Back.LIGHTYELLOW_EX - cr = colorama.Style.RESET_ALL print('* (Use "' + c3 + 'pytest' + cr + '" for running tests) *\n') diff --git a/seleniumbase/console_scripts/sb_mkdir.py b/seleniumbase/console_scripts/sb_mkdir.py index af288c082a3..4c9f3b6d23e 100755 --- a/seleniumbase/console_scripts/sb_mkdir.py +++ b/seleniumbase/console_scripts/sb_mkdir.py @@ -2,15 +2,18 @@ Creates a new folder for running SeleniumBase scripts. Usage: -seleniumbase mkdir [DIRECTORY_NAME] -OR sbase mkdir [DIRECTORY_NAME] + seleniumbase mkdir [DIRECTORY_NAME] + OR sbase mkdir [DIRECTORY_NAME] + +Example: + sbase mkdir browser_tests Output: -Creates a new folder for running SeleniumBase scripts. -The new folder contains default config files, -sample tests for helping new users get started, and -Python boilerplates for setting up customized -test frameworks. + Creates a new folder for running SBase scripts. + The new folder contains default config files, + sample tests for helping new users get started, + and Python boilerplates for setting up customized + test frameworks. """ import codecs @@ -27,10 +30,10 @@ def invalid_run_command(msg=None): exp += " Example:\n" exp += " sbase mkdir browser_tests\n" exp += " Output:\n" - exp += " Creates a new folder for running SeleniumBase scripts.\n" + exp += " Creates a new folder for running SBase scripts.\n" exp += " The new folder contains default config files,\n" - exp += " sample tests for helping new users get started, and\n" - exp += " Python boilerplates for setting up customized\n" + exp += " sample tests for helping new users get started,\n" + exp += " and Python boilerplates for setting up customized\n" exp += " test frameworks.\n" if not msg: raise Exception('INVALID RUN COMMAND!\n\n%s' % exp) diff --git a/seleniumbase/console_scripts/sb_mkfile.py b/seleniumbase/console_scripts/sb_mkfile.py index 7bde4620ade..745cf2ff04f 100755 --- a/seleniumbase/console_scripts/sb_mkfile.py +++ b/seleniumbase/console_scripts/sb_mkfile.py @@ -3,18 +3,31 @@ Creates a new SeleniumBase test file with boilerplate code. Usage: -seleniumbase mkfile [FILE_NAME.py] [OPTIONS] -or sbase mkfile [FILE_NAME.py] [OPTIONS] + seleniumbase mkfile [FILE_NAME.py] [OPTIONS] + or sbase mkfile [FILE_NAME.py] [OPTIONS] + +Example: + sbase mkfile new_test.py + +Options: + -b / --basic (Basic boilerplate / single-line test) + +Language Options: + --en / --English | --zh / --Chinese + --nl / --Dutch | --fr / --French + --it / --Italian | --ja / --Japanese + --ko / --Korean | --pt / --Portuguese + --ru / --Russian | --es / --Spanish Output: -Creates a new SB test file with boilerplate code. -If the file already exists, an error is raised. -By default, uses English mode and creates a -boilerplate with the 5 most common SeleniumBase -methods, which are "open", "click", "update_text", -"assert_element", and "assert_text". If using the -basic boilerplate option, only the "open" method -is included. + Creates a new SBase test file with boilerplate code. + If the file already exists, an error is raised. + By default, uses English mode and creates a + boilerplate with the 5 most common SeleniumBase + methods, which are "open", "click", "update_text", + "assert_element", and "assert_text". If using the + basic boilerplate option, only the "open" method + is included. """ import codecs @@ -39,7 +52,7 @@ def invalid_run_command(msg=None): exp += " --ko / --Korean | --pt / --Portuguese\n" exp += " --ru / --Russian | --es / --Spanish\n" exp += " Output:\n" - exp += " Creates a new SB test file with boilerplate code.\n" + exp += " Creates a new SBase test file with boilerplate code.\n" exp += " If the file already exists, an error is raised.\n" exp += " By default, uses English mode and creates a\n" exp += " boilerplate with the 5 most common SeleniumBase\n" From 7737787dee5f84153b1f20bcaf86273e55a0f738 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 18:25:51 -0400 Subject: [PATCH 25/47] Remove methods that were marked as deprecated over a year ago. --- seleniumbase/fixtures/base_case.py | 32 ------------------------------ 1 file changed, 32 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 7bacfe6f572..0870d851274 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -4925,38 +4925,6 @@ def __highlight_with_jquery_2(self, message, selector, o_bs): # Deprecated Methods (Replace these if they're still in your code!) - @decorators.deprecated( - "scroll_click() is deprecated. Use self.click() - It scrolls for you!") - def scroll_click(self, selector, by=By.CSS_SELECTOR): - # DEPRECATED - self.click() now scrolls to the element before clicking. - # self.scroll_to(selector, by=by) # Redundant - self.click(selector, by=by) - - @decorators.deprecated( - "update_text_value() is deprecated. Use self.update_text() instead!") - def update_text_value(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=None, retry=False): - # DEPRECATED - self.update_text() should be used instead. - if not timeout: - timeout = settings.LARGE_TIMEOUT - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - if page_utils.is_xpath_selector(selector): - by = By.XPATH - self.update_text( - selector, new_value, by=by, timeout=timeout, retry=retry) - - @decorators.deprecated( - "jquery_update_text_value() is deprecated. Use jquery_update_text()") - def jquery_update_text_value(self, selector, new_value, by=By.CSS_SELECTOR, - timeout=None): - # DEPRECATED - self.jquery_update_text() should be used instead. - if not timeout: - timeout = settings.LARGE_TIMEOUT - if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: - timeout = self.__get_new_timeout(timeout) - self.jquery_update_text(selector, new_value, by=by, timeout=timeout) - @decorators.deprecated( "jq_format() is deprecated. Use re.escape() instead!") def jq_format(self, code): From 57ab91f8c3b851eecd0c19803d39ab7ecbd2df54 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 19:18:28 -0400 Subject: [PATCH 26/47] Use consistent naming: Args called "new_value" become "text" --- seleniumbase/fixtures/base_case.py | 74 +++++++++++++++--------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 0870d851274..8ee0c91a1de 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -253,7 +253,7 @@ def click_chain(self, selectors_list, by=By.CSS_SELECTOR, if spacing > 0: time.sleep(spacing) - def update_text(self, selector, new_value, by=By.CSS_SELECTOR, + def update_text(self, selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False): """ This method updates an element's text field with new text. Has multiple parts: @@ -264,7 +264,7 @@ def update_text(self, selector, new_value, by=By.CSS_SELECTOR, * Hits Enter/Submit (if the text ends in "\n"). @Params selector - the selector of the text field - new_value - the new value to type into the text field + text - the new text to type into the text field by - the type of selector to search by (Default: CSS Selector) timeout - how long to wait for the selector to be visible retry - if True, use JS if the Selenium text update fails @@ -294,16 +294,16 @@ def update_text(self, selector, new_value, by=By.CSS_SELECTOR, pass # Clearing the text field first isn't critical self.__demo_mode_pause_if_active(tiny=True) pre_action_url = self.driver.current_url - if type(new_value) is int or type(new_value) is float: - new_value = str(new_value) + if type(text) is int or type(text) is float: + text = str(text) try: - if not new_value.endswith('\n'): - element.send_keys(new_value) + if not text.endswith('\n'): + element.send_keys(text) if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: self.wait_for_ready_state_complete() else: - new_value = new_value[:-1] - element.send_keys(new_value) + text = text[:-1] + element.send_keys(text) element.send_keys(Keys.RETURN) if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: self.wait_for_ready_state_complete() @@ -313,21 +313,21 @@ def update_text(self, selector, new_value, by=By.CSS_SELECTOR, element = self.wait_for_element_visible( selector, by=by, timeout=timeout) element.clear() - if not new_value.endswith('\n'): - element.send_keys(new_value) + if not text.endswith('\n'): + element.send_keys(text) else: - new_value = new_value[:-1] - element.send_keys(new_value) + text = text[:-1] + element.send_keys(text) element.send_keys(Keys.RETURN) if settings.WAIT_FOR_RSC_ON_PAGE_LOADS: self.wait_for_ready_state_complete() except Exception: exc_message = self.__get_improved_exception_message() raise Exception(exc_message) - if (retry and element.get_attribute('value') != new_value and ( - not new_value.endswith('\n'))): + if (retry and element.get_attribute('value') != text and ( + not text.endswith('\n'))): logging.debug('update_text() is falling back to JavaScript!') - self.set_value(selector, new_value, by=by) + self.set_value(selector, text, by=by) if self.demo_mode: if self.driver.current_url != pre_action_url: self.__demo_mode_pause_if_active() @@ -385,7 +385,7 @@ def add_text(self, selector, text, by=By.CSS_SELECTOR, timeout=None): def type(self, selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False): - """ Same as update_text() + """ Same as self.update_text() This method updates an element's text field with new text. Has multiple parts: * Waits for the element to be visible. @@ -395,7 +395,7 @@ def type(self, selector, text, by=By.CSS_SELECTOR, * Hits Enter/Submit (if the text ends in "\n"). @Params selector - the selector of the text field - new_value - the new value to type into the text field + text - the new text to type into the text field by - the type of selector to search by (Default: CSS Selector) timeout - how long to wait for the selector to be visible retry - if True, use JS if the Selenium text update fails @@ -1973,11 +1973,11 @@ def highlight_click(self, selector, by=By.CSS_SELECTOR, self.highlight(selector, by=by, loops=loops, scroll=scroll) self.click(selector, by=by) - def highlight_update_text(self, selector, new_value, by=By.CSS_SELECTOR, + def highlight_update_text(self, selector, text, by=By.CSS_SELECTOR, loops=3, scroll=True): if not self.demo_mode: self.highlight(selector, by=by, loops=loops, scroll=scroll) - self.update_text(selector, new_value, by=by) + self.update_text(selector, text, by=by) def highlight(self, selector, by=By.CSS_SELECTOR, loops=None, scroll=True): @@ -2821,7 +2821,7 @@ def convert_to_css_selector(self, selector, by): "Exception: Could not convert {%s}(by=%s) to CSS_SELECTOR!" % ( selector, by)) - def set_value(self, selector, new_value, by=By.CSS_SELECTOR, timeout=None): + def set_value(self, selector, text, by=By.CSS_SELECTOR, timeout=None): """ This method uses JavaScript to update a text field. """ if not timeout: timeout = settings.LARGE_TIMEOUT @@ -2833,14 +2833,16 @@ def set_value(self, selector, new_value, by=By.CSS_SELECTOR, timeout=None): self.__demo_mode_highlight_if_active(orginal_selector, by) if not self.demo_mode: self.scroll_to(orginal_selector, by=by, timeout=timeout) - value = re.escape(new_value) + if type(text) is int or type(text) is float: + text = str(text) + value = re.escape(text) value = self.__escape_quotes_if_needed(value) css_selector = re.escape(css_selector) css_selector = self.__escape_quotes_if_needed(css_selector) script = ("""document.querySelector('%s').value='%s';""" % (css_selector, value)) self.execute_script(script) - if new_value.endswith('\n'): + if text.endswith('\n'): element = self.wait_for_element_present( orginal_selector, by=by, timeout=timeout) element.send_keys(Keys.RETURN) @@ -2876,12 +2878,12 @@ def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR, selector = self.convert_to_css_selector(selector, by=by) selector = self.__make_css_match_first_element_only(selector) selector = self.__escape_quotes_if_needed(selector) - new_value = re.escape(new_value) - new_value = self.__escape_quotes_if_needed(new_value) + text = re.escape(text) + text = self.__escape_quotes_if_needed(text) update_text_script = """jQuery('%s').val('%s')""" % ( - selector, new_value) + selector, text) self.safe_execute_script(update_text_script) - if new_value.endswith('\n'): + if text.endswith('\n'): element.send_keys('\n') self.__demo_mode_pause_if_active() @@ -2974,36 +2976,36 @@ def get_session_storage_items(self): # Duplicates (Avoids name confusion when migrating from other frameworks.) def open_url(self, url): - """ Same as open() - Original saved for backwards compatibility. """ + """ Same as self.open() """ self.open(url) def visit(self, url): - """ Same as open() - Some test frameworks use this method name. """ + """ Same as self.open() """ self.open(url) def visit_url(self, url): - """ Same as open() - Some test frameworks use this method name. """ + """ Same as self.open() """ self.open(url) def goto(self, url): - """ Same as open() - Some test frameworks use this method name. """ + """ Same as self.open() """ self.open(url) def go_to(self, url): - """ Same as open() - Some test frameworks use this method name. """ + """ Same as self.open() """ self.open(url) def reload(self): - """ Same as refresh_page() """ + """ Same as self.refresh_page() """ self.refresh_page() def reload_page(self): - """ Same as refresh_page() """ + """ Same as self.refresh_page() """ self.refresh_page() def input(self, selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False): - """ Same as update_text() """ + """ Same as self.update_text() """ if not timeout: timeout = settings.LARGE_TIMEOUT if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: @@ -3013,7 +3015,7 @@ def input(self, selector, text, by=By.CSS_SELECTOR, def write(self, selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False): - """ Same as update_text() """ + """ Same as self.update_text() """ if not timeout: timeout = settings.LARGE_TIMEOUT if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: @@ -3022,7 +3024,7 @@ def write(self, selector, text, by=By.CSS_SELECTOR, self.update_text(selector, text, by=by, timeout=timeout, retry=retry) def send_keys(self, selector, text, by=By.CSS_SELECTOR, timeout=None): - """ Same as add_text() """ + """ Same as self.add_text() """ if not timeout: timeout = settings.LARGE_TIMEOUT if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: From 702f69bd8dfb893d2bbfc98ca3be910256416e9c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 19:21:48 -0400 Subject: [PATCH 27/47] Add self.js_type(s, t) and make arg-naming consistent --- seleniumbase/fixtures/base_case.py | 51 ++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 8ee0c91a1de..7a2598e9e0a 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -2850,21 +2850,60 @@ def set_value(self, selector, text, by=By.CSS_SELECTOR, timeout=None): self.wait_for_ready_state_complete() self.__demo_mode_pause_if_active() - def js_update_text(self, selector, new_value, by=By.CSS_SELECTOR, + def js_update_text(self, selector, text, by=By.CSS_SELECTOR, timeout=None): - """ Same as self.set_value() """ + """ JavaScript + send_keys are used to update a text field. + Performs self.set_value() and triggers event listeners. + If text ends in "\n", set_value() presses RETURN after. + Works faster than send_keys() alone due to the JS call. + """ + if not timeout: + timeout = settings.LARGE_TIMEOUT + if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: + timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if type(text) is int or type(text) is float: + text = str(text) + self.set_value( + selector, text, by=by, timeout=timeout) + if not text.endswith('\n'): + try: + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout=0.2) + element.send_keys(" \b") + except Exception: + pass + + def js_type(self, selector, text, by=By.CSS_SELECTOR, + timeout=None): + """ Same as self.js_update_text() + JavaScript + send_keys are used to update a text field. + Performs self.set_value() and triggers event listeners. + If text ends in "\n", set_value() presses RETURN after. + Works faster than send_keys() alone due to the JS call. + """ if not timeout: timeout = settings.LARGE_TIMEOUT if self.timeout_multiplier and timeout == settings.LARGE_TIMEOUT: timeout = self.__get_new_timeout(timeout) + selector, by = self.__recalculate_selector(selector, by) + if type(text) is int or type(text) is float: + text = str(text) self.set_value( - selector, new_value, by=by, timeout=timeout) + selector, text, by=by, timeout=timeout) + if not text.endswith('\n'): + try: + element = page_actions.wait_for_element_present( + self.driver, selector, by, timeout=0.2) + element.send_keys(" \b") + except Exception: + pass - def jquery_update_text(self, selector, new_value, by=By.CSS_SELECTOR, + def jquery_update_text(self, selector, text, by=By.CSS_SELECTOR, timeout=None): """ This method uses jQuery to update a text field. - If the new_value string ends with the newline character, - WebDriver will finish the call, which simulates pressing + If the text string ends with the newline character, + Selenium finishes the call, which simulates pressing {Enter/Return} after the text is entered. """ if not timeout: timeout = settings.LARGE_TIMEOUT From 4b654cbf675beb3cf0b658a28687ee910e657a30 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 19:22:42 -0400 Subject: [PATCH 28/47] Update method summary --- help_docs/method_summary.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index e6df0b52624..d78ea8fe3ff 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -23,7 +23,7 @@ self.double_click(selector, by=By.CSS_SELECTOR, timeout=None) self.click_chain(selectors_list, by=By.CSS_SELECTOR, timeout=None, spacing=0) -self.update_text(selector, new_value, by=By.CSS_SELECTOR, timeout=None, retry=False) +self.update_text(selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False) # Duplicates: self.type(selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False) # self.input(selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False) # self.write(selector, text, by=By.CSS_SELECTOR, timeout=None, retry=False) @@ -195,7 +195,7 @@ self.bring_to_front(selector, by=By.CSS_SELECTOR) self.highlight_click(selector, by=By.CSS_SELECTOR, loops=3, scroll=True) -self.highlight_update_text(selector, new_value, by=By.CSS_SELECTOR, loops=3, scroll=True) +self.highlight_update_text(selector, text, by=By.CSS_SELECTOR, loops=3, scroll=True) self.highlight(selector, by=By.CSS_SELECTOR, loops=4, scroll=True) @@ -302,11 +302,12 @@ self.convert_xpath_to_css(xpath) self.convert_to_css_selector(selector, by) -self.set_value(selector, new_value, by=By.CSS_SELECTOR, timeout=None) +self.set_value(selector, text, by=By.CSS_SELECTOR, timeout=None) -self.js_update_text(selector, new_value, by=By.CSS_SELECTOR, timeout=None) +self.js_update_text(selector, text, by=By.CSS_SELECTOR, timeout=None) +# Duplicates: self.js_type() -self.jquery_update_text(selector, new_value, by=By.CSS_SELECTOR, timeout=None) +self.jquery_update_text(selector, text, by=By.CSS_SELECTOR, timeout=None) self.set_time_limit(time_limit) From 82490abf3cb3daca4de3489de13911952239c7fc Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Fri, 12 Jun 2020 21:41:40 -0400 Subject: [PATCH 29/47] Boost speed by optimizing the location of imports --- seleniumbase/core/browser_launcher.py | 4 ++-- seleniumbase/fixtures/base_case.py | 24 ++++++++++++++++-------- seleniumbase/fixtures/js_utils.py | 2 +- seleniumbase/fixtures/page_actions.py | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 7e3c36f29f2..f4c6f87bce4 100755 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -4,7 +4,6 @@ import random import re import sys -import threading import time import urllib3 import warnings @@ -15,7 +14,6 @@ from seleniumbase.config import settings from seleniumbase.core import download_helper from seleniumbase.core import proxy_helper -from seleniumbase.core import capabilities_parser from seleniumbase.fixtures import constants from seleniumbase.fixtures import page_utils from seleniumbase import drivers # webdriver storage folder for SeleniumBase @@ -106,6 +104,7 @@ def _add_chrome_proxy_extension( proxy_helper.create_proxy_zip(proxy_string, proxy_user, proxy_pass) else: # Pytest multi-threaded test + import threading lock = threading.Lock() with lock: time.sleep(random.uniform(0.02, 0.15)) @@ -421,6 +420,7 @@ def get_remote_driver( desired_caps = {} extra_caps = {} if cap_file: + from seleniumbase.core import capabilities_parser desired_caps = capabilities_parser.get_desired_capabilities(cap_file) if cap_string: try: diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 7a2598e9e0a..76a996ee920 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -32,7 +32,6 @@ def test_anything(self): import time import urllib3 import unittest -import uuid from selenium.common.exceptions import (StaleElementReferenceException, MoveTargetOutOfBoundsException, WebDriverException) @@ -40,17 +39,11 @@ def test_anything(self): from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.remote.remote_connection import LOGGER -from selenium.webdriver.support.ui import Select from seleniumbase import config as sb_config from seleniumbase.common import decorators from seleniumbase.config import settings -from seleniumbase.core.testcase_manager import TestcaseDataPayload -from seleniumbase.core.testcase_manager import TestcaseManager -from seleniumbase.core import download_helper from seleniumbase.core import log_helper -from seleniumbase.core import settings_parser from seleniumbase.core import tour_helper -from seleniumbase.core import visual_helper from seleniumbase.fixtures import constants from seleniumbase.fixtures import js_utils from seleniumbase.fixtures import page_actions @@ -1307,6 +1300,7 @@ def __select_option(self, dropdown_selector, option, """ Selects an HTML