diff --git a/templates/sharing_base.mako b/templates/sharing_base.mako index 1cb7dc766a3..37ca901bb28 100644 --- a/templates/sharing_base.mako +++ b/templates/sharing_base.mako @@ -219,7 +219,7 @@

- Share with another user @@ -228,7 +228,7 @@

You have not shared this ${item_class_name_lc} with any users.

- Share with a user diff --git a/test/galaxy_selenium/components.py b/test/galaxy_selenium/components.py new file mode 100644 index 00000000000..a71ade2fb0f --- /dev/null +++ b/test/galaxy_selenium/components.py @@ -0,0 +1,193 @@ +import re +import string + +from abc import ( + ABCMeta, + abstractproperty, +) + +import six + +from selenium.webdriver.common.by import By + +from galaxy.util.bunch import Bunch + + +@six.add_metaclass(ABCMeta) +class Target(object): + + @abstractproperty + def description(self): + """Return a plain-text description of the browser target for logging/messages.""" + + @abstractproperty + def element_locator(self): + """Return a (by, selector) Selenium elment locator tuple for this selector.""" + + +class SelectorTemplate(Target): + + def __init__(self, selector, selector_type, children={}, kwds=None, with_classes=None): + self._selector = selector + self.selector_type = selector_type + self._children = children + self.__kwds = kwds or {} + self.with_classes = with_classes or [] + + @staticmethod + def from_dict(raw_value, children={}): + if isinstance(raw_value, dict): + return SelectorTemplate(raw_value["selector"], raw_value.get("type", "css"), children=children) + else: + return SelectorTemplate(raw_value, "css", children=children) + + 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_], children=self._children) + + 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, children=self._children) + + @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:] + + def __getattr__(self, name): + if name in self._children: + return self._children[name](**{"_": self.selector}) + else: + raise KeyError("Could not find child [%s] in %s" % (name, self._children)) + + __getitem__ = __getattr__ + + +class Label(Target): + + 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(Target): + + 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": + base_selector = None + if "_" in value: + base_selector = value["_"] + del value["_"] + + for selector_key, selector_value in value.items(): + selectors[selector_key] = SelectorTemplate.from_dict(selector_value) + + if base_selector: + selectors["_"] = SelectorTemplate.from_dict(base_selector, children=selectors) + + 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): + if attr in self._sub_components: + return self._sub_components[attr] + elif attr in self._selectors: + return self._selectors[attr] + elif attr in self._labels: + return self._labels[attr] + elif attr in self._text: + return self._text[attr] + else: + raise Exception("Failed to find referenced sub-component/selector/label/text [%s]" % attr) + + __getitem__ = __getattr__ + + def __str__(self): + return "Component[%s]" % self._name diff --git a/test/galaxy_selenium/data.py b/test/galaxy_selenium/data.py index 5c25bb09b65..f96a423c2c0 100644 --- a/test/galaxy_selenium/data.py +++ b/test/galaxy_selenium/data.py @@ -1,158 +1,10 @@ -import re -import string - import yaml from pkg_resources import resource_string -from selenium.webdriver.common.by import By +from .components import Component -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 2ad75fa062e..72d450b3427 100644 --- a/test/galaxy_selenium/has_driver.py +++ b/test/galaxy_selenium/has_driver.py @@ -130,6 +130,14 @@ class HasDriver: ) return element + def wait_for_absent(self, selector_template, **kwds): + element = self._wait_on( + lambda driver: len(driver.find_elements(*selector_template.element_locator)) == 0, + "%s to become absent" % selector_template.description, + **kwds + ) + 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), diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index e9abe3ddd8c..99b597b9d2d 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -18,8 +18,7 @@ from galaxy.util.bunch import Bunch from . import sizzle from .data import ( - NAVIGATION, # newer variant of NAVIGATION_DATA - NAVIGATION_DATA, + NAVIGATION, ) from .has_driver import ( exception_indicates_not_clickable, @@ -27,6 +26,7 @@ from .has_driver import ( HasDriver, TimeoutException, ) +from .smart_components import SmartComponent # Test case data DEFAULT_PASSWORD = '123456' @@ -137,6 +137,10 @@ class NavigatesGalaxy(HasDriver): def navigation(self): return NAVIGATION + @property + def components(self): + return SmartComponent(self.navigation, self) + def wait_length(self, wait_type): return wait_type.default_length * self.timeout_multiplier @@ -240,6 +244,15 @@ class NavigatesGalaxy(HasDriver): def history_panel_wait_for_hid_ok(self, hid, allowed_force_refreshes=0): self.history_panel_wait_for_hid_state(hid, 'ok', allowed_force_refreshes=allowed_force_refreshes) + def history_panel_item_component(self, history_item=None, hid=None): + if history_item is None: + assert hid + history_item = self.hid_to_history_item(hid) + return self.components.history_panel.item.selector( + history_content_type=history_item["history_content_type"], + id=history_item["id"] + ) + def history_panel_wait_for_hid_visible(self, hid, allowed_force_refreshes=0): current_history_id = self.current_history_id() @@ -249,12 +262,8 @@ class NavigatesGalaxy(HasDriver): timeout = self.timeout_for(wait_type=WAIT_TYPES.JOB_COMPLETION) 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 = self.navigation.history_panel.item.selector( - history_content_type=history_item["history_content_type"], - id=history_item["id"] - ) + history_item = self.hid_to_history_item(hid, current_history_id=current_history_id) + history_item_selector = self.history_panel_item_component(history_item) try: self.history_item_wait_for(history_item_selector, allowed_force_refreshes) except self.TimeoutException as e: @@ -265,6 +274,13 @@ class NavigatesGalaxy(HasDriver): raise self.prepend_timeout_message(e, message) return history_item_selector + def hid_to_history_item(self, hid, current_history_id=None): + if current_history_id is None: + 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] + return history_item + def history_item_wait_for(self, history_item_selector, allowed_force_refreshes): attempt = 0 while True: @@ -286,13 +302,8 @@ class NavigatesGalaxy(HasDriver): 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 = self.navigation.history_panel.item.selector( - history_content_type=history_item["history_content_type"], - id=history_item["id"] - ) + history_item = self.hid_to_history_item(hid) + history_item_selector = self.history_panel_item_component(history_item) self.wait_for_absent_or_hidden(history_item_selector, wait_type=WAIT_TYPES.JOB_COMPLETION) return history_item_selector @@ -595,8 +606,8 @@ class NavigatesGalaxy(HasDriver): def libraries_open(self): self.home() self.click_masthead_shared_data() - self.click_label(self.navigation_data["labels"]["masthead"]["sharedMenu"]["libraries"]) - self.wait_for_selector('.library_style_container') + self.components.masthead.libraries.wait_for_and_click() + self.components.libraries.selector.wait_for_visible() @retry_during_transitions def libraries_index_table_elements(self): @@ -704,7 +715,7 @@ class NavigatesGalaxy(HasDriver): ) def workflow_index_click_import(self): - self.wait_for_and_click_selector(self.test_data["selectors"]["workflows"]["import_button"]) + return self.components.workflows.import_button.wait_for_and_click() def workflow_index_rename(self, new_name, workflow_index=0): self.workflow_index_click_option("Rename", workflow_index=workflow_index) @@ -801,16 +812,13 @@ class NavigatesGalaxy(HasDriver): self.wait_for_and_click_selector("button#execute") def click_masthead_user(self): - 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.wait_for_and_click(self.navigation.masthead.labels.workflow) + self.components.masthead.user.wait_for_and_click() def click_masthead_shared_data(self): - self.click_xpath(self.navigation_data["selectors"]["masthead"]["shared_data"]) + self.components.masthead.shared_data.wait_for_and_click() + + def click_masthead_workflow(self): + self.components.masthead.workflow.wait_for_and_click() def click_button_new_workflow(self): self.wait_for_and_click(self.navigation.workflows.selectors.new_button) @@ -824,22 +832,14 @@ class NavigatesGalaxy(HasDriver): @retry_during_transitions def click_history_options(self): - history_options_button_selector = self.test_data["historyOptions"]["selectors"]["button"] - history_options_element = self.wait_for_selector(history_options_button_selector) - assert history_options_element.is_displayed() - - history_options_button_icon_selector = self.test_data["historyOptions"]["selectors"]["buttonIcon"] - history_options_button_icon_element = self.wait_for_selector(history_options_button_icon_selector) - assert history_options_button_icon_element.is_displayed() - - history_options_element.click() + self.components.history_panel.options_button_icon.wait_for_and_click() def click_history_option(self, option_label): # Open menu self.click_history_options() # Click labelled option - self.wait_for_visible(self.navigation.history_panel.selectors.options_menu) + self.wait_for_visible(self.navigation.history_panel.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() @@ -850,14 +850,14 @@ class NavigatesGalaxy(HasDriver): self.wait_for_and_click_selector(search_selector) def history_panel_add_tags(self, tags): - tag_icon_selector = self.test_data['historyPanel']['selectors']['history']['tagIcon'] - tag_area_selector = self.test_data['historyPanel']['selectors']['history']['tagArea'] + tag_icon_selector = self.components.history_panel.tag_icon + tag_area_selector = self.components.history_panel.tag_area + tag_area_input_selector = self.components.history_panel.tag_area_input - if not self.selector_is_displayed(tag_area_selector): - self.wait_for_and_click_selector(tag_icon_selector) + if not tag_area_selector.is_displayed: + tag_icon_selector.wait_for_and_click() - tag_area_selector += ' .tags-input input' - tag_area = self.wait_for_and_click_selector(tag_area_selector) + tag_area = tag_area_input_selector.wait_for_and_click() for tag in tags: tag_area.send_keys(tag) @@ -935,52 +935,37 @@ class NavigatesGalaxy(HasDriver): def modal_body_selector(self): return ".modal-body" - def history_panel_item_body_selector(self, hid, wait=False): - selector = "%s %s" % (self.history_panel_item_selector(hid), self.test_data["historyPanel"]["selectors"]["hda"]["body"]) + def history_panel_item_body_component(self, hid, wait=False): + details_component = self.history_panel_item_component(hid=hid).details if wait: - self.wait_for_selector_visible(selector) - return selector - - def hda_div_selector(self, hda_id): - return "#dataset-%s" % hda_id - - def hda_body_selector(self, hda_id): - return "%s %s" % (self.hda_div_selector(hda_id), self.test_data["historyPanel"]["selectors"]["hda"]["body"]) + details_component.wait_for_visible() + return details_component def hda_click_primary_action_button(self, hid, button_key): - self.history_panel_click_item_title(hid=hid, wait=True) - body_selector = self.history_panel_item_body_selector(hid=hid, wait=True) + item_component = self.history_panel_click_item_title(hid=hid, wait=True) + item_component.primary_action_buttons.wait_for_visible() + button_component = item_component["%s_button" % button_key] + button_component.wait_for_and_click() - buttons_selector = body_selector + " " + self.test_data["historyPanel"]["selectors"]["hda"]["primaryActionButtons"] - self.wait_for_selector_visible(buttons_selector) - - button_def = self.test_data["historyPanel"]["hdaPrimaryActionButtons"][button_key] - button_selector = button_def["selector"] - return self.wait_for_and_click_selector("%s %s" % (buttons_selector, button_selector)) - - def history_panel_click_item_title(self, **kwds): - if "hda_id" in kwds: - item_selector = self.hda_div_selector(kwds["hda_id"]) - else: - item_selector = self.history_panel_item_selector(kwds["hid"]) - title_selector = "%s .title" % item_selector - details_selector = "%s .details" % item_selector - details_displayed = self.selector_is_displayed(details_selector) - self.wait_for_and_click_selector(title_selector) + def history_panel_click_item_title(self, hid, **kwds): + item_component = self.history_panel_item_component(hid=hid) + details_component = item_component.details + details_displayed = details_component.is_displayed + item_component.title.wait_for_and_click() if kwds.get("wait", False): if details_displayed: - self.wait_for_selector_absent_or_hidden(details_selector) + details_component.wait_for_absent_or_hidden() else: - self.wait_for_selector_visible(details_selector) + details_component.wait_for_visible() + return item_component def history_panel_ensure_showing_item_details(self, hid): if not self.history_panel_item_showing_details(hid): self.history_panel_click_item_title(hid=hid, wait=True) def history_panel_item_showing_details(self, hid): - item_selector = self.history_panel_item_selector(hid) - details_selector = "%s .details" % item_selector - return self.selector_is_displayed(details_selector) + item_component = self.history_panel_item_component(hid=hid) + return item_component.details.is_displayed def collection_builder_set_name(self, name): name_element = self.wait_for_selector_visible("input.collection-name") @@ -1045,8 +1030,8 @@ class NavigatesGalaxy(HasDriver): return element def get_tooltip_text(self, element, sleep=0, click_away=True): - tooltip_selector = self.test_data["selectors"]["tooltipBalloon"] - self.wait_for_selector_absent(tooltip_selector) + tooltip_balloon = self.components._.tooltip_balloon + tooltip_balloon.wait_for_absent() action_chains = self.action_chains() action_chains.move_to_element(element) @@ -1055,7 +1040,7 @@ class NavigatesGalaxy(HasDriver): if sleep > 0: time.sleep(sleep) - tooltip_element = self.wait_for_selector_visible(tooltip_selector) + tooltip_element = tooltip_balloon.wait_for_visible() text = tooltip_element.text if click_away: self.click_center() @@ -1073,6 +1058,14 @@ class NavigatesGalaxy(HasDriver): """ return self.assert_selector_absent_or_hidden(selector) + @retry_during_transitions + def assert_absent_or_hidden_after_transitions(self, selector): + """Variant of assert_absent_or_hidden that retries during transitions. + + See details above for more information about this. + """ + return self.assert_absent_or_hidden(selector) + def assert_tooltip_text(self, element, expected, sleep=0, click_away=True): if hasattr(expected, "text"): expected = expected.text @@ -1085,8 +1078,8 @@ class NavigatesGalaxy(HasDriver): def assert_warning_message(self, contains=None): return self._assert_message("warning", contains=contains) - def _assert_message(self, type, contains=None): - element = self.wait_for_selector_visible(self.test_data["selectors"]["messages"][type]) + def _assert_message(self, message_type, contains=None): + element = self.components._.messages[message_type].wait_for_visible() assert element, "No error message found, one expected." if contains is not None: text = element.text @@ -1095,7 +1088,7 @@ class NavigatesGalaxy(HasDriver): raise AssertionError(message) def assert_no_error_message(self): - self.wait_for_selector_absent(self.test_data["selectors"]["messages"]["error"]) + self.components._.messages.error.assert_absent_or_hidden() def run_tour_step(self, step, step_index, tour_callback): preclick = step.get("preclick", []) diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index 8e7ef87d206..b4119d3653f 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -10,6 +10,17 @@ _: # global stuff selectors: editable_text: '.editable-text' + tooltip_balloon: '.tooltip' + + messages: + selectors: + all: '[class*="message"]' + error: '.errormessage' + warning: '.alert-warning' + done: '.donemessage' + info: '.infomessage' + donelarge: '.donemessagelarge' + infolarge: '.infomessagelarge' masthead: @@ -34,7 +45,7 @@ masthead: # top-level menus analyze: 'Analyze Data' workflow: 'Workflow' - libraries: 'Shared Data' + shared_data: 'Shared Data' visualization: 'Visualization' help: 'Help' user: 'User' @@ -43,6 +54,12 @@ masthead: register: 'Register' login: 'Login' logout: 'Logout' + custom_builds: 'Custom Builds' + saved_histories: 'Saved Histories' + + # Shared data + libraries: 'Data Libraries' + published_histories: 'Histories' history_panel: menu: @@ -53,15 +70,28 @@ history_panel: selectors: _: '#${history_content_type}-${id}' - title: '${item_selector} .title' - hid: '${item_selector} .hid' - name: '${item_selector} .name' - titleButtonArea: '${item_selector} .primary-actions' - summary: '${item_selector} .summary' - blurb: '${item_selector} .blurb .value' - dbkey: '${item_selector} .dbkey .value' - info: '${item_selector} .info .value' - body: '${item_selector} .details' + title: '${_} .title' + hid: '${_} .hid' + name: '${_} .name' + details: '${_} .details' + title_button_area: '${_} .primary-actions' + primary_action_buttons: '${_} .actions .left' + secondary_action_buttons: '${_} .actions .right' + summary: '${_} .summary' + blurb: '${_} .blurb .value' + dbkey: '${_} .dbkey .value' + info: '${_} .info .value' + peek: '${_} .dataset-peek' + + # Title buttons... + display_button: '${_} .icon-btn.display-btn' + edit_button: '${_} .icon-btn.edit-btn' + delete_button: '${_} .icon-btn.delete-btn' + + # Action buttons... + download_button: '${_} .icon-btn.download-btn' + info_button: '${_} .icon-btn.params-btn' + rerun_button: '${_} .icon-btn.rerun-btn' multi_operations: selectors: @@ -83,9 +113,10 @@ history_panel: contents: '#current-history-panel .list-items div.history-content' empty_message: '.empty-message' - + size: '.history-size' tag_icon: '.actions .history-tag-btn' tag_area: '.controls .tags-display' + tag_area_input: '.controls .tags-display .tags-input input' annotation_icon: '.actions .history-annotate-btn' annotation_area: '.controls .annotation-display' annotation_editable_text: '.controls .annotation-display .annotation.editable-text' @@ -98,6 +129,8 @@ history_panel: text: tooltip_name: 'Click to rename history' + new_name: 'Unnamed history' + new_size: '(empty)' collection_builders: @@ -107,6 +140,15 @@ collection_builders: forward_datasets: ".forward-column .column-datasets" reverse_datasets: ".reverse-column .column-datasets" +histories: + sharing: + selectors: + user_n_menu: '#user-${n}-popup' + share_with_a_user_button: '#share_with_a_user' + labels: + unshare: 'Unshare' + + login: selectors: form: 'form#login' @@ -126,6 +168,9 @@ workflows: libraries: + selectors: + _: .library_style_container + folder: selectors: # There needs to a different class or ID on the dataset button, this isn't diff --git a/test/galaxy_selenium/smart_components.py b/test/galaxy_selenium/smart_components.py new file mode 100644 index 00000000000..201436a36dc --- /dev/null +++ b/test/galaxy_selenium/smart_components.py @@ -0,0 +1,85 @@ +from .components import ( + Component, + Target, +) + + +class SmartComponent(object): + """Wrap a Component with driver aware methods. + + Allows smarter selectors that know how to wait for themselves, test themselves, + click themselvers, etc.... More "magic", but much cleaner usage. + """ + + def __init__(self, component, has_driver): + self._component = component + self._has_driver = has_driver + + def __getattr__(self, name): + return self._wrap(getattr(self._component, name)) + + def __getitem__(self, name): + return self._wrap(self._component[name]) + + def _wrap(self, simple_object): + if isinstance(simple_object, Component): + return SmartComponent(simple_object, self._has_driver) + elif isinstance(simple_object, Target): + return SmartTarget(simple_object, self._has_driver) + else: + return simple_object + + +class SmartTarget(object): + """Wrap a Target with driver aware methods. + """ + + def __init__(self, target, has_driver): + self._target = target + self._has_driver = has_driver + + def __call__(self, *args, **kwds): + return self._wrap(self._target(*args, **kwds)) + + def __getattr__(self, name): + return self._wrap(getattr(self._target, name)) + + def __getitem__(self, name): + return self._wrap(self._target[name]) + + def _wrap(self, simple_object): + if isinstance(simple_object, Target): + return SmartTarget(simple_object, self._has_driver) + else: + return simple_object + + def wait_for_and_click(self, **kwds): + return self._has_driver.wait_for_and_click(self._target, **kwds) + + def wait_for_visible(self, **kwds): + return self._has_driver.wait_for_visible(self._target, **kwds) + + def wait_for_text(self, **kwds): + return self._has_driver.wait_for_visible(self._target, **kwds).text + + @property + def is_displayed(self): + return self._has_driver.is_displayed(self._target) + + def wait_for_absent_or_hidden(self, **kwds): + self._has_driver.wait_for_absent_or_hidden(self._target, **kwds) + + def wait_for_absent(self, **kwds): + self._has_driver.wait_for_absent(self._target, **kwds) + + def assert_absent(self, **kwds): + self._has_driver.assert_absent(self._target, **kwds) + + def assert_absent_or_hidden(self, **kwds): + self._has_driver.assert_absent_or_hidden(self._target, **kwds) + + def assert_absent_or_hidden_after_transitions(self, **kwds): + self._has_driver.assert_absent_or_hidden_after_transitions(self._target, **kwds) + + def has_class(self, class_name): + return class_name in self._has_driver.driver.find_element(*self._target.element_locator).get_attribute("class") diff --git a/test/selenium_tests/framework.py b/test/selenium_tests/framework.py index bfebc600922..4f83ded97e7 100644 --- a/test/selenium_tests/framework.py +++ b/test/selenium_tests/framework.py @@ -272,31 +272,20 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy, UsesApiTestCaseMixin base = self.url return urljoin(base, url) - @property - def test_data(self): - return self.navigation_data - def assert_initial_history_panel_state_correct(self): # Move into a TestsHistoryPanel mixin - unnamed_name = self.test_data["historyPanel"]["text"]["history"]["newName"] + unnamed_name = self.components.history_panel.new_name.text name_element = self.history_panel_name_element() assert name_element.is_displayed() assert unnamed_name in name_element.text - size_selector = self.test_data["historyPanel"]["selectors"]["history"]["size"] - initial_size_str = self.test_data["historyPanel"]["text"]["history"]["newSize"] + initial_size_str = self.components.history_panel.new_size.text + size_selector = self.components.history_panel.size + size_text = size_selector.wait_for_text() + assert initial_size_str in size_text, "%s not in %s" % (initial_size_str, size_text) - size_element = self.wait_for_selector(size_selector) - assert size_element.is_displayed() - assert initial_size_str in size_element.text, "%s not in %s" % (initial_size_str, size_element.text) - - empty_msg_selector = self.test_data["historyPanel"]["selectors"]["history"]["emptyMsg"] - empty_msg_str = self.test_data["historyPanel"]["text"]["history"]["emptyMsg"] - - empty_msg_element = self.wait_for_selector(empty_msg_selector) - assert empty_msg_element.is_displayed() - assert empty_msg_str in empty_msg_element.text + self.components.history_panel.empty_message.wait_for_visible() def admin_login(self): self.home() @@ -355,50 +344,35 @@ class SharedStateSeleniumTestCase(SeleniumTestCase): class UsesHistoryItemAssertions: def assert_item_peek_includes(self, hid, expected): - item_body_selector = self.history_panel_item_body_selector(hid=hid, wait=True) - peek_selector = item_body_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["peek"] - peek_selector = self.wait_for_selector_visible(peek_selector) + item_body = self.history_panel_item_component(hid=hid) + peek_text = item_body.peek.wait_for_text() + assert expected in peek_text def assert_item_info_includes(self, hid, expected): - item_body_selector = self.history_panel_item_body_selector(hid=hid, wait=True) - info_selector = item_body_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["info"] - info_element = self.wait_for_selector_visible(info_selector) - text = info_element.text - assert expected in text, "Failed to find expected info text [%s] in info [%s]" % (expected, text) + item_body = self.history_panel_item_component(hid=hid) + info_text = item_body.info.wait_for_text() + assert expected in info_text, "Failed to find expected info text [%s] in info [%s]" % (expected, info_text) def assert_item_dbkey_displayed_as(self, hid, dbkey): - item_body_selector = self.history_panel_item_body_selector(hid=hid, wait=True) - dbkey_selector = item_body_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["dbkey"] - dbkey_element = self.wait_for_selector_visible(dbkey_selector) - assert dbkey in dbkey_element.text + item_body = self.history_panel_item_component(hid=hid) + dbkey_text = item_body.dbkey.wait_for_text() + assert dbkey in dbkey_text def assert_item_summary_includes(self, hid, expected_text): - item_body_selector = self.history_panel_item_body_selector(hid=hid, wait=True) - summary_selector = "%s %s" % (item_body_selector, self.test_data["historyPanel"]["selectors"]["hda"]["summary"]) - summary_element = self.wait_for_selector_visible(summary_selector) - text = summary_element.text - assert expected_text in text, "Expected summary [%s] not found in [%s]." % (expected_text, text) + item_body = self.history_panel_item_component(hid=hid) + summary_text = item_body.summary.wait_for_text() + assert expected_text in summary_text, "Expected summary [%s] not found in [%s]." % (expected_text, summary_text) - def assert_item_name(self, hid, name): - item_selector = self.history_panel_item_selector(hid, wait=True) - title_selector = item_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["name"] - title_element = self.wait_for_selector_visible(title_selector) - assert title_element.text == name, title_element.text + def assert_item_name(self, hid, expected_name): + item_body = self.history_panel_item_component(hid=hid) + name = item_body.name.wait_for_text() + assert name == expected_name, name def assert_item_hid_text(self, hid): # Check the text HID matches HID returned from API. - item_selector = self.history_panel_item_selector(hid, wait=True) - hid_selector = item_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["hid"] - hid_element = self.wait_for_selector_visible(hid_selector) - assert hid_element.text == str(hid), hid_element.text - - def _assert_item_button(self, buttons_area, expected_button, button_def): - selector = button_def["selector"] - # Let old tooltip expire, etc... - self.sleep_for(self.wait_types.UX_TRANSITION) - button_item = self.wait_for_selector_visible("%s %s" % (buttons_area, selector)) - expected_tooltip = button_def.get("tooltip") - self.assert_tooltip_text(button_item, expected_tooltip) + item_body = self.history_panel_item_component(hid=hid) + hid_text = item_body.hid.wait_for_text() + assert hid_text == str(hid), hid_text def default_web_host_for_selenium_tests(): diff --git a/test/selenium_tests/test_anon_history.py b/test/selenium_tests/test_anon_history.py index 4b94e8dc757..2f14fa29638 100644 --- a/test/selenium_tests/test_anon_history.py +++ b/test/selenium_tests/test_anon_history.py @@ -11,14 +11,12 @@ class AnonymousHistoriesTestCase(SeleniumTestCase): self.home() self.assert_initial_history_panel_state_correct() - tag_icon_selector = self.navigation.history_panel.selectors.tag_icon - annotation_icon_selector = self.navigation.history_panel.selectors.annotation_icon - self.assert_absent_or_hidden(tag_icon_selector) - self.assert_absent_or_hidden(annotation_icon_selector) + # Anonymous users can annotate or tag, these components should be absent. + self.components.history_panel.tag_icon.assert_absent_or_hidden() + self.components.history_panel.annotation_icon.assert_absent_or_hidden() # History has a name but... - name_element = self.wait_for_present(self.navigation.history_panel.selectors.name) - name_element.click() + name_element = self.components.history_panel.name.wait_for_and_click() # ... name should NOT be editable when clicked by anon-user editable_text_class = self.navigation._.selectors.editable_text @@ -29,21 +27,18 @@ class AnonymousHistoriesTestCase(SeleniumTestCase): self.home() self.perform_upload(self.get_filename("1.txt")) self.wait_for_history() - hda = self.latest_history_item() + # Reload the history and make sure the state is preserved. self.home() - element = self.wait_for_selector(self.hda_div_selector(hda["id"])) - assert 'state-ok' in element.get_attribute("class") + self.history_panel_wait_for_hid_state(1, "ok") # empty should be NO LONGER be displayed - self.assert_absent_or_hidden(self.navigation.history_panel.selectors.empty_message) + self.components.history_panel.empty_message.assert_absent_or_hidden() @selenium_test def test_anon_history_after_registration(self): self._upload_file_anonymous_then_register_user() - hda = self.latest_history_item() self.home() - element = self.wait_for_selector(self.hda_div_selector(hda["id"])) - assert 'state-ok' in element.get_attribute("class") + self.history_panel_wait_for_hid_state(1, "ok") @selenium_test def test_clean_anon_history_after_logout(self): diff --git a/test/selenium_tests/test_custom_builds.py b/test/selenium_tests/test_custom_builds.py index d4684c73d6a..c678540381a 100644 --- a/test/selenium_tests/test_custom_builds.py +++ b/test/selenium_tests/test_custom_builds.py @@ -90,7 +90,7 @@ class CustomBuildsTestcase(SharedStateSeleniumTestCase): def navigate_to_custom_builds_page(self): self.home() self.click_masthead_user() # Open masthead menu - self.wait_for_and_click_selector('a[href="/custom_builds"]') + self.components.masthead.custom_builds.wait_for_and_click() def setup_shared_state(self): CustomBuildsTestcase.user_email = self._get_random_email() diff --git a/test/selenium_tests/test_history_dataset_state.py b/test/selenium_tests/test_history_dataset_state.py index 95b86b015de..8c1ce882997 100644 --- a/test/selenium_tests/test_history_dataset_state.py +++ b/test/selenium_tests/test_history_dataset_state.py @@ -4,6 +4,15 @@ from .framework import ( UsesHistoryItemAssertions, ) +BUTTON_TOOLTIPS = { + "display": 'View data', + "edit": 'Edit attributes', + "delete": 'Delete', + "download": 'Download', + "info": 'View details', + "rerun": 'Run this job again', +} + class HistoryDatasetStateTestCase(SeleniumTestCase, UsesHistoryItemAssertions): @@ -16,8 +25,9 @@ class HistoryDatasetStateTestCase(SeleniumTestCase, UsesHistoryItemAssertions): self.assert_item_hid_text(1) self._assert_title_buttons(1) - # Expand HDA + # Expand HDA and wait for details to show up. self.history_panel_click_item_title(hid=1, wait=True) + self.history_panel_item_body_component(1, wait=True) self.assert_item_summary_includes(1, "1 sequence") self.assert_item_dbkey_displayed_as(1, "?") @@ -27,20 +37,15 @@ class HistoryDatasetStateTestCase(SeleniumTestCase, UsesHistoryItemAssertions): self._assert_action_buttons(1) def _assert_title_buttons(self, hid, expected_buttons=['display', 'edit', 'delete']): - item_selector = self.history_panel_item_selector(hid, wait=True) - buttons_area = item_selector + ' ' + self.test_data["historyPanel"]["selectors"]["hda"]["titleButtonArea"] - buttons = self.test_data["historyPanel"]["hdaTitleButtons"] - - for expected_button in expected_buttons: - button = buttons[expected_button] - self._assert_item_button(buttons_area, expected_button, button) + self._assert_buttons(hid, expected_buttons) def _assert_action_buttons(self, hid, expected_buttons=["info", "download"]): - item_body_selector = self.history_panel_item_body_selector(hid=hid) - self.wait_for_selector_visible(item_body_selector) - - buttons_selector = item_body_selector + " " + self.test_data["historyPanel"]["selectors"]["hda"]["primaryActionButtons"] - self.wait_for_selector_visible(buttons_selector) + self._assert_buttons(hid, expected_buttons) + def _assert_buttons(self, hid, expected_buttons): + item_button = self.history_panel_item_component(hid=hid) + # Let old tooltip expire, etc... for expected_button in expected_buttons: - self._assert_item_button(buttons_selector, expected_button, self.test_data["historyPanel"]["hdaPrimaryActionButtons"][expected_button]) + self.sleep_for(self.wait_types.UX_TRANSITION) + button = item_button["%s_button" % expected_button] + self.assert_tooltip_text(button.wait_for_visible(), BUTTON_TOOLTIPS[expected_button]) diff --git a/test/selenium_tests/test_history_options.py b/test/selenium_tests/test_history_options.py index 0be01e55785..a97a82e915e 100644 --- a/test/selenium_tests/test_history_options.py +++ b/test/selenium_tests/test_history_options.py @@ -21,13 +21,9 @@ class HistoryOptionsTestCase(SeleniumTestCase): self.wait_for_absent_or_hidden(menu_selector) - hda = self.latest_history_item() - hid = hda["hid"] + hid = 1 self.history_panel_click_item_title(hid=hid, wait=True) - - hda_body_selector = self.hda_body_selector(hda["id"]) - self.wait_for_selector_visible(hda_body_selector) - + item_component = self.history_panel_item_body_component(hid=hid) + item_component.wait_for_visible() self.history_panel_click_item_title(hid=hid, wait=True) - - self.assert_selector_absent_or_hidden_after_transitions(hda_body_selector) + item_component.assert_absent_or_hidden_after_transitions() diff --git a/test/selenium_tests/test_history_panel.py b/test/selenium_tests/test_history_panel.py index f0c853ca886..739e9480972 100644 --- a/test/selenium_tests/test_history_panel.py +++ b/test/selenium_tests/test_history_panel.py @@ -90,7 +90,7 @@ class HistoryPanelTestCase(SeleniumTestCase): # Open the details, verify they are open and do a refresh. self.history_panel_ensure_showing_item_details(hid=1) - self.history_panel_item_body_selector(1, wait=True) + self.history_panel_item_body_component(1, wait=True) self.history_panel_refresh_click() # After the refresh, verify the details are still open. diff --git a/test/selenium_tests/test_history_sharing.py b/test/selenium_tests/test_history_sharing.py index 7129c2713ff..1da3f80bd96 100644 --- a/test/selenium_tests/test_history_sharing.py +++ b/test/selenium_tests/test_history_sharing.py @@ -29,16 +29,14 @@ class HistorySharingTestCase(SeleniumTestCase): self.submit_login(user1_email, retries=VALID_LOGIN_RETRIES) self.navigate_to_history_share_page() + user_sharing_menu = self.components.histories.sharing.user_n_menu(n="0") with self.main_panel(): - first_user_element = self.wait_for_selector("#user-0-popup") - first_user_element.click() - - unshare_link = self.wait_for_selector('a[href^="/history/sharing?unshare_user"]') - unshare_link.click() + user_sharing_menu.wait_for_and_click() + self.components.histories.sharing.unshare.wait_for_and_click() self.navigate_to_history_share_page() with self.main_panel(): - self.assert_selector_absent("#user-0-popup") + user_sharing_menu.assert_absent() self.logout_if_needed() self.submit_login(user2_email, retries=VALID_LOGIN_RETRIES) @@ -106,8 +104,7 @@ class HistorySharingTestCase(SeleniumTestCase): def navigate_to_history_user_share_page(self): self.navigate_to_history_share_page() with self.main_panel(): - user_share_link_selector = 'a[href^="/history/share?"]' - self.wait_for_and_click_selector(user_share_link_selector) + self.components.histories.sharing.share_with_a_user_button.wait_for_and_click() def share_history_with_user(self, user_id=None, user_email=None, assert_valid=False): """Share the current history with a target user by ID or email. diff --git a/test/selenium_tests/test_published_histories_grid.py b/test/selenium_tests/test_published_histories_grid.py index 31cd767351d..701da7ca04d 100644 --- a/test/selenium_tests/test_published_histories_grid.py +++ b/test/selenium_tests/test_published_histories_grid.py @@ -209,6 +209,5 @@ class HistoryGridTestCase(SharedStateSeleniumTestCase): def navigate_to_published_histories_page(self): self.home() - self.click_masthead_libraries() - selector = 'a[href="/histories/list_published"]' - self.wait_for_and_click_selector(selector) + self.click_masthead_shared_data() + self.components.masthead.published_histories.wait_for_and_click() diff --git a/test/selenium_tests/test_registration.py b/test/selenium_tests/test_registration.py index 5ed4fb73632..aec67208f3b 100644 --- a/test/selenium_tests/test_registration.py +++ b/test/selenium_tests/test_registration.py @@ -26,7 +26,7 @@ class RegistrationTestCase(SeleniumTestCase): self.logout_if_needed() assert not self.is_logged_in() self.home() - assert len(self.driver.find_elements_by_xpath(self.test_data["selectors"]["masthead"]["userMenu"]["userEmail_xpath"])) == 0 + self.components.masthead.user_email.assert_absent_or_hidden() @selenium_test def test_reregister_email_fails(self): diff --git a/test/selenium_tests/test_saved_histories.py b/test/selenium_tests/test_saved_histories.py index 5cd1e9e98f4..a12c67fa1ec 100644 --- a/test/selenium_tests/test_saved_histories.py +++ b/test/selenium_tests/test_saved_histories.py @@ -275,7 +275,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): def navigate_to_saved_histories_page(self): self.home() self.click_masthead_user() # Open masthead menu - self.wait_for_and_click_selector('a[href="/histories/list"]') + self.components.masthead.saved_histories.wait_for_and_click() def setup_shared_state(self): SavedHistoriesTestCase.user_email = self._get_random_email()