diff --git a/test/galaxy_selenium/has_driver.py b/test/galaxy_selenium/has_driver.py index 021de2eac0a..4dca4fbe5d3 100644 --- a/test/galaxy_selenium/has_driver.py +++ b/test/galaxy_selenium/has_driver.py @@ -140,5 +140,17 @@ class HasDriver: ) +def execption_indicates_not_clickable(exception): + return "not clickable" in str(exception) + + def exception_indicates_stale_element(exception): return "stale" in str(exception) + + +__all__ = ( + "execption_indicates_not_clickable", + "exception_indicates_stale_element", + "HasDriver", + "TimeoutException", +) diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index 7bdcc96d987..cff486af9a7 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -15,12 +15,20 @@ import requests import yaml from .data import NAVIGATION_DATA -from .has_driver import exception_indicates_stale_element, HasDriver, TimeoutException +from .has_driver import ( + execption_indicates_not_clickable, + exception_indicates_stale_element, + HasDriver, + TimeoutException, +) from . import sizzle # Test case data DEFAULT_PASSWORD = '123456' +RETRY_DURING_TRANSITIONS_SLEEP_DEFAULT = .1 +RETRY_DURING_TRANSITIONS_ATTEMPTS_DEFAULT = 10 + class NullTourCallback(object): @@ -28,7 +36,24 @@ class NullTourCallback(object): pass -def retry_call_during_transitions(f, attempts=5, sleep=.1, exception_check=exception_indicates_stale_element): +def excepion_seems_to_indicate_transition(e): + """True if exception seems to indicate the page state is transitioning. + + Galaxy features many different transition effects that change the page state over time. + These transitions make it slightly more difficult to test Galaxy because atomic input + actions take an indeterminate amount of time to be reflected on the screen. This method + takes a Selenium assertion and tries to infer if such a transition could be the root + cause of the exception. The methods that follow use it to allow retrying actions during + transitions. + + Currently the two kinds of exceptions that we say may indicate a transition are + StaleElement exceptions (a DOM element grabbed at one step is no longer available) + and "not clickable" exceptions (so perhaps a popup modal is blocking a click). + """ + return exception_indicates_stale_element(e) or execption_indicates_not_clickable(e) + + +def retry_call_during_transitions(f, attempts=RETRY_DURING_TRANSITIONS_ATTEMPTS_DEFAULT, sleep=RETRY_DURING_TRANSITIONS_SLEEP_DEFAULT, exception_check=excepion_seems_to_indicate_transition): previous_attempts = 0 while True: try: @@ -44,7 +69,7 @@ def retry_call_during_transitions(f, attempts=5, sleep=.1, exception_check=excep previous_attempts += 1 -def retry_during_transitions(f, attempts=5, sleep=.1, exception_check=exception_indicates_stale_element): +def retry_during_transitions(f, attempts=RETRY_DURING_TRANSITIONS_ATTEMPTS_DEFAULT, sleep=RETRY_DURING_TRANSITIONS_SLEEP_DEFAULT, exception_check=excepion_seems_to_indicate_transition): @wraps(f) def _retry(*args, **kwds): @@ -201,12 +226,27 @@ class NavigatesGalaxy(HasDriver): raise self.prepend_timeout_message(e, message) return history_item_selector_state + def published_grid_search_for(self, search_term=None): + return self._inline_search_for( + '#input-free-text-search-filter', + search_term, + ) + def get_logged_in_user(self): return self.api_get("users/current") def is_logged_in(self): return "email" in self.get_logged_in_user() + @retry_during_transitions + def _inline_search_for(self, selector, search_term=None): + search_box = self.wait_for_and_click_selector(selector) + search_box.clear() + if search_term is not None: + search_box.send_keys(search_term) + self.send_enter(search_box) + return search_box + def _get_random_name(self, prefix=None, suffix=None, len=10): return '%s%s%s' % ( prefix or '', @@ -480,6 +520,12 @@ class NavigatesGalaxy(HasDriver): def workflow_index_click_search(self): return self.wait_for_and_click_selector("input.search-wf") + def workflow_index_search_for(self, search_term=None): + return self._inline_search_for( + "input.search-wf", + search_term, + ) + def workflow_index_click_import(self): self.wait_for_and_click_selector(self.test_data["selectors"]["workflows"]["import_button"]) @@ -516,6 +562,7 @@ class NavigatesGalaxy(HasDriver): tag_display = workflow_row_element.find_element_by_css_selector(".tags-display") tag_display.click() + @retry_during_transitions def workflow_index_tags(self, workflow_index=0): workflow_row_element = self.workflow_index_table_row(workflow_index) tag_display = workflow_row_element.find_element_by_css_selector(".tags-display") @@ -796,6 +843,18 @@ class NavigatesGalaxy(HasDriver): self.click_center() return text + @retry_during_transitions + def assert_selector_absent_or_hidden_after_transitions(self, selector): + """Variant of assert_selector_absent_or_hidden that retries during transitions. + + In the parent method - the element is found and then it is checked to see + if it is visible. It may disappear from the page in the middle there + and cause a StaleElement error. For checks where we care about the final + resting state after transitions - this method can be used to retry + during those transitions. + """ + return self.assert_selector_absent_or_hidden(selector) + def assert_tooltip_text(self, element, expected, sleep=0, click_away=True): text = self.get_tooltip_text(element, sleep=sleep, click_away=click_away) assert text == expected, "Tooltip text [%s] was not expected text [%s]." % (text, expected) diff --git a/test/selenium_tests/test_history_options.py b/test/selenium_tests/test_history_options.py index 8e8f975801c..f701f65d357 100644 --- a/test/selenium_tests/test_history_options.py +++ b/test/selenium_tests/test_history_options.py @@ -27,4 +27,4 @@ class HistoryOptionsTestCase(SeleniumTestCase): self.click_hda_title(hda_id, wait=True) - self.assert_selector_absent_or_hidden(hda_body_selector) + self.assert_selector_absent_or_hidden_after_transitions(hda_body_selector) diff --git a/test/selenium_tests/test_published_histories_grid.py b/test/selenium_tests/test_published_histories_grid.py index 9937a19e602..7cf86e06884 100644 --- a/test/selenium_tests/test_published_histories_grid.py +++ b/test/selenium_tests/test_published_histories_grid.py @@ -9,10 +9,6 @@ from .framework import ( class HistoryGridTestCase(SharedStateSeleniumTestCase): - def setUp(self): - super(HistoryGridTestCase, self).setUp() - self.home() - @selenium_test def test_history_grid_histories(self): self.navigate_to_published_histories_page() @@ -22,17 +18,11 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase): def test_history_grid_search_standard(self): self.navigate_to_published_histories_page() - input_selector = '#input-free-text-search-filter' - search_input = self.wait_for_selector(input_selector) - search_input.send_keys(self.history1_name) - self.send_enter(search_input) - + self.published_grid_search_for(self.history1_name) self.assert_grid_histories_are([self.history1_name]) self.unset_filter('free-text-search', self.history1_name) - search_input = self.wait_for_selector(input_selector) - search_input.send_keys(self.history4_name) - self.send_enter(search_input) + self.published_grid_search_for(self.history4_name) self.assert_grid_histories_are(['No Items']) diff --git a/test/selenium_tests/test_saved_histories.py b/test/selenium_tests/test_saved_histories.py index 422b7c8d713..8bd65ec0348 100644 --- a/test/selenium_tests/test_saved_histories.py +++ b/test/selenium_tests/test_saved_histories.py @@ -9,18 +9,15 @@ from .framework import ( class SavedHistoriesTestCase(SharedStateSeleniumTestCase): - def setUp(self): - super(SavedHistoriesTestCase, self).setUp() - self.home() - self.submit_login(self.user_email, retries=3) - @selenium_test def test_saved_histories_list(self): + self._login() self.navigate_to_saved_histories_page() self.assert_histories_in_grid([self.history2_name, self.history3_name]) @selenium_test def test_history_switch(self): + self._login() self.navigate_to_saved_histories_page() self.click_popup_option(self.history2_name, 'Switch') time.sleep(1) @@ -29,6 +26,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_history_view(self): + self._login() self.navigate_to_saved_histories_page() self.click_popup_option(self.history2_name, 'View') history_name = self.wait_for_selector('.name.editable-text') @@ -36,6 +34,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_history_publish(self): + self._login() self.navigate_to_saved_histories_page() # Publish the history @@ -52,6 +51,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_rename_history(self): + self._login() self.navigate_to_saved_histories_page() self.click_popup_option('Unnamed history', 'Rename') @@ -72,6 +72,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_delete_and_undelete_history(self): + self._login() self.navigate_to_saved_histories_page() # Delete the history @@ -93,6 +94,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_permanently_delete_history(self): + self._login() self.create_history(self.history4_name) self.navigate_to_saved_histories_page() @@ -111,6 +113,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_delete_and_undelete_multiple_histories(self): + self._login() self.navigate_to_saved_histories_page() delete_button_selector = 'input[type="button"][value="Delete"]' @@ -139,6 +142,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_sort_by_name(self): + self._login() self.navigate_to_saved_histories_page() self.wait_for_and_click_selector('.sort-link[sort_key="name"]') @@ -156,6 +160,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_standard_search(self): + self._login() self.navigate_to_saved_histories_page() input_selector = '#input-free-text-search-filter' @@ -174,6 +179,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_advanced_search(self): + self._login() self.navigate_to_saved_histories_page() self.show_advanced_search() @@ -201,6 +207,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): @selenium_test def test_tags(self): + self._login() self.navigate_to_saved_histories_page() # Click the add tag button @@ -221,6 +228,10 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.assert_grid_histories_are([self.history2_name], False) + def _login(self): + self.home() + self.submit_login(self.user_email, retries=3) + @retry_assertion_during_transitions def assert_grid_histories_are(self, expected_histories, sort_matters=True): actual_histories = self.get_histories() diff --git a/test/selenium_tests/test_workflow_editor.py b/test/selenium_tests/test_workflow_editor.py index 6dd7044b500..458cb3c032a 100644 --- a/test/selenium_tests/test_workflow_editor.py +++ b/test/selenium_tests/test_workflow_editor.py @@ -1,6 +1,7 @@ import time from .framework import ( + retry_assertion_during_transitions, selenium_test, SeleniumTestCase ) @@ -52,10 +53,7 @@ class WorkflowEditorTestCase(SeleniumTestCase): workflow_populator.upload_yaml_workflow(WORKFLOW_WITH_OLD_TOOL_VERSION, exact_tools=True) self.workflow_index_open() self.workflow_index_click_option("Edit") - time.sleep(.5) - modal_element = self.wait_for_selector_visible(self.modal_body_selector()) - text = modal_element.text - assert "Using version '0.2' instead of version '0.0.1'" in text, text + self.assert_modal_has_text("Using version '0.2' instead of version '0.0.1'") @selenium_test def test_editor_invalid_tool_state(self): @@ -63,11 +61,8 @@ class WorkflowEditorTestCase(SeleniumTestCase): workflow_populator.upload_yaml_workflow(WORKFLOW_WITH_INVALID_STATE, exact_tools=True) self.workflow_index_open() self.workflow_index_click_option("Edit") - time.sleep(.5) - modal_element = self.wait_for_selector_visible(self.modal_body_selector()) - text = modal_element.text - assert "Using version '0.2' instead of version '0.0.1'" in text, text - assert "Using default: '1'" in text, text + self.assert_modal_has_text("Using version '0.2' instead of version '0.0.1'") + self.assert_modal_has_text("Using default: '1'") @selenium_test def test_missing_tools(self): @@ -84,10 +79,7 @@ steps: """) self.workflow_index_open() self.workflow_index_click_option("Edit") - time.sleep(.5) - modal_element = self.wait_for_selector_visible(self.modal_body_selector()) - text = modal_element.text - assert "Tool is not installed" in text, text + self.assert_modal_has_text("Tool is not installed") def workflow_create_new(self, name=None, annotation=None): self.workflow_index_open() @@ -104,3 +96,9 @@ steps: 'workflow_annotation': annotation, }) self.click_submit(form_element) + + @retry_assertion_during_transitions + def assert_modal_has_text(self, expected_text): + modal_element = self.wait_for_selector_visible(self.modal_body_selector()) + text = modal_element.text + assert expected_text in text, "Failed to find expected text [%s] in modal text [%s]" % (expected_text, text) diff --git a/test/selenium_tests/test_workflow_management.py b/test/selenium_tests/test_workflow_management.py index 776c4ed9ee7..760d1c85108 100644 --- a/test/selenium_tests/test_workflow_management.py +++ b/test/selenium_tests/test_workflow_management.py @@ -73,20 +73,13 @@ class WorkflowManagementTestCase(SeleniumTestCase): self.workflow_index_rename("searchforthis") self._assert_showing_n_workflows(1) - search_box = self.workflow_index_click_search() - search_box.send_keys("doesnotmatch") + self.workflow_index_search_for("doesnotmatch") self._assert_showing_n_workflows(0) - # Prevent stale element textbox by re-fetching, seems to be - # needed but I don't understand why exactly. -John - search_box = self.workflow_index_click_search() - search_box.clear() - self.send_enter(search_box) + self.workflow_index_search_for() self._assert_showing_n_workflows(1) - search_box = self.workflow_index_click_search() - search_box.send_keys("searchforthis") - self.send_enter(search_box) + self.workflow_index_search_for("searchforthis") self._assert_showing_n_workflows(1) @selenium_test