From 3c64167788ea40fcc95883fdd98d51022c21537f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Mon, 2 Oct 2017 22:04:26 -0400 Subject: [PATCH] Initial work on selenium testing for libraries. - Test importing datasets from histories. - Test basic importing of a dataset from a path. - Test creating a folder. - Test create library, renaming a library, filtering libraries by name, and sorting of libraries. - Test various buttons (e.g. details, help). --- test/galaxy_selenium/navigates_galaxy.py | 88 ++++++++++++++++ test/galaxy_selenium/navigation-data.yml | 6 +- test/galaxy_selenium/navigation.yml | 30 ++++++ test/selenium_tests/framework.py | 21 +++- test/selenium_tests/test_library_contents.py | 103 ++++++++++++++++++ test/selenium_tests/test_library_landing.py | 105 +++++++++++++++++++ 6 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 test/selenium_tests/test_library_contents.py create mode 100644 test/selenium_tests/test_library_landing.py diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index efa72bcb470..e9abe3ddd8c 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -359,6 +359,7 @@ class NavigatesGalaxy(HasDriver): self.wait_for_and_click(self.navigation.masthead.labels.login) with self.main_panel(): + self.sleep_for(WAIT_TYPES.UX_RENDER) form = self.wait_for_visible(self.navigation.login.selectors.form) self.fill(form, login_info) self.snapshot("logging-in") @@ -591,6 +592,90 @@ class NavigatesGalaxy(HasDriver): def workflow_editor_options_menu_element(self): return self.wait_for_selector_visible("#workflow-options-button-menu") + 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') + + @retry_during_transitions + def libraries_index_table_elements(self): + container = self.wait_for_selector_visible(".library_container") + elements = container.find_elements_by_css_selector("#library_list_body") + if not elements: + return [] + else: + assert len(elements) == 1 + element = elements[0] + return element.find_elements_by_css_selector("tr") # [style='display: table-row'] + + def libraries_index_click_create_new(self): + self.wait_for_and_click_selector("#create_new_library_btn") + + def libraries_index_create(self, name): + self.libraries_index_click_create_new() + name_text_box = self.wait_for_selector_clickable("input[name='Name']") + name_text_box.send_keys(name) + + self.wait_for_and_click_selector("#button-0") + + def libraries_index_click_search(self): + self.sleep_for(WAIT_TYPES.UX_RENDER) + search_element = self.wait_for_selector_clickable("input.library-search-input") + search_element.click() + return search_element + + def libraries_index_sort_selector(self): + return ".sort-libraries-link" + + def libraries_index_sort_click(self): + sort_element = self.wait_for_selector_clickable(self.libraries_index_sort_selector()) + sort_element.click() + return sort_element + + def libraries_index_search_for(self, text): + self.wait_for_overlays_cleared() + search_box = self.libraries_index_click_search() + search_box.clear() + search_box.send_keys(text) + value = search_box.get_attribute("value") + assert value == text, value + self.driver.execute_script("$(arguments[0]).keyup();", search_box) + + def libraries_folder_create(self, name): + create_folder_button = self.wait_for_selector_clickable("#toolbtn_create_folder") + create_folder_button.click() + + name_text_box = self.wait_for_selector_clickable("input[name='Name']") + name_text_box.send_keys(name) + + create_button = self.wait_for_selector_clickable("#button-0") + create_button.click() + + def libraries_click_dataset_import(self): + self.wait_for_and_click(self.navigation.libraries.folder.selectors.add_items_button) + + def libraries_dataset_import_from_history(self): + self.libraries_click_dataset_import() + + self.wait_for_visible(self.navigation.libraries.folder.selectors.add_items_menu) + self.wait_for_and_click(self.navigation.libraries.folder.labels.from_history) + + def libraries_dataset_import_from_path(self): + self.libraries_click_dataset_import() + + self.wait_for_visible(self.navigation.libraries.folder.selectors.add_items_menu) + self.wait_for_and_click(self.navigation.libraries.folder.labels.from_path) + + def libraries_table_elements(self): + tbody_element = self.wait_for_selector_visible("#folder_list_body") + return tbody_element.find_elements_by_css_selector("tr")[1:] + + def wait_for_overlays_cleared(self): + """Wait for modals and Toast notifications to disappear.""" + self.wait_for_selector_absent_or_hidden(".ui-modal") + self.wait_for_selector_absent_or_hidden(".toast") + def workflow_index_open(self): self.home() self.click_masthead_workflow() @@ -724,6 +809,9 @@ class NavigatesGalaxy(HasDriver): def click_masthead_workflow(self): self.wait_for_and_click(self.navigation.masthead.labels.workflow) + def click_masthead_shared_data(self): + self.click_xpath(self.navigation_data["selectors"]["masthead"]["shared_data"]) + def click_button_new_workflow(self): self.wait_for_and_click(self.navigation.workflows.selectors.new_button) diff --git a/test/galaxy_selenium/navigation-data.yml b/test/galaxy_selenium/navigation-data.yml index 6e975af6a26..8ff9ae5c0a6 100644 --- a/test/galaxy_selenium/navigation-data.yml +++ b/test/galaxy_selenium/navigation-data.yml @@ -21,6 +21,7 @@ selectors: user: '//ul[@id="user"]' workflow: '//ul[@id="workflow"]' + shared_data: '//ul[@id="shared"]' toolMenu: container: '.toolMenuContainer' @@ -55,7 +56,7 @@ labels: menus: analyze: 'Analyze Data' workflow: 'Workflow' - libraries: 'Shared Data' + shared_data: 'Shared Data' visualization: 'Visualization' help: 'Help' user: 'User' @@ -64,6 +65,9 @@ labels: register: 'Register' login: 'Login' logout: 'Logout' + + sharedMenu: + libraries: 'Data Libraries' tools: upload: diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index 045ff6e7c1c..8e7ef87d206 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -124,6 +124,36 @@ workflows: new_button: '#new-workflow' import_button: '#import-workflow' +libraries: + + folder: + selectors: + # There needs to a different class or ID on the dataset button, this isn't + # a good selector for that button since .add-library-items also applies to + # folder (but it is a button.add-library-items instead). + add_items_button: 'div.add-library-items' + add_items_menu: 'div.add-library-items .dropdown-menu' + add_items_options: 'div.add-library-items .dropdown-menu li a' + + # TODO: Most of these are very good selectors but the same DOM elements + # are reused without adding specific classes, IDs, or roles to anything. + import_modal: '.modal' + import_datasets_ok_button: '.modal-footer .buttons #button-0' + import_datasets_cancel_button: '.modal-footer .buttons #button-1' + import_progress_bar: '.progress-bar-import' + import_history_content: '#selected_history_content' + import_history_contents_items: '#selected_history_content ul li' + import_from_path_textarea: '#import_paths' + + labels: + from_history: 'from History' + from_path: 'from Path' + + dataset: + selectors: + table: '.dataset_table' + table_rows: '.dataset_table table tbody tr' + published_grids: selectors: search: '#input-free-text-search-filter' diff --git a/test/selenium_tests/framework.py b/test/selenium_tests/framework.py index 2cd5a475c7c..bfebc600922 100644 --- a/test/selenium_tests/framework.py +++ b/test/selenium_tests/framework.py @@ -17,6 +17,7 @@ except ImportError: from six.moves.urllib.parse import urljoin from base import populators # noqa: I100 +from base.api import UsesApiTestCaseMixin # noqa: I100 from base.driver_util import classproperty, DEFAULT_WEB_HOST, get_ip_address # noqa: I100 from base.testcase import FunctionalTestCase # noqa: I100 from base.workflows_format_2 import ( # noqa: I100 @@ -36,6 +37,8 @@ DEFAULT_SELENIUM_REMOTE = False DEFAULT_SELENIUM_REMOTE_PORT = "4444" DEFAULT_SELENIUM_REMOTE_HOST = "127.0.0.1" DEFAULT_SELENIUM_HEADLESS = "auto" +DEFAULT_ADMIN_USER = "test@bx.psu.edu" +DEFAULT_ADMIN_PASSWORD = "testpass" TIMEOUT_MULTIPLIER = float(os.environ.get("GALAXY_TEST_TIMEOUT_MULTIPLIER", DEFAULT_TIMEOUT_MULTIPLIER)) GALAXY_TEST_ERRORS_DIRECTORY = os.environ.get("GALAXY_TEST_ERRORS_DIRECTORY", DEFAULT_TEST_ERRORS_DIRECTORY) @@ -51,7 +54,8 @@ GALAXY_TEST_SELENIUM_RETRIES = int(os.environ.get("GALAXY_TEST_SELENIUM_RETRIES" GALAXY_TEST_SELENIUM_USER_EMAIL = os.environ.get("GALAXY_TEST_SELENIUM_USER_EMAIL", None) GALAXY_TEST_SELENIUM_USER_PASSWORD = os.environ.get("GALAXY_TEST_SELENIUM_USER_PASSWORD", None) - +GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL = os.environ.get("GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL", DEFAULT_ADMIN_USER) +GALAXY_TEST_SELENIUM_ADMIN_USER_PASSWORD = os.environ.get("GALAXY_TEST_SELENIUM_ADMIN_USER_PASSWORD", DEFAULT_ADMIN_PASSWORD) try: from nose.tools import nottest @@ -156,7 +160,7 @@ class TestSnapshot(object): write_file_func("%s-stack.txt" % prefix, str(self.stack)) -class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy): +class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy, UsesApiTestCaseMixin): # If run one-off via nosetests, the next line ensures test # tools and datatypes are used instead of configured tools. framework_tool_and_types = True @@ -166,6 +170,7 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy): # GALAXY_TEST_SELENIUM_USER_PASSWORD are set these values # will be used to login. ensure_registered = False + requires_admin = False def setUp(self): super(SeleniumTestCase, self).setUp() @@ -177,6 +182,9 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy): self.target_url_from_selenium = self.url self.snapshots = [] self.setup_driver_and_session() + if self.requires_admin and GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL == DEFAULT_ADMIN_USER: + self._setup_interactor() + self._setup_user(GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL) try: self.setup_with_driver() except Exception: @@ -290,6 +298,15 @@ class SeleniumTestCase(FunctionalTestCase, NavigatesGalaxy): assert empty_msg_element.is_displayed() assert empty_msg_str in empty_msg_element.text + def admin_login(self): + self.home() + self.submit_login( + GALAXY_TEST_SELENIUM_ADMIN_USER_EMAIL, + GALAXY_TEST_SELENIUM_ADMIN_USER_PASSWORD + ) + with self.main_panel(): + self.assert_no_error_message() + @property def workflow_populator(self): return SeleniumSessionWorkflowPopulator(self) diff --git a/test/selenium_tests/test_library_contents.py b/test/selenium_tests/test_library_contents.py new file mode 100644 index 00000000000..1004c07370b --- /dev/null +++ b/test/selenium_tests/test_library_contents.py @@ -0,0 +1,103 @@ +from .framework import ( + retry_assertion_during_transitions, + selenium_test, + SeleniumTestCase, +) + + +class LibraryContentsTestCase(SeleniumTestCase): + + requires_admin = True + + @selenium_test + def test_create_folder(self): + self._navigate_to_new_library() + self._assert_num_displayed_items_is(0) + self.libraries_folder_create("folder1") + self._assert_num_displayed_items_is(1) + + @selenium_test + def test_import_dataset_from_history(self): + self.admin_login() + self.perform_upload(self.get_filename("1.txt")) + self.wait_for_history() + self._navigate_to_new_library(login=False) + self._assert_num_displayed_items_is(0) + self.sleep_for(self.wait_types.UX_RENDER) + self.libraries_dataset_import_from_history() + # Click the cancel button, make sure modal is hidden. + self.wait_for_visible(self.navigation.libraries.folder.selectors.import_modal) + self.wait_for_and_click(self.navigation.libraries.folder.selectors.import_datasets_cancel_button) + self.wait_for_absent_or_hidden(self.navigation.libraries.folder.selectors.import_modal) + + self.libraries_dataset_import_from_history() + self.wait_for_visible(self.navigation.libraries.folder.selectors.import_history_content) + history_elements = self.find_elements(self.navigation.libraries.folder.selectors.import_history_contents_items) + assert "1.txt" in history_elements[0].text + history_elements[0].find_element_by_css_selector("input").click() + # Add + self.sleep_for(self.wait_types.UX_RENDER) + self.wait_for_and_click(self.navigation.libraries.folder.selectors.import_datasets_ok_button) + # Let the progress bar disappear... + self.wait_for_absent_or_hidden(self.navigation.libraries.folder.selectors.import_progress_bar) + self._assert_num_displayed_items_is(1) + + @selenium_test + def test_import_dataset_from_path(self): + self._navigate_to_new_library() + self._assert_num_displayed_items_is(0) + self.sleep_for(self.wait_types.UX_RENDER) + + # Click the cancel button, make sure modal is hidden. + self.libraries_dataset_import_from_path() + self.wait_for_visible(self.navigation.libraries.folder.selectors.import_modal) + self.wait_for_and_click(self.navigation.libraries.folder.selectors.import_datasets_cancel_button) + self.wait_for_absent_or_hidden(self.navigation.libraries.folder.selectors.import_modal) + + # Try again... this time actually select some paths. + self.libraries_dataset_import_from_path() + textarea = self.wait_for_and_click(self.navigation.libraries.folder.selectors.import_from_path_textarea) + textarea.send_keys("test-data/1.txt") + self.sleep_for(self.wait_types.UX_RENDER) + self.wait_for_and_click(self.navigation.libraries.folder.selectors.import_datasets_ok_button) + # Let the progress bar disappear... + self.wait_for_absent_or_hidden(self.navigation.libraries.folder.selectors.import_progress_bar) + self._assert_num_displayed_items_is(1) + + self.click_label("1.txt") + self.wait_for_visible(self.navigation.libraries.dataset.selectors.table) + elements = self.find_elements(self.navigation.libraries.dataset.selectors.table_rows) + table_as_dict = {} + for element in elements: + key = element.find_element_by_tag_name("th").text + value = element.find_element_by_tag_name("td").text + table_as_dict[key] = value + + assert table_as_dict["Name"] == "1.txt", table_as_dict + assert table_as_dict["Genome build"] == "?", table_as_dict + + @selenium_test + def test_show_details(self): + self._navigate_to_new_library() + self.sleep_for(self.wait_types.UX_RENDER) + self.wait_for_selector_clickable(".toolbtn-show-locinfo").click() + self.sleep_for(self.wait_types.UX_RENDER) + self.wait_for_selector_clickable(".ui-modal #button-0").click() + self.wait_for_overlays_cleared() + + @retry_assertion_during_transitions + def _assert_num_displayed_items_is(self, n): + self.assertEqual(n, self._num_displayed_items()) + + def _num_displayed_items(self): + return len(self.libraries_table_elements()) + + def _navigate_to_new_library(self, login=True): + if login: + self.admin_login() + self.libraries_open() + self.name = self._get_random_name(prefix="testcontents") + self.libraries_index_create(self.name) + self.wait_for_overlays_cleared() + self.libraries_index_search_for(self.name) + self.libraries_index_table_elements()[0].find_element_by_css_selector("td a").click() diff --git a/test/selenium_tests/test_library_landing.py b/test/selenium_tests/test_library_landing.py new file mode 100644 index 00000000000..490d3b521ee --- /dev/null +++ b/test/selenium_tests/test_library_landing.py @@ -0,0 +1,105 @@ +from .framework import ( + retry_assertion_during_transitions, + selenium_test, + SeleniumTestCase, +) + + +class LibraryLandingTestCase(SeleniumTestCase): + + requires_admin = True + + def setup_with_driver(self): + self.admin_login() + self.libraries_open() + + @selenium_test + def test_create_new_close(self): + num_displayed_libraries = self._num_displayed_libraries() + self.libraries_index_click_create_new() + self.wait_for_selector_visible(".ui-modal") + close_button = self.wait_for_selector_clickable("#button-1") + close_button.click() + self.wait_for_overlays_cleared() + assert self._num_displayed_libraries() == num_displayed_libraries + + @selenium_test + def test_create_new(self): + name = self._get_random_name(prefix="testcreatenew") + self.libraries_index_create(name) + + # Must be showing at least one library now. + self._assert_at_least_one_library_displayed() + + self._search_for_only_with_name(name) + + @selenium_test + def test_rename(self): + name = self._get_random_name(prefix="testprerename") + self.libraries_index_create(name) + self._search_for_only_with_name(name) + + edit_button = self.wait_for_selector_clickable(".edit_library_btn") + edit_button.click() + + new_name = self._get_random_name(prefix="testpostrename") + name_box = self.wait_for_selector_clickable(".input_library_name") + name_box.send_keys(new_name) + + save_button = self.wait_for_selector_clickable(".save_library_btn") + save_button.click() + + self._search_for_only_with_name(new_name) + + @selenium_test + def test_help(self): + help_button = self.wait_for_selector_clickable(".help-button") + help_link = help_button.find_element_by_css_selector("a") + self.assertEqual(help_link.get_attribute("href"), "https://galaxyproject.org/data-libraries/screen/list-of-libraries/") + + @selenium_test + def test_sorting(self): + # Throw in another library to ensure filtering is working... + other = self._get_random_name(prefix="notthesame") + self.libraries_index_create(other) + self.wait_for_overlays_cleared() + + namebase = self._get_random_name(prefix="testsort") + self.libraries_index_create(namebase + " b") + self.wait_for_overlays_cleared() + self.libraries_index_create(namebase + " a") + self.wait_for_overlays_cleared() + self.libraries_index_create(namebase + " c") + + self.libraries_index_search_for(namebase) + + self._assert_num_displayed_libraries_is(3) + + self._assert_names_are([namebase + " a", namebase + " b", namebase + " c"]) + self.libraries_index_sort_click() + + # TODO: Usability bug in libraries - should need to resort - it should + # sort the entities on the screen. + self.libraries_index_search_for(namebase) + + self._assert_names_are([namebase + " c", namebase + " b", namebase + " a"]) + + def _search_for_only_with_name(self, name): + self.libraries_index_search_for(name) + self._assert_num_displayed_libraries_is(1) + + @retry_assertion_during_transitions + def _assert_names_are(self, expected_names): + names = [e.find_element_by_css_selector("td a").text for e in self.libraries_index_table_elements()] + self.assertEqual(names, expected_names) + + @retry_assertion_during_transitions + def _assert_at_least_one_library_displayed(self): + assert self._num_displayed_libraries() > 0 + + @retry_assertion_during_transitions + def _assert_num_displayed_libraries_is(self, n): + self.assertEqual(n, self._num_displayed_libraries()) + + def _num_displayed_libraries(self): + return len(self.libraries_index_table_elements())