From 0650978a9171866aeda4be0aedbd0090fd8649ec Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 30 Sep 2017 14:32:51 -0400 Subject: [PATCH] Switch to a new more structured YAML format for navigation data. Break everything into components. --- test/galaxy_selenium/data.py | 157 +++++++++++++++++- test/galaxy_selenium/has_driver.py | 47 ++++++ test/galaxy_selenium/navigates_galaxy.py | 126 +++++++------- test/galaxy_selenium/navigation-data.yml | 1 + test/galaxy_selenium/navigation.yml | 129 ++++++++++++++ test/selenium_tests/test_anon_history.py | 23 ++- .../test_collection_builders.py | 17 +- test/selenium_tests/test_custom_builds.py | 2 - test/selenium_tests/test_history_options.py | 6 +- test/selenium_tests/test_history_panel.py | 44 ++--- .../test_published_histories_grid.py | 27 ++- test/selenium_tests/test_saved_histories.py | 2 - test/selenium_tests/test_uploads.py | 4 +- 13 files changed, 458 insertions(+), 127 deletions(-) create mode 100644 test/galaxy_selenium/navigation.yml diff --git a/test/galaxy_selenium/data.py b/test/galaxy_selenium/data.py index 1ed3baf0a5a..5c25bb09b65 100644 --- a/test/galaxy_selenium/data.py +++ b/test/galaxy_selenium/data.py @@ -1,5 +1,158 @@ +import re +import string + import yaml + from pkg_resources import resource_string -data_yaml = resource_string(__name__, 'navigation-data.yml').decode("UTF-8") -NAVIGATION_DATA = yaml.safe_load(data_yaml) +from selenium.webdriver.common.by import By + +from galaxy.util.bunch import Bunch + + +old_data_yaml = resource_string(__name__, 'navigation-data.yml').decode("UTF-8") +NAVIGATION_DATA = yaml.safe_load(old_data_yaml) +new_data_yaml = resource_string(__name__, 'navigation.yml').decode("UTF-8") +NAVIGATION_RAW = yaml.safe_load(new_data_yaml) + + +class SelectorTemplate(object): + + def __init__(self, selector, selector_type, kwds=None, with_classes=None): + self._selector = selector + self.selector_type = selector_type + self.__kwds = kwds or {} + self.with_classes = with_classes or [] + + @staticmethod + def from_dict(raw_value): + if isinstance(raw_value, dict): + return SelectorTemplate(raw_value["selector"], raw_value.get("type", "css")) + else: + return SelectorTemplate(raw_value, "css") + + def with_class(self, class_): + assert self.selector_type == "css" + return SelectorTemplate(self._selector, self.selector_type, kwds=self.__kwds, with_classes=self.with_classes + [class_]) + + def __call__(self, **kwds): + new_kwds = self.__kwds + new_kwds.update(**kwds) + return SelectorTemplate(self._selector, self.selector_type, kwds=new_kwds, with_classes=self.with_classes) + + @property + def description(self): + if self.selector_type == "css": + template = "CSS selector [%s]" + elif self.selector_type == "xpath": + template = "XPATH selector [%s]" + elif self.selector_type == "id": + template = "DOM element with id [%s]" + return template % self.selector + + @property + def selector(self): + selector = self._selector + if self.__kwds is not None: + selector = string.Template(selector).substitute(self.__kwds) + selector = selector + "".join([".%s" % c for c in self.with_classes]) + return selector + + @property + def element_locator(self): + if self.selector_type == "css": + by = By.CSS_SELECTOR + elif self.selector_type == "xpath": + by = By.XPATH + elif self.selector_type == "id": + by = By.ID + else: + raise Exception("Unknown selector type") + return (by, self.selector) + + @property + def as_css_class(self): + assert self.selector_type == "css" + assert re.compile(r"\.\w+").match(self._selector) + return self._selector[1:] + + +class Label(object): + + def __init__(self, text): + self.text = text + + @property + def description(self): + return "Link text [%s]" % self.text + + @property + def element_locator(self): + return (By.LINK_TEXT, self.text) + + +class Text(object): + + def __init__(self, text): + self.text = text + + @property + def description(self): + return "Text containing [%s]" % self.text + + @property + def element_locator(self): + return (By.PARTIAL_LINK_TEXT, self.text) + + +class Component(object): + + def __init__(self, name, sub_components, selectors, labels, text): + self.name = name + self._sub_components = sub_components + self._selectors = selectors + self._labels = labels + self._text = text + + self.selectors = Bunch(**self._selectors) + self.labels = Bunch(**self._labels) + self.text = Bunch(**self._text) + + @property + def selector(self): + if "_" in self._selectors: + return self._selectors["_"] + else: + raise Exception("No _ selector for [%s]" % self) + + @staticmethod + def from_dict(name, raw_value): + selectors = {} + labels = {} + text = {} + sub_components = {} + + for key, value in raw_value.items(): + if key == "selectors": + for selector_key, selector_value in value.items(): + selectors[selector_key] = SelectorTemplate.from_dict(selector_value) + elif key == "labels": + for label_key, label_value in value.items(): + labels[label_key] = Label(label_value) + elif key == "text": + for text_key, text_value in value.items(): + text[text_key] = Text(text_value) + else: + component = Component.from_dict(key, value) + sub_components[key] = component + + return Component(name, sub_components, selectors, labels, text) + + def __getattr__(self, attr): + return self._sub_components[attr] + + def __str__(self): + return "Component[%s]" % self.name + + +NAVIGATION = Component.from_dict("root", NAVIGATION_RAW) diff --git a/test/galaxy_selenium/has_driver.py b/test/galaxy_selenium/has_driver.py index 9e7346fe486..2ad75fa062e 100644 --- a/test/galaxy_selenium/has_driver.py +++ b/test/galaxy_selenium/has_driver.py @@ -28,13 +28,28 @@ class HasDriver: for element in elements: assert not element.is_displayed() + def assert_absent_or_hidden(self, selector_template): + elements = self.find_elements(selector_template) + for element in elements: + assert not element.is_displayed() + def selector_is_displayed(self, selector): element = self.driver.find_element_by_css_selector(selector) return element.is_displayed() + def is_displayed(self, selector_template): + element = self.driver.find_element(*selector_template.element_locator) + return element.is_displayed() + def assert_selector_absent(self, selector): assert len(self.driver.find_elements_by_css_selector(selector)) == 0 + def find_elements(self, selector_template): + return self.driver.find_elements(*selector_template.element_locator) + + def assert_absent(self, selector_template): + assert len(self.find_elements(selector_template)) == 0 + def wait_for_xpath(self, xpath, **kwds): element = self._wait_on( ec.presence_of_element_located((By.XPATH, xpath)), @@ -59,6 +74,22 @@ class HasDriver: ) return element + def wait_for_present(self, selector_template, **kwds): + element = self._wait_on( + ec.presence_of_element_located(selector_template.element_locator), + "%s to become present" % selector_template.description, + **kwds + ) + return element + + def wait_for_visible(self, selector_template, **kwds): + element = self._wait_on( + ec.visibility_of_element_located(selector_template.element_locator), + "%s to become visible" % selector_template.description, + **kwds + ) + return element + def wait_for_selector_visible(self, selector, **kwds): element = self._wait_on( ec.visibility_of_element_located((By.CSS_SELECTOR, selector)), @@ -75,6 +106,14 @@ class HasDriver: ) return element + def wait_for_clickable(self, selector_template, **kwds): + element = self._wait_on( + ec.element_to_be_clickable(selector_template.element_locator), + "%s to become clickable" % selector_template.description, + **kwds + ) + return element + def wait_for_selector_absent_or_hidden(self, selector, **kwds): element = self._wait_on( ec.invisibility_of_element_located((By.CSS_SELECTOR, selector)), @@ -91,6 +130,14 @@ class HasDriver: ) return element + def wait_for_absent_or_hidden(self, selector_template, **kwds): + element = self._wait_on( + ec.invisibility_of_element_located(selector_template.element_locator), + "%s to become absent or hidden" % selector_template.description, + **kwds + ) + return element + def wait_for_id(self, id, **kwds): return self._wait_on( ec.presence_of_element_located((By.ID, id)), diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index fc6e9cb37bd..efa72bcb470 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -17,7 +17,10 @@ import yaml from galaxy.util.bunch import Bunch from . import sizzle -from .data import NAVIGATION_DATA +from .data import ( + NAVIGATION, # newer variant of NAVIGATION_DATA + NAVIGATION_DATA, +) from .has_driver import ( exception_indicates_not_clickable, exception_indicates_stale_element, @@ -130,6 +133,10 @@ class NavigatesGalaxy(HasDriver): def navigation_data(self): return NAVIGATION_DATA + @property + def navigation(self): + return NAVIGATION + def wait_length(self, wait_type): return wait_type.default_length * self.timeout_multiplier @@ -142,11 +149,11 @@ class NavigatesGalaxy(HasDriver): def home(self): self.get() - self.wait_for_selector_visible("#masthead") - self.wait_for_selector_visible("#current-history-panel") + self.wait_for_visible(self.navigation.masthead.selector) + self.wait_for_visible(self.navigation.history_panel.selector) def switch_to_main_panel(self): - self.driver.switch_to.frame(self.navigation_data["selectors"]["frames"]["main"]) + self.driver.switch_to.frame("galaxy_main") @contextlib.contextmanager def main_panel(self): @@ -182,12 +189,8 @@ class NavigatesGalaxy(HasDriver): 'galaxysession': self.get_galaxy_session() } - def history_panel_name_selector(self): - name_selector = self.test_data["historyPanel"]["selectors"]["history"]["name"] - return name_selector - def history_panel_name_element(self): - return self.wait_for_selector(self.history_panel_name_selector()) + return self.wait_for_present(self.navigation.history_panel.selectors.name) @retry_during_transitions def history_panel_name(self): @@ -248,23 +251,25 @@ class NavigatesGalaxy(HasDriver): self.wait(timeout).until(history_has_hid) contents = self.api_get("histories/%s/contents" % current_history_id) history_item = [d for d in contents if d["hid"] == hid][0] - history_item_selector = "#%s-%s" % (history_item["history_content_type"], history_item["id"]) - + history_item_selector = self.navigation.history_panel.item.selector( + history_content_type=history_item["history_content_type"], + id=history_item["id"] + ) try: - self.history_item_wait_for_selector(history_item_selector, allowed_force_refreshes) + self.history_item_wait_for(history_item_selector, allowed_force_refreshes) except self.TimeoutException as e: - dataset_elements = self.driver.find_elements_by_css_selector("#current-history-panel .list-items div") - div_ids = [d.get_attribute('id') for d in dataset_elements] + contents_elements = self.find_elements(self.navigation.history_panel.selectors.contents) + div_ids = [("#" + d.get_attribute('id')) for d in contents_elements] template = "Failed waiting on history item %d to become visible, visible datasets include [%s]." message = template % (hid, ",".join(div_ids)) raise self.prepend_timeout_message(e, message) return history_item_selector - def history_item_wait_for_selector(self, history_item_selector, allowed_force_refreshes): + def history_item_wait_for(self, history_item_selector, allowed_force_refreshes): attempt = 0 while True: try: - rval = self.wait_for_selector_visible(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) + rval = self.wait_for_visible(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) break except self.TimeoutException: if attempt >= allowed_force_refreshes: @@ -278,23 +283,26 @@ class NavigatesGalaxy(HasDriver): # Use the search box showing up as a proxy that the history display # has left the "loading" state and is showing a valid set of history contents # (even if empty). - self.wait_for_selector_visible("#current-history-panel input.search-query", wait_type=WAIT_TYPES.DATABASE_OPERATION) + self.wait_for_visible(self.navigation.history_panel.selectors.search, wait_type=WAIT_TYPES.DATABASE_OPERATION) def history_panel_wait_for_hid_hidden(self, hid): current_history_id = self.current_history_id() contents = self.api_get("histories/%s/contents" % current_history_id) history_item = [d for d in contents if d["hid"] == hid][0] - history_item_selector = "#%s-%s" % (history_item["history_content_type"], history_item["id"]) - self.wait_for_selector_absent(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) + history_item_selector = self.navigation.history_panel.item.selector( + history_content_type=history_item["history_content_type"], + id=history_item["id"] + ) + self.wait_for_absent_or_hidden(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) return history_item_selector def history_panel_wait_for_hid_state(self, hid, state, allowed_force_refreshes=0): history_item_selector = self.history_panel_wait_for_hid_visible(hid, allowed_force_refreshes=allowed_force_refreshes) - history_item_selector_state = "%s.state-%s" % (history_item_selector, state) + history_item_selector_state = history_item_selector.with_class("state-%s" % state) try: - self.history_item_wait_for_selector(history_item_selector_state, allowed_force_refreshes) + self.history_item_wait_for(history_item_selector_state, allowed_force_refreshes) except self.TimeoutException as e: - history_item = self.driver.find_element_by_css_selector(history_item_selector) + history_item = self.wait_for_visible(history_item_selector) current_state = "UNKNOWN" classes = history_item.get_attribute("class").split(" ") for clazz in classes: @@ -348,10 +356,10 @@ class NavigatesGalaxy(HasDriver): } self.click_masthead_user() - self.click_label(self.navigation_data["labels"]["masthead"]["userMenu"]["login"]) + self.wait_for_and_click(self.navigation.masthead.labels.login) with self.main_panel(): - form = self.wait_for_selector(self.navigation_data["selectors"]["loginPage"]["form"]) + form = self.wait_for_visible(self.navigation.login.selectors.form) self.fill(form, login_info) self.snapshot("logging-in") self.click_submit(form) @@ -380,17 +388,16 @@ class NavigatesGalaxy(HasDriver): self.home() self.click_masthead_user() - self.click_label(self.navigation_data["labels"]["masthead"]["userMenu"]["register"]) + self.wait_for_and_click(self.navigation.masthead.labels.register) with self.main_panel(): - register_form_id = self.navigation_data["selectors"]["registrationPage"]["form"] - form = self.wait_for_id(register_form_id) + form = self.wait_for_visible(self.navigation.registration.selectors.form) self.fill(form, dict( email=email, password=password, username=username, confirm=confirm )) - self.click_xpath(self.navigation_data["selectors"]["registrationPage"]["submit_xpath"]) + self.wait_for_and_click(self.navigation.registration.selectors.submit) # Give the browser a bit of time to submit the request. # It would be good to eliminate this sleep, but it can't be because Galaxy # doesn't swap the "User" menu automatically after it registers a user and @@ -409,7 +416,7 @@ class NavigatesGalaxy(HasDriver): # Make sure the user menu was dropped down user_menu = self.wait_for_selector_visible("ul.nav#user .dropdown-menu") try: - user_email_element = self.wait_for_xpath_visible(self.navigation_data["selectors"]["masthead"]["userMenu"]["userEmail_xpath"]) + user_email_element = self.wait_for_visible(self.navigation.masthead.selectors.user_email) except self.TimeoutException as e: menu_items = user_menu.find_elements_by_css_selector("li a") menu_text = [mi.text for mi in menu_items] @@ -425,7 +432,7 @@ class NavigatesGalaxy(HasDriver): def wait_for_logged_in(self): try: - self.wait_for_selector_visible("a.loggedin-only") + self.wait_for_visible(self.navigation.masthead.selectors.logged_in_only) except self.TimeoutException as e: user_info = self.api_get("users/current") if "username" in user_info: @@ -709,13 +716,16 @@ class NavigatesGalaxy(HasDriver): self.wait_for_and_click_selector("button#execute") def click_masthead_user(self): - self.click_xpath(self.navigation_data["selectors"]["masthead"]["user"]) + self.wait_for_and_click(self.navigation.masthead.selectors.user) + + def click_masthead_libraries(self): + self.wait_for_and_click(self.navigation.masthead.labels.libraries) def click_masthead_workflow(self): - self.click_xpath(self.navigation_data["selectors"]["masthead"]["workflow"]) + self.wait_for_and_click(self.navigation.masthead.labels.workflow) def click_button_new_workflow(self): - self.wait_for_and_click_selector(self.navigation_data["selectors"]["workflows"]["new_button"]) + self.wait_for_and_click(self.navigation.workflows.selectors.new_button) def wait_for_sizzle_selector_clickable(self, selector): element = self._wait_on( @@ -741,16 +751,11 @@ class NavigatesGalaxy(HasDriver): self.click_history_options() # Click labelled option - menu_selector = self.history_options_menu_selector() - self.wait_for_selector_visible(menu_selector) + self.wait_for_visible(self.navigation.history_panel.selectors.options_menu) menu_item_sizzle_selector = '#history-options-button-menu > li > a:contains("%s")' % option_label menu_selection_element = self.wait_for_sizzle_selector_clickable(menu_item_sizzle_selector) menu_selection_element.click() - def history_options_menu_selector(self): - menu_selector = self.test_data["historyOptions"]["selectors"]["menu"] - return menu_selector - @retry_during_transitions def histories_click_advanced_search(self): search_selector = '#standard-search .advanced-search-toggle' @@ -777,36 +782,31 @@ class NavigatesGalaxy(HasDriver): self.send_enter(editable_text_input_element) def history_panel_click_to_rename(self): - self.wait_for_and_click_selector(self.history_panel_name_selector()) - return self.wait_for_selector(self.history_panel_edit_title_input_selector()) - - def history_panel_edit_title_input_selector(self): - return self.test_data["historyPanel"]["selectors"]["history"]["nameEditableTextInput"] + self.wait_for_and_click(self.navigation.history_panel.selectors.name) + return self.wait_for_visible(self.navigation.history_panel.selectors.name_edit_input) def history_panel_refresh_click(self): - self.wait_for_and_click_selector("#history-refresh-button") - - def history_panel_multi_operations_selector(self): - return self.test_data["historyPanel"]["selectors"]["history"]["multiOperationsIcon"] + self.wait_for_and_click(self.navigation.history_panel.selectors.refresh_button) def history_panel_multi_operations_show(self): - operations_selector = self.history_panel_multi_operations_selector() - self.wait_for_and_click_selector(operations_selector) + return self.wait_for_and_click(self.navigation.history_panel.multi_operations.selectors.show_button) def history_panel_muli_operation_select_hid(self, hid): item_selector = self.history_panel_item_selector(hid, wait=True) operation_radio_selector = "%s .selector" % item_selector self.wait_for_and_click_selector(operation_radio_selector) - def history_panel_multi_operation_action_selector(self): - return self.test_data["historyPanel"]["selectors"]["history"]["multiOperationsActionBtn"] - def history_panel_multi_operation_action_click(self, action): - time.sleep(5) - self.wait_for_and_click_selector(self.history_panel_multi_operation_action_selector()) - menu_element = self.wait_for_selector_visible(".list-action-menu.open") - action_element = menu_element.find_element_by_link_text(action) - action_element.click() + # Maybe isn't needed? + # self.sleep_for(WAIT_TYPES.UX_RENDER) + self.wait_for_and_click(self.navigation.history_panel.multi_operations.selectors.action_button) + + @retry_during_transitions + def _click_action_in_menu(): + menu_element = self.wait_for_visible(self.navigation.history_panel.multi_operations.selectors.action_menu) + menu_element.find_element_by_link_text(action.text).click() + + _click_action_in_menu() def history_panel_item_click_visualization_menu(self, hid): viz_button_selector = "%s %s" % (self.history_panel_item_selector(hid), ".visualizations-dropdown") @@ -917,7 +917,7 @@ class NavigatesGalaxy(HasDriver): if self.is_logged_in(): self.home() self.click_masthead_user() - self.click_label(self.navigation_data["labels"]["masthead"]["userMenu"]["logout"]) + self.wait_for_and_click(self.navigation.masthead.labels.logout) self.click_label('go to the home page') assert not self.is_logged_in() @@ -986,6 +986,8 @@ class NavigatesGalaxy(HasDriver): return self.assert_selector_absent_or_hidden(selector) def assert_tooltip_text(self, element, expected, sleep=0, click_away=True): + if hasattr(expected, "text"): + expected = expected.text 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) @@ -1038,6 +1040,12 @@ class NavigatesGalaxy(HasDriver): element.click() return element + @retry_during_transitions + def wait_for_and_click(self, selector_template): + element = self.wait_for_clickable(selector_template) + element.click() + return element + def select2_set_value(self, container_selector, value, with_click=True): # There are two hacky was to select things from the select2 widget - # with_click=True: This simulates the mouse click after the suggestion contains diff --git a/test/galaxy_selenium/navigation-data.yml b/test/galaxy_selenium/navigation-data.yml index 640c53c0b39..6e975af6a26 100644 --- a/test/galaxy_selenium/navigation-data.yml +++ b/test/galaxy_selenium/navigation-data.yml @@ -1,3 +1,4 @@ +# Deprecated navigation constants - prefer navigation.yml. selectors: tooltipBalloon: '.tooltip' editableText: '.editable-text' diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml new file mode 100644 index 00000000000..045ff6e7c1c --- /dev/null +++ b/test/galaxy_selenium/navigation.yml @@ -0,0 +1,129 @@ +# top_level: {global: , workflows: , histories: } +# components := {: } | +# component := {selectors: , labels: , ids: } +# selectors_map := {: } +# selector := | {type: , selector: } +# selector_type := css|xpath|sizzle|id +# labels_map := {: