diff --git a/django_expanded_test_cases/constants.py b/django_expanded_test_cases/constants.py index 7851687e3f34e623dc476338fae0e9850d62d85d..7b53a2171737d6e1830a80b458482208f7594b9c 100644 --- a/django_expanded_test_cases/constants.py +++ b/django_expanded_test_cases/constants.py @@ -17,6 +17,8 @@ UNDERLINE = '\u001b[4m' UNDERLINE_RESET = '\u001b[0m' +# region Console Color Options + # General output/color format settings. ETC_OUTPUT_ERROR_COLOR = str(getattr( settings, @@ -96,6 +98,11 @@ ETC_RESPONSE_DEBUG_USER_INFO_COLOR = str(getattr( Fore.MAGENTA if COLORAMA_PRESENT else '', )) +# endregion Console Color Options + + +# region Console Region Show/Hide Options + # Enabling/disabling output of specific sections. ETC_INCLUDE_RESPONSE_DEBUG_URL = bool(getattr( settings, @@ -138,6 +145,8 @@ ETC_INCLUDE_RESPONSE_DEBUG_USER_INFO = bool(getattr( True, )) +# endregion Console Region Show/Hide Options + # Void element list as defined at: # https://www.w3.org/TR/2011/WD-html-markup-20110113/syntax.html#void-element @@ -163,6 +172,8 @@ VOID_ELEMENT_LIST = [ ] +# region General Options + # Indicates whether the additional debug information should be output. ETC_DEBUG_PRINT = bool(getattr( settings, @@ -193,6 +204,57 @@ ETC_MATCH_ALL_CONTEXT_MESSAGES = bool(getattr( False, )) +# endregion General Options + + +# region Selenium Options + +# Browser to launch for selenium testing. Options of 'chrome'/'firefox' are good defaults. +ETC_SELENIUM_BROWSER = str(getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_SELENIUM_BROWSER', + 'chrome', +)) +# Run Selenium tests in "headless" mode. +# Default is to visually show browser. "Headless" will skipp visually rendering the browser while still running tests. +# Good for speed but bad for debugging issues. +ETC_SELENIUM_HEADLESS = bool(getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_SELENIUM_HEADLESS', + False, +)) +# Set browser/selenium cache to be ignored. +# Using the cache can sometimes lead to incorrect/failing tests when running selenium in a multi-threaded environment. +ETC_SELENIUM_DISABLE_CACHE = bool(getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_DISABLE_CACHE', + False, +)) +# Extra options to pass into browser. Should be some iterable, such as a list or tuple of strings. +ETC_SELENIUM_EXTRA_BROWSER_OPTIONS = getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_EXTRA_BROWSER_OPTIONS', + None, +) +# Number of seconds to wait on selenium page load before giving up. +# Refers to the full page itself loading. As in getting any page response at all. Default of 30 seconds. +ETC_SELENIUM_PAGE_TIMEOUT_DEFAULT = int(getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_SELENIUM_PAGE_TIMEOUT_DEFAULT', + 30, +)) +# Number of seconds to wait on selenium page element (constantly checking for existence) before giving up. +# Refers to a specific element loading within a page. Default of 5 seconds. +ETC_SELENIUM_IMPLICIT_WAIT_DEFAULT = int(getattr( + settings, + 'DJANGO_EXPANDED_TESTCASES_SELENIUM_IMPLICIT_WAIT_DEFAULT', + 5, +)) + +# endregion Selenium Options + + +# region User Handling Options # Controls if test-users should be automatically generated or not. ETC_AUTO_GENERATE_USERS = bool(getattr( @@ -250,6 +312,8 @@ ETC_GENERATE_USERS_WITH_REAL_NAMES = bool(getattr( False, )) +# endregion User Handling Options + # region User Identifiers diff --git a/django_expanded_test_cases/mixins/live_server_mixin.py b/django_expanded_test_cases/mixins/live_server_mixin.py index 67039883c2d3f9b0c6fe9fe3b8b07fba96c4792b..cbf820344cd1965940765863d16f34e47df4921c 100644 --- a/django_expanded_test_cases/mixins/live_server_mixin.py +++ b/django_expanded_test_cases/mixins/live_server_mixin.py @@ -17,40 +17,79 @@ from django_expanded_test_cases.constants import ( ETC_INCLUDE_RESPONSE_DEBUG_CONTENT, ETC_RESPONSE_DEBUG_CONTENT_COLOR, ETC_OUTPUT_EMPHASIS_COLOR, + + ETC_SELENIUM_PAGE_TIMEOUT_DEFAULT, + ETC_SELENIUM_IMPLICIT_WAIT_DEFAULT, ) from django_expanded_test_cases.mixins.response_mixin import ResponseTestCaseMixin +# Module Variables. +# Debug port, to get around st +SELENIUM_DEBUG_PORT = 9221 + + class LiveServerMixin(ResponseTestCaseMixin): """Universal logic for all selenium LiveServer test cases.""" # region Utility Functions - def create_driver(self): - """Creates new browser manager instance.""" + def create_driver(self, switch_window=True): + """Creates new browser manager instance. + Each driver represents one or more browser windows, each with a set of one or more tabs. + :param switch_window: Bool indicating if window should be immediately switched to after creation. + """ # Create instance, based on selected driver type. if self._browser == 'chrome': + # # Avoid possible error when many drivers are opened. + # # See https://stackoverflow.com/a/56638103 + global SELENIUM_DEBUG_PORT + SELENIUM_DEBUG_PORT += 1 + self._options.add_argument('--remote-debugging-port={0}'.format(SELENIUM_DEBUG_PORT)) driver = webdriver.Chrome(service=self._service, options=self._options) elif self._browser == 'firefox': driver = webdriver.Firefox(service=self._service, options=self._options) else: raise ValueError('Unknown browser "{0}".'.format(self._browser)) - # Make class aware of window. + # Set number of seconds to wait before giving up on page response. + driver.set_page_load_timeout(ETC_SELENIUM_PAGE_TIMEOUT_DEFAULT) + + # Set number of seconds to wait before giving up on page element. + driver.implicitly_wait(ETC_SELENIUM_IMPLICIT_WAIT_DEFAULT) + + # Make class aware of driver set. self._driver_set.append(driver) - # Set to check page for 5 seconds before giving up on element. - driver.implicitly_wait(5) + if switch_window: + # Assume we want to auto-switch windows. + driver.switch_to.window(driver.window_handles[-1]) return driver - def get_driver(self): - """Returns first driver off of driver stack, or creates new one if none are present.""" + def get_driver(self, index=-1, switch_window=True): + """Returns first driver off of driver stack, or creates new one if none are present. + + :param switch_window: Bool indicating if window should be immediately switched to after creation. + """ + + # Don't even attempt, index is not int. + if not isinstance(index, int): + return self.create_driver() + + # Attempt to get given index. try: - return self._driver_set[-1] + driver = self._driver_set[index] except IndexError: - return self.create_driver() + # Invalid index. Create new instead. + driver = self.create_driver() + + if switch_window: + # Assume we want to auto-switch windows. + driver.switch_to.window(driver.window_handles[-1]) + + return driver def close_driver(self, driver): """Closes provided browser manager instance. @@ -60,7 +99,7 @@ class LiveServerMixin(ResponseTestCaseMixin): # Remove reference in class. self._driver_set.remove(driver) - # Close window. + # Close driver itself. driver.quit() def close_all_drivers(self): @@ -74,12 +113,9 @@ class LiveServerMixin(ResponseTestCaseMixin): :param driver: Driver manager object to generate window for. :return: New focus window. """ - # Open blank new window. + # Open blank new window and automatically switch. driver.switch_to.new_window('window') - # Switch to recently created window. - return self.switch_to_window_at_index(driver, len(driver.window_handles) - 1) - def open_new_tab(self, driver): """Opens a new window for the provided driver. @@ -90,29 +126,41 @@ class LiveServerMixin(ResponseTestCaseMixin): driver.switch_to.new_window('tab') # Switch to recently created window. - return self.switch_to_window_at_index(driver, len(driver.window_handles) - 1) + return self.switch_to_window(driver, len(driver.window_handles) - 1) - def close_window_at_index(self, driver, window_index): + def close_window(self, driver, window_index): """Closes a window at specific index for the provided driver. :param driver: Driver manager object containing the desired window. :param window_index: Index of window to close. """ - # Attempt to get window at specified driver index. - try: - focus_window = driver.window_handles[window_index] - except IndexError: - err_msg = 'Attempted to close to window of index "{0}", but driver only has "{1}" windows open.'.format( - window_index, - len(driver.window_handles) - ) - raise IndexError(err_msg) - # Close window. - self.switch_to_window_at_index(driver, window_index) - driver.execute_script('window.close();') + self.switch_to_window(driver, window_index) + driver.close() - def switch_to_window_at_index(self, driver, window_index): + def close_all_windows(self, driver): + """Closes all open windows for a given driver. + + :param driver: Driver manager object to close windows for. + """ + # Iterate upon all windows until none remain. + while len(driver.window_handles) > 1: + self.close_window(driver, 0) + + def close_all_other_windows(self, driver, window_index_to_keep): + """Closes all open windows, except for window at given index. + + :param driver: Driver manager object containing the desired window. + :param window_index: Index of window to keep open. + """ + # Iterate upon all windows until none remain. + while len(driver.window_handles) > 2: + if window_index_to_keep == 0: + self.close_window_at_index(driver, 1) + else: + self.close_window_at_index(driver, 0) + + def switch_to_window(self, driver, window_index): """Sets window at specific driver/index to be the current focus. :param driver: Driver manager object containing the desired window. @@ -123,7 +171,7 @@ class LiveServerMixin(ResponseTestCaseMixin): try: focus_window = driver.window_handles[window_index] except IndexError: - err_msg = 'Attempted to switch to window of index "{0}", but driver only has "{1}" windows open.'.format( + err_msg = 'Attempted to switch to window of index "{0}", but driver only has {1} windows open.'.format( window_index, len(driver.window_handles) ) @@ -135,15 +183,22 @@ class LiveServerMixin(ResponseTestCaseMixin): # Return newly switched window. return focus_window + def sleep_driver(self, seconds): + """Halts driver for provided number of seconds. + + Useful for visually verifying browser state, when trying to debug tests. + """ + time.sleep(seconds) + def sleep_browser(self, seconds): - """Halts browser for provided number of seconds. + """Halts browser for provided number of seconds. Alias for sleep_driver(). Useful for visually verifying browser state, when trying to debug tests. """ time.sleep(seconds) def is_webdriver(self, obj): - """Verifies if object matches set on known web driver types.""" + """Verifies if object matches set of known web driver types.""" # Import all known drivers for type checking. from selenium.webdriver.chrome.webdriver import WebDriver as ChromeDriver @@ -502,6 +557,50 @@ class LiveServerMixin(ResponseTestCaseMixin): self.show_debug_data(content) raise err + def find_elements_by_text(self, content, text, element_type=None): + """Finds all HTML elements that match the provided inner text. + + :param content: Content to search through. + :param text: Element text to search for. + :param element_type: Optionally filter by type of element as well (h1, p, li, etc). + """ + self.current_url = None + + # Handle if webdriver was provided. + # Otherwise assume was standard "page content". + if self.is_webdriver(content): + self.current_url = content.current_url + content = content.page_source + + try: + # Return original parent call with correct variables. + return super().find_elements_by_text(content, text, element_type=element_type) + except Exception as err: + self.show_debug_data(content) + raise err + + def find_element_by_text(self, content, text, element_type=None): + """Finds first HTML element that matches the provided inner text. + + :param content: Content to search through. + :param text: Element text to search for. + :param element_type: Optionally filter by type of element as well (h1, p, li, etc). + """ + self.current_url = None + + # Handle if webdriver was provided. + # Otherwise assume was standard "page content". + if self.is_webdriver(content): + self.current_url = content.current_url + content = content.page_source + + try: + # Return original parent call with correct variables. + return super().find_element_by_text(content, text, element_type=element_type) + except Exception as err: + self.show_debug_data(content) + raise err + # endregion Html Search Functions diff --git a/django_expanded_test_cases/mixins/response_mixin.py b/django_expanded_test_cases/mixins/response_mixin.py index 5319a92ce25f8edc1867d77bc6530d6a47e14a93..2bc86c921c10542a70e38597b1cf34c675e1813d 100644 --- a/django_expanded_test_cases/mixins/response_mixin.py +++ b/django_expanded_test_cases/mixins/response_mixin.py @@ -822,10 +822,11 @@ class ResponseTestCaseMixin(CoreTestCaseMixin): return element_list[0] def find_elements_by_text(self, content, text, element_type=None): - """Finds all HTML elements that contain the provided text. + """Finds all HTML elements that contain the provided inner text. :param content: Content to search through. :param text: Element text to search for. + :param element_type: Optionally filter by type of element as well (h1, p, li, etc). """ # Ensure response content is in expected minimized format. content = self.get_minimized_response_content(content) @@ -854,10 +855,11 @@ class ResponseTestCaseMixin(CoreTestCaseMixin): return element_list def find_element_by_text(self, content, text, element_type=None): - """Finds first HTML element that matches the provided text. + """Finds first HTML element that matches the provided inner text. :param content: Content to search through. :param text: Element text to search for. + :param element_type: Optionally filter by type of element as well (h1, p, li, etc). """ # Ensure response content is in expected minimized format. content = self.get_minimized_response_content(content) diff --git a/django_expanded_test_cases/test_cases/live_server_test_case.py b/django_expanded_test_cases/test_cases/live_server_test_case.py index 9bf0b988799c2c0db2f71cd2861601c343700c78..cd92c17707a44726778e160946e86e21a3eb1375 100644 --- a/django_expanded_test_cases/test_cases/live_server_test_case.py +++ b/django_expanded_test_cases/test_cases/live_server_test_case.py @@ -12,9 +12,14 @@ from django.test import LiveServerTestCase as DjangoLiveServerTestCase from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FireFoxService -from django.conf import settings # Internal Imports. +from django_expanded_test_cases.constants import ( + ETC_SELENIUM_BROWSER, + ETC_SELENIUM_HEADLESS, + ETC_SELENIUM_DISABLE_CACHE, + ETC_SELENIUM_EXTRA_BROWSER_OPTIONS, +) from django_expanded_test_cases.mixins.live_server_mixin import LiveServerMixin @@ -34,7 +39,7 @@ class LiveServerTestCase(DjangoLiveServerTestCase, LiveServerMixin): cls._options = None # Import/Initialize some values based on chosen testing browser. Default to chrome. - cls._browser = str(getattr(settings, 'SELENIUM_TEST_BROWSER', 'chrome')).lower() + cls._browser = str(ETC_SELENIUM_BROWSER).lower() if cls._browser in ['chrome', 'chromium']: # Setup for Chrome/Chromium browser. @@ -46,14 +51,22 @@ class LiveServerTestCase(DjangoLiveServerTestCase, LiveServerMixin): # Set required options to prevent crashing. chromeOptions = webdriver.ChromeOptions() - chromeOptions.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2}) - chromeOptions.add_argument("--no-sandbox") - chromeOptions.add_argument("--disable-setuid-sandbox") - chromeOptions.add_argument("--remote-debugging-port=9222") - chromeOptions.add_argument("--disable-dev-shm-using") - chromeOptions.add_argument("--disable-extensions") - chromeOptions.add_argument("--disable-gpu") - chromeOptions.add_argument("disable-infobars") + # Disable any existing extensions on local chrome setup, for consistent test runs across machines. + chromeOptions.add_argument('--disable-extensions') + + # Add any user-provided options. + if ETC_SELENIUM_EXTRA_BROWSER_OPTIONS: + for browser_option in ETC_SELENIUM_EXTRA_BROWSER_OPTIONS: + chromeOptions.add_argument(browser_option) + + # TODO: Document these? Seemed to come up a lot in googling errors and whatnot. + # # Avoid possible error in certain development environments about resource limits. + # # Error is along the lines of "DevToolsActivePort file doesn't exist". + # # See https://stackoverflow.com/a/69175552 + # chromeOptions.add_argument('--disable-dev-shm-using') + # # Avoid possible error when many drivers are opened. + # # See https://stackoverflow.com/a/56638103 + # chromeOptions.add_argument("--remote-debugging-port=9222") # Save options. cls._options = chromeOptions @@ -87,8 +100,14 @@ class LiveServerTestCase(DjangoLiveServerTestCase, LiveServerMixin): else: raise ValueError('Unknown browser "{0}".'.format(cls._browser)) - # Create initial testing driver. - cls.create_driver(cls) + # Add universal options based on project settings. + if ETC_SELENIUM_HEADLESS: + cls._options.add_argument('headless') + if ETC_SELENIUM_DISABLE_CACHE: + cls._options.add_argument('disable-application-cache') + + # Create initial testing driver, one for each test. + cls.driver = cls.create_driver(cls) def setUp(self): # Run parent setup logic. @@ -103,9 +122,20 @@ class LiveServerTestCase(DjangoLiveServerTestCase, LiveServerMixin): # Run parent logic. return super().subTest(*args, **kwargs) + @classmethod + def tearDownClass(cls): + # Close all remaining driver instances for class. + while len(cls._driver_set) > 0: + cls.close_driver(cls, cls._driver_set[0]) + + # Call parent teardown logic. + super().tearDownClass() + def tearDown(self): - # Close all remaining browser instances for test. - self.close_all_drivers() + # TODO: Below seems probably unecessary? Research more. + # # Close all remaining window instances for test. + # # (Or at least attempt to for default driver for test). + # self.close_all_windows(self.driver) # Call parent teardown logic. super().tearDown() diff --git a/tests/test_cases/test_base_case.py b/tests/test_cases/test_base_case.py index 2f1bf839357ebbd40f0f36c206875def366472af..3ac08e7e000862d6460e14e1e81bf77b3c5cf74e 100644 --- a/tests/test_cases/test_base_case.py +++ b/tests/test_cases/test_base_case.py @@ -1743,7 +1743,6 @@ class BaseClassTest(BaseTestCase): url = self.generate_get_url('/my/url/here////???//', my_value='test') self.assertText('/my/url/here/?my_value=test', url) - def assert_symbol_standardization(self, symbol_str, expected_return): """ Helper sub-function for testing the standardization methods. diff --git a/tests/test_cases/test_channels_live_server_case.py b/tests/test_cases/test_channels_live_server_case.py index ed9868c7107aaf1fc56d4b763f75b51868e8834d..f336ca712178af7ea0609dd4525eb531759f9f69 100644 --- a/tests/test_cases/test_channels_live_server_case.py +++ b/tests/test_cases/test_channels_live_server_case.py @@ -6,41 +6,40 @@ Tests for test_cases/channels_live_server_test_case.py. import unittest # Internal Imports. -from .universal_live_test_mixin import UniversalLiveTestMixin +from .universal_live_test_mixin import UniversalLiveTestMixin, UniversalLiveTestMixin__DriverTests from django_expanded_test_cases import ChannelsLiveServerTestCase def skip_if_channels_not_installed(): """Skip decorator, to handle when selenium package is not installed.""" try: - import webdriver_manager from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FireFoxService from channels.testing import ChannelsLiveServerTestCase as DjangoChannelsLiveServerTestCase - # If we made it this far, channels is installed. Proceed with test. + # If we made it this far, selenium + channels is installed. Proceed with test. return False except ModuleNotFoundError: # Failed to import channels. Skip test. return True -class LiveServerClassTest(ChannelsLiveServerTestCase, UniversalLiveTestMixin): +class ChannelsLiveServerClassTest(ChannelsLiveServerTestCase, UniversalLiveTestMixin): """Tests for LiveServerTestCase class.""" @classmethod - @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" package.') + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') def setUpClass(cls): # Run parent setup logic. super().setUpClass() - @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" package.') + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') def setUp(self): # Run parent setup logic. super().setUp() - @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" package.') + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') def __int__(self, *args, **kwargs): # Run parent setup logic. super().__init__(*args, **kwargs) @@ -76,3 +75,23 @@ class LiveServerClassTest(ChannelsLiveServerTestCase, UniversalLiveTestMixin): # def test_bbb(self): # self.assertTrue(True) # self.assertFalse(True) + + +class ChannelsLiveServerClassTest__DriverTests(ChannelsLiveServerTestCase, UniversalLiveTestMixin__DriverTests): + """Tests for LiveServerTestCase class.""" + + @classmethod + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') + def setUpClass(cls): + # Run parent setup logic. + super().setUpClass() + + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') + def setUp(self): + # Run parent setup logic. + super().setUp() + + @unittest.skipIf(skip_if_channels_not_installed(), 'Requires "selenium" and "channels" packages.') + def __int__(self, *args, **kwargs): + # Run parent setup logic. + super().__init__(*args, **kwargs) diff --git a/tests/test_cases/test_live_server_case.py b/tests/test_cases/test_live_server_case.py index ceda6087478760448181713800929cae7c8af194..8dc034dca730153cf86f6ca9be192d9eddf51c8c 100644 --- a/tests/test_cases/test_live_server_case.py +++ b/tests/test_cases/test_live_server_case.py @@ -6,19 +6,18 @@ Tests for test_cases/live_server_test_case.py. import unittest # Internal Imports. -from .universal_live_test_mixin import UniversalLiveTestMixin +from .universal_live_test_mixin import UniversalLiveTestMixin, UniversalLiveTestMixin__DriverTests from django_expanded_test_cases import LiveServerTestCase def skip_if_selenium_not_installed(): """Skip decorator, to handle when selenium package is not installed.""" try: - import webdriver_manager from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FireFoxService - # If we made it this far, channels is installed. Proceed with test. + # If we made it this far, selenium is installed. Proceed with test. return False except ModuleNotFoundError: # Failed to import channels. Skip test. @@ -75,3 +74,23 @@ class LiveServerClassTest(LiveServerTestCase, UniversalLiveTestMixin): # def test_bbb(self): # self.assertTrue(True) # self.assertFalse(True) + + +class LiveServerClassTest__DriverTests(LiveServerTestCase, UniversalLiveTestMixin__DriverTests): + """Tests for LiveServerTestCase class.""" + + @classmethod + @unittest.skipIf(skip_if_selenium_not_installed(), 'Requires "selenium" package.') + def setUpClass(cls): + # Run parent setup logic. + super().setUpClass() + + @unittest.skipIf(skip_if_selenium_not_installed(), 'Requires "selenium" package.') + def setUp(self): + # Run parent setup logic. + super().setUp() + + @unittest.skipIf(skip_if_selenium_not_installed(), 'Requires "selenium" package.') + def __int__(self, *args, **kwargs): + # Run parent setup logic. + super().__init__(*args, **kwargs) diff --git a/tests/test_cases/universal_live_test_mixin.py b/tests/test_cases/universal_live_test_mixin.py index 1ee91a3f5e11967b1ec1b3ab7c02de44d15bf75c..8867eea3d364e252d7fc22e6061b9f3d36304f82 100644 --- a/tests/test_cases/universal_live_test_mixin.py +++ b/tests/test_cases/universal_live_test_mixin.py @@ -8,6 +8,193 @@ from pathlib import Path class UniversalLiveTestMixin: """Tests which apply to all LiveServerTestCase classes (both selenium and channels).""" + # region Core Logic Tests + + def test__window_handling(self): + """ + Tests standard create/get/destroy driver functions. + """ + # Get initial driver. + driver = self.create_driver() + + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__window_handling.html').resolve()) + + # Verify handle length before test. + self.assertEqual(1, len(driver.window_handles)) + + with self.subTest('Test open_new_window() function'): + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Window 1</h1>') + file.write('<h2>Window 1</h2>') + file.write('<h3>Window 1</h3>') + file.write('<h4>Window 1</h4>') + file.write('<h5>Window 1</h5>') + file.write('<h6>Window 1</h6>') + file.write('<p>Window 1</p>') + file.write('<span>Window 1</span>') + file.write('<li>Window 1</li>') + + # Get driver object to open generated file. + self.open_new_window(driver) + driver.get('file://{0}'.format(file_name)) + + # Verify updated window count. + self.assertEqual(2, len(driver.window_handles)) + + # Check for expected elements. + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 1\n</li>', results) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Window 2</h1>') + file.write('<h2>Window 2</h2>') + file.write('<h3>Window 2</h3>') + file.write('<h4>Window 2</h4>') + file.write('<h5>Window 2</h5>') + file.write('<h6>Window 2</h6>') + file.write('<p>Window 2</p>') + file.write('<span>Window 2</span>') + file.write('<li>Window 2</li>') + + # Get driver object to open generated file. + self.open_new_window(driver) + driver.get('file://{0}'.format(file_name)) + + # Verify updated window count. + self.assertEqual(len(driver.window_handles), 3) + + # Check for expected elements. + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 2\n</li>', results) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Window 3</h1>') + file.write('<h2>Window 3</h2>') + file.write('<h3>Window 3</h3>') + file.write('<h4>Window 3</h4>') + file.write('<h5>Window 3</h5>') + file.write('<h6>Window 3</h6>') + file.write('<p>Window 3</p>') + file.write('<span>Window 3</span>') + file.write('<li>Window 3</li>') + + # Get driver object to open generated file. + self.open_new_window(driver) + driver.get('file://{0}'.format(file_name)) + + # Verify updated window count. + self.assertEqual(4, len(driver.window_handles)) + + # Check for expected elements. + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 3\n</li>', results) + + # Three separate windows created. + # We have verified we can access expected data immediately after each windows creation. + # Now verify we can switch between them and still get expected data. + # Aka, verify that we didn't accidentally reuse the same window for all above tests. + with self.subTest('Test switch_to_window() function'): + # Verify we actually have four separate windows (3 we created plus the default). + self.assertEqual(4, len(driver.window_handles)) + + # Get individual driver objects. + first_window_index = 1 + second_window_index = 2 + third_window_index = 3 + + # Verify each window handle is unique. + # Window "handle" checks. + self.assertNotEqual(driver.window_handles[first_window_index], driver.window_handles[second_window_index]) + self.assertNotEqual(driver.window_handles[second_window_index], driver.window_handles[third_window_index]) + self.assertNotEqual(driver.window_handles[first_window_index], driver.window_handles[third_window_index]) + + # Verify able to switch between windows and get different data for each. + self.switch_to_window(driver, first_window_index) + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 1\n</li>', results) + + self.switch_to_window(driver, second_window_index) + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 2\n</li>', results) + + self.switch_to_window(driver, third_window_index) + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(1, len(results)) + self.assertIn('<li>\n Window 3\n</li>', results) + + # Verified that our windows are unique and have expected data. + # Now verify we can close individual windows as expected. + with self.subTest('Test close_window() function'): + # Double check we still have three separate drivers. + self.assertEqual(4, len(driver.window_handles)) + + # Close our middle window. + self.close_window(driver, 2) + + # Verify actually closed. + self.assertEqual(3, len(driver.window_handles)) + for index in range(len(driver.window_handles)): + # Check contents of each open window. + self.switch_to_window(driver, index) + + # Verify we do NOT have our closed window data present. + self.assertFalse('Window 2' in driver.page_source) + + # If not the "default" window, verify some content while we're at it. + if index == 1: + results = self.find_elements_by_text(driver, 'Window 1') + self.assertEqual(9, len(results)) + self.assertIn('<li>\n Window 1\n</li>', results) + elif index == 2: + results = self.find_elements_by_text(driver, 'Window 3') + self.assertEqual(9, len(results)) + self.assertIn('<li>\n Window 3\n</li>', results) + + # Close first window. + self.close_window(driver, 1) + + # Verify actually closed. + self.assertEqual(2, len(driver.window_handles)) + for index in range(len(driver.window_handles)): + # Check contents of each open window. + self.switch_to_window(driver, index) + + # Verify we do NOT have our closed window data present. + self.assertFalse('Window 1' in driver.page_source) + self.assertFalse('Window 2' in driver.page_source) + + # If not the "default" window, verify some content while we're at it. + if index == 1: + results = self.find_elements_by_text(driver, 'Window 3') + self.assertEqual(9, len(results)) + self.assertIn('<li>\n Window 3\n</li>', results) + + # Close remaining window. + self.close_window(driver, 1) + + # Verify actually closed. + self.assertEqual(1, len(driver.window_handles)) + for index in range(len(driver.window_handles)): + # Check contents of each open window. + self.switch_to_window(driver, index) + + # Verify we do NOT have our closed window data present. + self.assertFalse('Window 1' in driver.page_source) + self.assertFalse('Window 2' in driver.page_source) + self.assertFalse('Window 3' in driver.page_source) + + # endregion Core Logic Tests + # region Helper Function Tests def test__find_elements_by_tag__success(self): @@ -2875,4 +3062,473 @@ class UniversalLiveTestMixin: self.find_element_by_link_text(driver, 'test_link_text') self.assertText(err_msg, str(err.exception)) + def test__find_elements_by_text__success(self): + """ + Tests find_elements_by_text() function, in cases when it should succeed. + """ + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__find_elements_by_text__success.html').resolve()) + + with self.subTest('When expected text is the only item, with standard element'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">test_element_text</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + results = self.find_elements_by_text(driver, 'test_element_text') + self.assertEqual(len(results), 1) + self.assertIn('<a href="">\n test_element_text\n</a>', results) + + with self.subTest('When expected text exists multiple times - Two instances'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">test_element_text One</a><a href="">test_element_text Two</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + # By base element tag. + results = self.find_elements_by_text(driver, 'test_element_text') + self.assertEqual(len(results), 2) + self.assertIn('<a href="">\n test_element_text One\n</a>', results) + self.assertIn('<a href="">\n test_element_text Two\n</a>', results) + + with self.subTest('When expected element exists multiple times - Three instances plus extra'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write( + """ + <div> + <ul> + <li><a href="">test_element_text One</a></li> + <li><a href="">test_element_text Two</a></li> + <li><a href="">other_element_text Three</a></li> + </ul> + <ul> + <li><a href="">test_element_text Four</a></li> + <li><a href="">another_element_text Five</a></li> + <li><a href="">test Six</a></li> + </ul> + </div> + """ + ) + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + results = self.find_elements_by_text(driver, 'test_element_text') + self.assertEqual(len(results), 3) + self.assertIn('<a href="">\n test_element_text One\n</a>', results) + self.assertIn('<a href="">\n test_element_text Two\n</a>', results) + self.assertIn('<a href="">\n test_element_text Four\n</a>', results) + + def test__find_elements_by_text__failure(self): + """ + Tests find_elements_by_text() function, in cases when it should fail. + """ + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__find_elements_by_text__failure.html').resolve()) + + with self.subTest('When expected text is not present - Blank response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. Provided content was:\n' + '<html><head></head><body></body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_elements_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + with self.subTest('When expected text is not present - Single-item response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. ' + 'Provided content was:\n' + '<html><head></head><body>' + '<a href="">other_element_text</a>' + '</body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">other_element_text</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_elements_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + with self.subTest('When expected text is not present - Multi-item response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. Provided content was:\n' + '<html><head></head><body>' + '<div>\n' + '<h1>Page Header</h1>\n' + '<a href="">other_element_text Some text.</a>\n' + '<a href="">another_element_text Some more text.</a>\n' + '<a href="">test Some text with the str "element_text" in it.</a>\n' + '</div>\n' + '</body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write( + """ + <div> + <h1>Page Header</h1> + <a href="">other_element_text Some text.</a> + <a href="">another_element_text Some more text.</a> + <a href="">test Some text with the str "element_text" in it.</a> + </div> + """ + ) + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_elements_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + def test__find_element_by_text__success(self): + """ + Tests find_element_by_text() function, in cases when it should succeed. + """ + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__find_element_by_text__success.html').resolve()) + + with self.subTest('When expected element is the only item, with standard element'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">test_element_text</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + results = self.find_element_by_text(driver, 'test_element_text') + self.assertText('<a href="">\n test_element_text\n</a>', results) + + with self.subTest('When expected element exists plus extra'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write( + """ + <div> + <ul> + <li><a href="">test_element_text One</a></li> + </ul> + <ul></ul> + </div> + <div> + <ul></ul> + </div> + """ + ) + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + results = self.find_element_by_text(driver, 'test_element_text') + self.assertText('<a href="">\n test_element_text One\n</a>', results) + + def test__find_element_by_text__failure(self): + """ + Tests find_element_by_text() function, in cases when it should fail. + """ + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__find_element_by_text__failure.html').resolve()) + + with self.subTest('When expected text is not present - Blank response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. Provided content was:\n' + '<html><head></head><body></body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_element_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + with self.subTest('When expected text is not present - Single-item response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. ' + 'Provided content was:\n' + '<html><head></head><body>' + '<a href="">other_element_text</a>' + '</body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">other_element_text</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_element_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + with self.subTest('When expected text is not present - Multi-item response'): + err_msg = ( + 'Unable to find element text "test_element_text" in content. Provided content was:\n' + '<html><head></head><body>' + '<div>\n' + '<h1>Page Header</h1>\n' + '<a href="">other_element_text Some text.</a>\n' + '<a href="">another_element_text Some more text.</a>\n' + '<a href="">test Some text with the str "element_text" in it.</a>\n' + '</div>\n' + '</body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write( + """ + <div> + <h1>Page Header</h1> + <a href="">other_element_text Some text.</a> + <a href="">another_element_text Some more text.</a> + <a href="">test Some text with the str "element_text" in it.</a> + </div> + """ + ) + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_element_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + + with self.subTest('When expected text is present multiple times'): + err_msg = ( + 'Found multiple instances of "test_element_text" element text. Expected only one instance. Content was:\n' + '<html><head></head><body>' + '<a href="">test_element_text</a><a href="">test_element_text</a>' + '</body></html>' + ) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<a href="">test_element_text</a><a href="">test_element_text</a>') + + # Get driver object to open generated file. + driver = self.get_driver() + driver.get('file://{0}'.format(file_name)) + + with self.assertRaises(AssertionError) as err: + self.find_element_by_text(driver, 'test_element_text') + self.assertText(err_msg, str(err.exception)) + # endregion Helper Function Tests + + +class UniversalLiveTestMixin__DriverTests: + """Tests which apply to all LiveServerTestCase classes (both selenium and channels). + + Specifically, this mixin contains tests that mess with driver create/grab/destroy. + Easier to just keep it separate from the rest, to ensure consistency in testing. + """ + + def test__driver_handling(self): + """ + Tests standard create/get/destroy driver functions. + """ + # Declare file name for all subtests. + file_name = str(Path('./tests/mock_pages/test__driver_handling.html').resolve()) + + # Start by closing base driver. + self.close_driver(self.driver) + + with self.subTest('Test driver_create() function'): + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Driver 1</h1>') + file.write('<h2>Driver 1</h2>') + file.write('<h3>Driver 1</h3>') + file.write('<h4>Driver 1</h4>') + file.write('<h5>Driver 1</h5>') + file.write('<h6>Driver 1</h6>') + file.write('<p>Driver 1</p>') + file.write('<span>Driver 1</span>') + file.write('<li>Driver 1</li>') + + # Get driver object to open generated file. + driver = self.create_driver() + first_driver = driver + driver.get('file://{0}'.format(file_name)) + + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 1\n</li>', results) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Driver 2</h1>') + file.write('<h2>Driver 2</h2>') + file.write('<h3>Driver 2</h3>') + file.write('<h4>Driver 2</h4>') + file.write('<h5>Driver 2</h5>') + file.write('<h6>Driver 2</h6>') + file.write('<p>Driver 2</p>') + file.write('<span>Driver 2</span>') + file.write('<li>Driver 2</li>') + + # Get driver object to open generated file. + driver = self.create_driver() + second_driver = driver + driver.get('file://{0}'.format(file_name)) + + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 2\n</li>', results) + + # Open file and write expected page contents. + with open(file_name, 'w') as file: + file.write('<h1>Driver 3</h1>') + file.write('<h2>Driver 3</h2>') + file.write('<h3>Driver 3</h3>') + file.write('<h4>Driver 3</h4>') + file.write('<h5>Driver 3</h5>') + file.write('<h6>Driver 3</h6>') + file.write('<p>Driver 3</p>') + file.write('<span>Driver 3</span>') + file.write('<li>Driver 3</li>') + + # Get driver object to open generated file. + driver = self.create_driver() + third_driver = driver + driver.get('file://{0}'.format(file_name)) + + results = self.find_elements_by_tag(driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 3\n</li>', results) + + print('\n\n\n\n') + print('printing driver info...') + for driver in self._driver_set: + print(' {0}'.format(driver)) + print(' Handles: {0}'.format(driver.window_handles)) + + # Three separate drivers created. + # We have verified we can access expected data immediately after each driver creation. + # Now verify we can switch between them and still get expected data. + # Aka, verify that we didn't accidentally reuse the same window for all above tests. + with self.subTest('Test driver_get() function'): + # Verify we actually have three our separate drivers. + self.assertEqual(3, len(self._driver_set)) + self.assertIn(first_driver, self._driver_set) + self.assertIn(second_driver, self._driver_set) + self.assertIn(third_driver, self._driver_set) + + # Verify each driver is different, each driver handle is unique, and each driver session is unique. + # Driver checks. + self.assertNotEqual(first_driver, second_driver) + self.assertNotEqual(second_driver, third_driver) + self.assertNotEqual(first_driver, third_driver) + # Driver "handle" checks. + self.assertNotEqual(first_driver.window_handles, second_driver.window_handles) + self.assertNotEqual(second_driver.window_handles, third_driver.window_handles) + self.assertNotEqual(first_driver.window_handles, third_driver.window_handles) + # Driver session checks. + self.assertNotEqual(first_driver.session_id, second_driver.session_id) + self.assertNotEqual(second_driver.session_id, third_driver.session_id) + self.assertNotEqual(first_driver.session_id, third_driver.session_id) + + # Verify each driver only has one associated window. + self.assertEqual(1, len(first_driver.window_handles)) + self.assertEqual(1, len(second_driver.window_handles)) + self.assertEqual(1, len(third_driver.window_handles)) + + # Verify able to switch between drivers and get different data for each. + self.switch_to_window(first_driver, 0) + results = self.find_elements_by_tag(first_driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 1\n</li>', results) + + self.switch_to_window(second_driver, 0) + results = self.find_elements_by_tag(second_driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 2\n</li>', results) + + self.switch_to_window(third_driver, 0) + results = self.find_elements_by_tag(third_driver, 'li') + self.assertEqual(len(results), 1) + self.assertIn('<li>\n Driver 3\n</li>', results) + + # Verified that our drivers are unique and have expected data. + # Now verify we can close individual drivers as expected. + # Note: For below tests, unsure how useful the session_id comparisons are. + # But it's at least attempting to verify a driver is still present, and then check said driver for a known + # still-valid id. Unsure how better to identify drivers when other tests are generating an arbitrary number + # of drivers in the background. + with self.subTest('Test driver_close() function'): + # Double check we still have three separate drivers. + self.assertEqual(3, len(self._driver_set)) + + # Save some data for verification. + first_session_id = first_driver.session_id + second_session_id = second_driver.session_id + third_session_id = third_driver.session_id + + # Close our middle driver. + self.close_driver(second_driver) + + # Verify actually closed. + self.assertEqual(2, len(self._driver_set)) + self.assertIn(first_driver, self._driver_set) + self.assertNotIn(second_driver, self._driver_set) + self.assertIn(third_driver, self._driver_set) + self.assertEqual(first_session_id, first_driver.session_id) + self.assertEqual(third_session_id, third_driver.session_id) + self.assertNotEqual(second_session_id, first_driver.session_id) + self.assertNotEqual(second_session_id, third_driver.session_id) + + # Close first driver. + self.close_driver(third_driver) + + # Verify actually closed. + self.assertEqual(1, len(self._driver_set)) + self.assertIn(first_driver, self._driver_set) + self.assertNotIn(second_driver, self._driver_set) + self.assertNotIn(third_driver, self._driver_set) + self.assertEqual(first_session_id, first_driver.session_id) + self.assertNotEqual(second_session_id, first_driver.session_id) + self.assertNotEqual(third_session_id, first_driver.session_id) + + # Close remaining driver and check that none remain. + self.close_driver(first_driver) + self.assertEqual(0, len(self._driver_set)) + self.assertNotIn(first_driver, self._driver_set) + self.assertNotIn(second_driver, self._driver_set) + self.assertNotIn(third_driver, self._driver_set)