From 420e845d9488abfbc647a1eb4118909835f740ab Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:06:31 -0400 Subject: [PATCH 01/12] Add methods for getting status codes from links --- seleniumbase/fixtures/base_case.py | 37 +++++++++++++ seleniumbase/fixtures/page_utils.py | 85 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 9579e505..db5da730 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1622,6 +1622,43 @@ class BaseCase(unittest.TestCase): soup = BeautifulSoup(source, "html.parser") return soup + def get_unique_links(self): + """ Get all unique links in the html of the page source. + Page links include those obtained from: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src". """ + page_url = self.get_current_url() + soup = self.get_beautiful_soup(self.get_page_source()) + links = page_utils._get_unique_links(page_url, soup) + return links + + def get_link_status_code(self, link, allow_redirects=False, timeout=5): + """ Get the status code of a link. + If the timeout is exceeded, will return a 404. + For a list of available status codes, see: + https://en.wikipedia.org/wiki/List_of_HTTP_status_codes """ + status_code = page_utils._get_link_status_code( + link, allow_redirects=allow_redirects, timeout=timeout) + return status_code + + def assert_no_404_errors(self): + """ Assert no 404 errors from page links obtained from: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src". """ + links = self.get_unique_links() + for link in links: + status_code = str(self.get_link_status_code(link)) + bad_link_str = 'Error: "%s" returned a 404!' % link + self.assert_not_equal(status_code, "404", bad_link_str) + + def print_unique_links_with_status_codes(self): + """ Finds all unique links in the html of the page source + and then prints out those links with their status codes. + Format: ["link" -> "status_code"] (per line) + Page links include those obtained from: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src". """ + page_url = self.get_current_url() + soup = self.get_beautiful_soup(self.get_page_source()) + page_utils._print_unique_links_with_status_codes(page_url, soup) + def safe_execute_script(self, script): """ When executing a script that contains a jQuery command, it's important that the jQuery library has been loaded first. diff --git a/seleniumbase/fixtures/page_utils.py b/seleniumbase/fixtures/page_utils.py index 822f3491..ab674ef6 100755 --- a/seleniumbase/fixtures/page_utils.py +++ b/seleniumbase/fixtures/page_utils.py @@ -65,6 +65,91 @@ def is_valid_url(url): return False +def _get_unique_links(page_url, soup): + """ + Returns all unique links. + Includes: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src" links. + """ + prefix = 'http:' + if page_url.startswith('https:'): + prefix = 'https:' + simple_url = page_url.split('://')[1] + base_url = simple_url.split('/')[0] + full_base_url = prefix + "//" + base_url + + raw_links = [] + raw_unique_links = [] + + # Get "href" from all "a" tags + links = soup.find_all('a') + for link in links: + raw_links.append(link.get('href')) + + # Get "src" from all "img" tags + img_links = soup.find_all('img') + for img_link in img_links: + raw_links.append(img_link.get('src')) + + # Get "href" from all "link" tags + links = soup.find_all('link') + for link in links: + raw_links.append(link.get('href')) + + # Get "src" from all "script" tags + img_links = soup.find_all('script') + for img_link in img_links: + raw_links.append(img_link.get('src')) + + for link in raw_links: + if link not in raw_unique_links: + raw_unique_links.append(link) + + unique_links = [] + for link in raw_unique_links: + if link and len(link) > 1: + if link.startswith('//'): + link = prefix + link + elif link.startswith('/'): + link = full_base_url + link + elif link.startswith('#'): + link = full_base_url + link + else: + pass + unique_links.append(link) + + return unique_links + + +def _get_link_status_code(link, allow_redirects=False, timeout=5): + """ Get the status code of a link. + If the timeout is exceeded, will return a 404. + For a list of available status codes, see: + https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + """ + status_code = None + try: + response = requests.get( + link, allow_redirects=allow_redirects, timeout=timeout) + status_code = response.status_code + except Exception: + status_code = 404 + return status_code + + +def _print_unique_links_with_status_codes(page_url, soup): + """ Finds all unique links in the html of the page source + and then prints out those links with their status codes. + Format: ["link" -> "status_code"] (per line) + Page links include those obtained from: + "a"->"href", "img"->"src", "link"->"href", and "script"->"src". + """ + links = _get_unique_links(page_url, soup) + for link in links: + status_code = _get_link_status_code(link) + print(link, " -> ", status_code) + + def _download_file_to(file_url, destination_folder, new_file_name=None): if new_file_name: file_name = new_file_name From fa02034748144685cf2ce7875f218130835e1fa5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:09:21 -0400 Subject: [PATCH 02/12] Refactoring and renaming --- seleniumbase/fixtures/base_case.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index db5da730..7be503b5 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -71,8 +71,8 @@ class BaseCase(unittest.TestCase): self.env = None # Add a shortened version of self.environment self.__last_url_of_delayed_assert = "data:," self.__last_page_load_url = "data:," - self.__page_check_count = 0 - self.__page_check_failures = [] + self.__delayed_assert_count = 0 + self.__delayed_assert_failures = [] # Requires self._* instead of self.__* for external class use self._html_report_extra = [] # (Used by pytest_plugin.py) self._default_driver = None @@ -2287,7 +2287,7 @@ class BaseCase(unittest.TestCase): return True # For backwards compatibility, earlier method names of the next - # four methods have remained even though they do the same thing, + # three methods have remained even though they do the same thing, # with the exception of assert_*, which won't return the element, # but like the others, will raise an exception if the call fails. @@ -2609,16 +2609,16 @@ class BaseCase(unittest.TestCase): """ Add a delayed_assert failure into a list for future processing. """ current_url = self.driver.current_url message = self.__get_exception_message() - self.__page_check_failures.append( + self.__delayed_assert_failures.append( "CHECK #%s: (%s)\n %s" % ( - self.__page_check_count, current_url, message)) + self.__delayed_assert_count, current_url, message)) def delayed_assert_element(self, selector, by=By.CSS_SELECTOR, timeout=settings.MINI_TIMEOUT): """ A non-terminating assertion for an element on a page. Failures will be saved until the process_delayed_asserts() method is called from inside a test, likely at the end of it. """ - self.__page_check_count += 1 + self.__delayed_assert_count += 1 try: url = self.get_current_url() if url == self.__last_url_of_delayed_assert: @@ -2639,7 +2639,7 @@ class BaseCase(unittest.TestCase): """ A non-terminating assertion for text from an element on a page. Failures will be saved until the process_delayed_asserts() method is called from inside a test, likely at the end of it. """ - self.__page_check_count += 1 + self.__delayed_assert_count += 1 try: url = self.get_current_url() if url == self.__last_url_of_delayed_assert: @@ -2666,12 +2666,12 @@ class BaseCase(unittest.TestCase): the delayed asserts on a single html page so that the failure screenshot matches the location of the delayed asserts. If "print_only" is set to True, the exception won't get raised. """ - if self.__page_check_failures: + if self.__delayed_assert_failures: exception_output = '' exception_output += "\n*** DELAYED ASSERTION FAILURES FOR: " exception_output += "%s\n" % self.id() - all_failing_checks = self.__page_check_failures - self.__page_check_failures = [] + all_failing_checks = self.__delayed_assert_failures + self.__delayed_assert_failures = [] for tb in all_failing_checks: exception_output += "%s\n" % tb if print_only: @@ -3017,7 +3017,7 @@ class BaseCase(unittest.TestCase): has_exception = True else: has_exception = sys.exc_info()[1] is not None - if self.__page_check_failures: + if self.__delayed_assert_failures: print( "\nWhen using self.delayed_assert_*() methods in your tests, " "remember to call self.process_delayed_asserts() afterwards. " From 19af2edc6dbd30ea44df84e1970fcf206f463e3c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:10:20 -0400 Subject: [PATCH 03/12] Include the message in the assert methods --- seleniumbase/fixtures/base_case.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 7be503b5..17792f6c 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -1712,16 +1712,16 @@ class BaseCase(unittest.TestCase): assert os.path.exists(self.get_path_of_downloaded_file(file)) def assert_true(self, expr, msg=None): - self.assertTrue(expr, msg=None) + self.assertTrue(expr, msg=msg) def assert_false(self, expr, msg=None): - self.assertFalse(expr, msg=None) + self.assertFalse(expr, msg=msg) def assert_equal(self, first, second, msg=None): - self.assertEqual(first, second, msg=None) + self.assertEqual(first, second, msg=msg) def assert_not_equal(self, first, second, msg=None): - self.assertNotEqual(first, second, msg=None) + self.assertNotEqual(first, second, msg=msg) def assert_no_js_errors(self): """ Asserts that there are no JavaScript "SEVERE"-level page errors. From 0e4bb615556e680f63d64d83070a4ccb482a7629 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:12:02 -0400 Subject: [PATCH 04/12] Add methods for setting up the visual_baseline folder --- seleniumbase/core/visual_helper.py | 19 +++++++++++++++++++ seleniumbase/fixtures/constants.py | 4 ++++ 2 files changed, 23 insertions(+) create mode 100755 seleniumbase/core/visual_helper.py diff --git a/seleniumbase/core/visual_helper.py b/seleniumbase/core/visual_helper.py new file mode 100755 index 00000000..21cc4ec4 --- /dev/null +++ b/seleniumbase/core/visual_helper.py @@ -0,0 +1,19 @@ +import os +from seleniumbase.fixtures import constants + +VISUAL_BASELINE_DIR = constants.VisualBaseline.STORAGE_FOLDER +abs_path = os.path.abspath('.') +visual_baseline_path = os.path.join(abs_path, VISUAL_BASELINE_DIR) + + +def get_visual_baseline_folder(): + return visual_baseline_path + + +def visual_baseline_folder_setup(): + """ Handle Logging """ + if not os.path.exists(visual_baseline_path): + try: + os.makedirs(visual_baseline_path) + except Exception: + pass # Should only be reachable during multi-threaded runs diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index a434fae6..a9ee1e24 100755 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -19,6 +19,10 @@ class Files: ARCHIVED_DOWNLOADS_FOLDER = "archived_files" +class VisualBaseline: + STORAGE_FOLDER = "visual_baseline" + + class JQuery: VER = "3.3.1" # MIN_JS = "//cdnjs.cloudflare.com/ajax/libs/jquery/%s/jquery.min.js" % VER From dfbe75ca718c1f18ba4c3855c1a404293ab31bc8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:15:36 -0400 Subject: [PATCH 05/12] Add command line option to set/reset the visual baseline --- seleniumbase/plugins/pytest_plugin.py | 8 ++++++++ seleniumbase/plugins/selenium_plugin.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 2e4d5ce6..34ac8cd8 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -173,6 +173,13 @@ def pytest_addoption(parser): default=False, help="""Take a screenshot on last page after the last step of the test. (Added to the "latest_logs" folder.)""") + parser.addoption('--visual_baseline', action='store_true', + dest='visual_baseline', + default=False, + help="""Setting this resets the visual baseline for + Automated Visual Testing with SeleniumBase. + When a test calls self.check_window(), it will + rebuild its files in the visual_baseline folder.""") parser.addoption('--timeout_multiplier', action='store', dest='timeout_multiplier', default=None, @@ -212,6 +219,7 @@ def pytest_configure(config): sb_config.verify_delay = config.getoption('verify_delay') sb_config.disable_csp = config.getoption('disable_csp') sb_config.save_screenshot = config.getoption('save_screenshot') + sb_config.visual_baseline = config.getoption('visual_baseline') sb_config.timeout_multiplier = config.getoption('timeout_multiplier') if sb_config.with_testing_base: diff --git a/seleniumbase/plugins/selenium_plugin.py b/seleniumbase/plugins/selenium_plugin.py index f6f71cd3..743c6275 100755 --- a/seleniumbase/plugins/selenium_plugin.py +++ b/seleniumbase/plugins/selenium_plugin.py @@ -30,6 +30,7 @@ class SeleniumBrowser(Plugin): self.options.verify_delay -- delay before MasterQA checks (--verify_delay) self.options.disable_csp -- disable Content Security Policy (--disable_csp) self.options.save_screenshot -- save screen after test (--save_screenshot) + self.options.visual_baseline -- set the visual baseline (--visual_baseline) self.options.timeout_multiplier -- increase defaults (--timeout_multiplier) """ name = 'selenium' # Usage: --with-selenium @@ -144,6 +145,14 @@ class SeleniumBrowser(Plugin): default=False, help="""Take a screenshot on last page after the last step of the test. (Added to the "latest_logs" folder.)""") + parser.add_option( + '--visual_baseline', action='store_true', + dest='visual_baseline', + default=False, + help="""Setting this resets the visual baseline for + Automated Visual Testing with SeleniumBase. + When a test calls self.check_window(), it will + rebuild its files in the visual_baseline folder.""") parser.add_option( '--timeout_multiplier', action='store', dest='timeout_multiplier', @@ -176,6 +185,7 @@ class SeleniumBrowser(Plugin): test.test.verify_delay = self.options.verify_delay # MasterQA test.test.disable_csp = self.options.disable_csp test.test.save_screenshot_after_test = self.options.save_screenshot + test.test.visual_baseline = self.options.visual_baseline test.test.timeout_multiplier = self.options.timeout_multiplier test.test.use_grid = False if test.test.servername != "localhost": From ded0dad7c991c46e116de024852160c446bff833 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 02:16:39 -0400 Subject: [PATCH 06/12] Add visual_baseline to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 097a1ab5..9cc487f2 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ report.xml # Tours tours_exported +# Automated Visual Testing +visual_baseline + # Other selenium-server-standalone.jar proxy.zip From 0950aef7b18d045bf0901166ca3adc820a2d6dbe Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:04:48 -0400 Subject: [PATCH 07/12] Add check_window() for making visual comparison validations --- seleniumbase/fixtures/base_case.py | 196 +++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index 17792f6c..ce70179a 100755 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -21,6 +21,8 @@ Page elements are given enough time to load before WebDriver acts on them. Code becomes greatly simplified and easier to maintain. """ +import codecs +import json import logging import math import os @@ -38,6 +40,7 @@ from seleniumbase.core.testcase_manager import TestcaseManager from seleniumbase.core import download_helper from seleniumbase.core import log_helper 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 @@ -2436,6 +2439,198 @@ class BaseCase(unittest.TestCase): def switch_to_default_window(self): self.switch_to_window(0) + def check_window(self, name="default", level=0, baseline=False): + """ *** Automated Visual Testing with SeleniumBase *** + + The first time a test calls self.check_window() for a unique "name" + parameter provided, it will set a visual baseline, meaning that it + creates a folder, saves the URL to a file, saves the current window + screenshot to a file, and creates the following three files + with the listed data saved: + tags_level1.txt -> HTML tags from the window + tags_level2.txt -> HTML tags + attributes from the window + tags_level3.txt -> HTML tags + attributes/values from the window + + Baseline folders are named based on the test name and the name + parameter passed to self.check_window(). The same test can store + multiple baseline folders. + + If the baseline is being set/reset, the "level" doesn't matter. + + After the first run of self.check_window(), it will compare the + HTML tags of the latest window to the one from the initial run. + Here's how the level system works: + * level=0 -> + DRY RUN ONLY - Will perform a comparison to the baseline, and + print out any differences that are found, but + won't fail the test even if differences exist. + * level=1 -> + HTML tags are compared to tags_level1.txt + * level=2 -> + HTML tags are compared to tags_level1.txt and + HTML tags/attributes are compared to tags_level2.txt + * level=3 -> + HTML tags are compared to tags_level1.txt and + HTML tags + attributes are compared to tags_level2.txt and + HTML tags + attributes/values are compared to tags_level3.txt + As shown, Level-3 is the most strict, Level-1 is the least strict. + If the comparisons from the latest window to the existing baseline + don't match, the current test will fail, except for Level-0 tests. + + You can reset the visual baseline on the command line by using: + --visual_baseline + As long as "--visual_baseline" is used on the command line while + running tests, the self.check_window() method cannot fail because + it will rebuild the visual baseline rather than comparing the html + tags of the latest run to the existing baseline. If there are any + expected layout changes to a website that you're testing, you'll + need to reset the baseline to prevent unnecessary failures. + + self.check_window() will fail with "Page Domain Mismatch Failure" + if the page domain doesn't match the domain of the baseline. + + If you want to use self.check_window() to compare a web page to + a later version of itself from within the same test run, you can + add the parameter "baseline=True" to the first time you call + self.check_window() in a test to use that as the baseline. This + only makes sense if you're calling self.check_window() more than + once with the same name parameter in the same test. + + Automated Visual Testing with self.check_window() is not very + effective for websites that have dynamic content that changes + the layout and structure of web pages. For those, you're much + better off using regular SeleniumBase functional testing. + + Example usage: + self.check_window(name="testing", level=0) + self.check_window(name="xkcd_home", level=1) + self.check_window(name="github_page", level=2) + self.check_window(name="wikipedia_page", level=3) + """ + if level == "0": + level = 0 + if level == "1": + level = 1 + if level == "2": + level = 2 + if level == "3": + level = 3 + if level != 0 and level != 1 and level != 2 and level != 3: + raise Exception('Parameter "level" must be set to 0, 1, 2, or 3!') + + module = self.__class__.__module__ + if '.' in module and len(module.split('.')[-1]) > 1: + module = module.split('.')[-1] + test_id = "%s.%s" % (module, self._testMethodName) + if not name or len(name) < 1: + name = "default" + name = str(name) + visual_helper.visual_baseline_folder_setup() + baseline_dir = constants.VisualBaseline.STORAGE_FOLDER + visual_baseline_path = baseline_dir + "/" + test_id + "/" + name + page_url_file = visual_baseline_path + "/page_url.txt" + screenshot_file = visual_baseline_path + "/screenshot.png" + level_1_file = visual_baseline_path + "/tags_level_1.txt" + level_2_file = visual_baseline_path + "/tags_level_2.txt" + level_3_file = visual_baseline_path + "/tags_level_3.txt" + + set_baseline = False + if baseline or self.visual_baseline: + set_baseline = True + if not os.path.exists(visual_baseline_path): + set_baseline = True + try: + os.makedirs(visual_baseline_path) + except Exception: + pass # Only reachable during multi-threaded test runs + if not os.path.exists(page_url_file): + set_baseline = True + if not os.path.exists(screenshot_file): + set_baseline = True + if not os.path.exists(level_1_file): + set_baseline = True + if not os.path.exists(level_2_file): + set_baseline = True + if not os.path.exists(level_3_file): + set_baseline = True + + page_url = self.get_current_url() + soup = self.get_beautiful_soup() + html_tags = soup.body.find_all() + level_1 = [[tag.name] for tag in html_tags] + level_1 = json.loads(json.dumps(level_1)) # Tuples become lists + level_2 = [[tag.name, sorted(tag.attrs.keys())] for tag in html_tags] + level_2 = json.loads(json.dumps(level_2)) # Tuples become lists + level_3 = [[tag.name, sorted(tag.attrs.items())] for tag in html_tags] + level_3 = json.loads(json.dumps(level_3)) # Tuples become lists + + if set_baseline: + self.save_screenshot("screenshot.png", visual_baseline_path) + out_file = codecs.open(page_url_file, "w+") + out_file.writelines(page_url) + out_file.close() + out_file = codecs.open(level_1_file, "w+") + out_file.writelines(json.dumps(level_1)) + out_file.close() + out_file = codecs.open(level_2_file, "w+") + out_file.writelines(json.dumps(level_2)) + out_file.close() + out_file = codecs.open(level_3_file, "w+") + out_file.writelines(json.dumps(level_3)) + out_file.close() + + if not set_baseline: + f = open(page_url_file, 'r') + page_url_data = f.read().strip() + f.close() + f = open(level_1_file, 'r') + level_1_data = json.loads(f.read()) + f.close() + f = open(level_2_file, 'r') + level_2_data = json.loads(f.read()) + f.close() + f = open(level_3_file, 'r') + level_3_data = json.loads(f.read()) + f.close() + + domain_fail = ( + "Page Domain Mismatch Failure: " + "Current Page Domain doesn't match the Page Domain of the " + "Baseline! Can't compare two completely different sites! " + "Run with --visual_baseline to reset the baseline!") + level_1_failure = ( + "\n\n*** Exception: Visual Diff Failure:\n" + "* HTML tags don't match the baseline!") + level_2_failure = ( + "\n\n*** Exception: Visual Diff Failure:\n" + "* HTML tag attributes don't match the baseline!") + level_3_failure = ( + "\n\n*** Exception: Visual Diff Failure:\n" + "* HTML tag attribute values don't match the baseline!") + + page_domain = self.get_domain_url(page_url) + page_data_domain = self.get_domain_url(page_url_data) + unittest.TestCase.maxDiff = 1000 + if level == 1 or level == 2 or level == 3: + self.assert_equal(page_domain, page_data_domain, domain_fail) + self.assert_equal(level_1, level_1_data, level_1_failure) + unittest.TestCase.maxDiff = None + if level == 2 or level == 3: + self.assert_equal(level_2, level_2_data, level_2_failure) + if level == 3: + self.assert_equal(level_3, level_3_data, level_3_failure) + if level == 0: + try: + unittest.TestCase.maxDiff = 1000 + self.assert_equal( + page_domain, page_data_domain, domain_fail) + self.assert_equal(level_1, level_1_data, level_1_failure) + unittest.TestCase.maxDiff = None + self.assert_equal(level_2, level_2_data, level_2_failure) + self.assert_equal(level_3, level_3_data, level_3_failure) + except Exception as e: + print(e) # Level-0 Dry Run (Only print the differences) + def save_screenshot(self, name, folder=None): """ The screenshot will be in PNG format. """ return page_actions.save_screenshot(self.driver, name, folder) @@ -2888,6 +3083,7 @@ class BaseCase(unittest.TestCase): self.verify_delay = sb_config.verify_delay self.disable_csp = sb_config.disable_csp self.save_screenshot_after_test = sb_config.save_screenshot + self.visual_baseline = sb_config.visual_baseline self.timeout_multiplier = sb_config.timeout_multiplier self.use_grid = False if self.servername != "localhost": From caa8ca8728d14fc6ec3dc7d3d1f112a9e2a557ed Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:05:24 -0400 Subject: [PATCH 08/12] Add a visual test to verify the check_window() method --- examples/visual_test.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 examples/visual_test.py diff --git a/examples/visual_test.py b/examples/visual_test.py new file mode 100755 index 00000000..beca6126 --- /dev/null +++ b/examples/visual_test.py @@ -0,0 +1,25 @@ +from seleniumbase import BaseCase + + +class AutomatedVisualTest(BaseCase): + + def test_applitools_helloworld(self): + self.open('https://applitools.com/helloworld?diff1') + print('Creating baseline in "visual_baseline" folder...') + self.check_window(name="helloworld", baseline=True) + self.click('a[href="?diff1"]') + # Verify html tags match previous version + self.check_window(name="helloworld", level=1) + # Verify html tags + attributes match previous version + self.check_window(name="helloworld", level=2) + # Verify html tags + attributes + values match previous version + self.check_window(name="helloworld", level=3) + # Change the page enough for a Level-3 comparison to fail + self.click("button") + self.check_window(name="helloworld", level=1) + self.check_window(name="helloworld", level=2) + with self.assertRaises(Exception): + self.check_window(name="helloworld", level=3) + # Now that we know the exception was raised as expected, + # let's print out the comparison results by running in Level-0. + self.check_window(name="helloworld", level=0) From fc6757040216cf53e42e7a3b7c03778d0cb5803d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:05:37 -0400 Subject: [PATCH 09/12] Update method summary --- help_docs/method_summary.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 74267225..7f3f2f01 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -172,6 +172,14 @@ self.get_domain_url(url) self.get_beautiful_soup(source=None) +self.get_unique_links() + +self.get_link_status_code(link, allow_redirects=False, timeout=5) + +self.assert_no_404_errors() + +self.print_unique_links_with_status_codes() + self.safe_execute_script(script) self.download_file(file_url, destination_folder=None) @@ -345,6 +353,8 @@ self.switch_to_window(window, timeout=settings.SMALL_TIMEOUT) self.switch_to_default_window() +self.check_window(name="default", level=1, baseline=False) + self.save_screenshot(name, folder=None) self.get_new_driver(browser=None, headless=None, servername=None, port=None, From 4985ef26ebc4e98e17b1538f85bd0e20074ff30b Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:06:25 -0400 Subject: [PATCH 10/12] Update pytest requirements --- requirements.txt | 6 +++--- setup.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 612553ee..cbf765c4 100755 --- a/requirements.txt +++ b/requirements.txt @@ -8,11 +8,11 @@ unittest2 selenium==3.141.0 requests==2.21.0 urllib3==1.24.1 -pytest>=4.3.1 +pytest>=4.4.0 pytest-cov>=2.6.1 pytest-html>=1.20.0 -pytest-rerunfailures>=6.0 -pytest-xdist>=1.26.1 +pytest-rerunfailures>=7.0 +pytest-xdist>=1.27.0 parameterized>=0.7.0 beautifulsoup4>=4.6.0 colorama==0.4.1 diff --git a/setup.py b/setup.py index ac41ed0b..42ed3b98 100755 --- a/setup.py +++ b/setup.py @@ -61,11 +61,11 @@ setup( 'selenium==3.141.0', 'requests==2.21.0', # Changing this may effect "urllib3" 'urllib3==1.24.1', # Keep this lib in sync with "requests" - 'pytest>=4.3.1', + 'pytest>=4.4.0', 'pytest-cov>=2.6.1', 'pytest-html>=1.20.0', - 'pytest-rerunfailures>=6.0', - 'pytest-xdist>=1.26.1', + 'pytest-rerunfailures>=7.0', + 'pytest-xdist>=1.27.0', 'parameterized>=0.7.0', 'beautifulsoup4>=4.6.0', # Keep at >=4.6.0 while using bs4 'colorama==0.4.1', From fffb886fd6a2d1a0b58b29aae7e6c94140504b0a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:06:48 -0400 Subject: [PATCH 11/12] Version 1.22.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42ed3b98..ff3026f9 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ except IOError: setup( name='seleniumbase', - version='1.21.9', + version='1.22.0', description='Reliable Browser Automation & Testing Framework', long_description=long_description, long_description_content_type='text/markdown', From 932ee2409f0a2ee54c20ff6128cb3752343d29a7 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Mon, 1 Apr 2019 03:12:11 -0400 Subject: [PATCH 12/12] Update default check_window level in method_summary --- help_docs/method_summary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help_docs/method_summary.md b/help_docs/method_summary.md index 7f3f2f01..4959032f 100755 --- a/help_docs/method_summary.md +++ b/help_docs/method_summary.md @@ -353,7 +353,7 @@ self.switch_to_window(window, timeout=settings.SMALL_TIMEOUT) self.switch_to_default_window() -self.check_window(name="default", level=1, baseline=False) +self.check_window(name="default", level=0, baseline=False) self.save_screenshot(name, folder=None)