horizon/openstack_dashboard/test/integration_tests/regions/menus.py
Manpreet Kaur 3230e1fb15 Changes for tacker-horizon integration tests
The tacker-horizon integration test framework has been implemented[1]
While adding integration test cases a selenium exception [2] was
observed for test cases under directory [3].

Tacker-horizon UI Details:

* Most of the Horizon plugins, such as manila-ui and vitrage-dashboard
  support integration tests, these plugins reside under "Project" tab.
* Whereas, tacker-horizon, mistral-dashboard are horizon plugins which
  have their dashboard anchored directly at the top-level alongside
  "Project" tab.
* The tacker-horizon has two-panel groups,
   * VNF Management
   * NFV Orchestration
* The "VNF Management" is a default panel and remains expanded in NFV
  dashboard, whereas "NFV Orchestration" is collapsed initially.

Selenium Exception Details:

* The horizon framework reports an exception
  ElementNotInteractableException, while opening pages under
  "NFV Orchestration" panel group.
* This error occurs when an element is not clicked or it is not visible
  yet.
* The menu item expansion code in the horizon integration framework
  expands only those menu items (level first, second or third) if only
  the previous menu item at the same level was expanded earlier.
* For example, in order to open a page[4] under NVF Orchestration panel
  group, the framework collapses "Project" tab and clicks "NFV"
  dashboard.
  But it never expands "NFV Orchestration" panel as in second-level no
  such transition occured hence it remains collapsed. So test case fails
  to find desired page.
  On the other hand, test cases for pages under "VNF Management" succeed
  as the panel group by default remains open.

This patch adds functionality to detect collapsed menu items and expand
the items.

[1] https://review.opendev.org/c/openstack/tacker-horizon/+/790958
[2] https://zuul.opendev.org/t/openstack/build/b8bd1618206945b68ebdbcdc7f1bdf10/console
[3] tacker_horizon/test/integration/pages/nfv/nfv_orchestration/
[4] tacker_horizon/test/integration/pages/nfv/nfv_orchestration/vimmanagementpage.py

Change-Id: I8fec54eb5f2a9bf218ee36e9c0a1ce9c15c97a26
2021-08-27 09:07:18 +05:30

471 lines
19 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
from selenium.common import exceptions
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class NavigationAccordionRegion(baseregion.BaseRegion):
"""Navigation menu located in the left."""
_project_security_groups_locator = (
by.By.CSS_SELECTOR, 'a[href*="/project/security_groups/"]')
_project_key_pairs_locator = (
by.By.CSS_SELECTOR, 'a[href*="/project/key_pairs/"]')
_settings_change_password_locator = (
by.By.CSS_SELECTOR, 'a[href*="/settings/password//"]')
_project_bar_locator = (by.By.XPATH,
".//*[@id='main_content']//div[contains(text(),"
"'Project')]")
@property
def project_bar(self):
return self._get_element(*self._project_bar_locator)
_first_level_item_selected_locator = (
by.By.CSS_SELECTOR, '.panel.openstack-dashboard > a:not(.collapsed)')
_second_level_item_selected_locator = (
by.By.CSS_SELECTOR, '.panel.openstack-panel-group > a:not(.collapsed)')
_first_level_item_xpath_template = (
"//li[contains(concat('', @class, ''), 'panel openstack-dashboard') "
"and contains(., '%s')]/a")
_second_level_item_xpath_template = (
"//ul[contains(@class, 'in')]//li[contains(@class, "
"'panel openstack-panel-group') and contains(., '%s')]/a")
_third_level_item_xpath_template = (
"//ul[contains(@class, 'in')]//a[contains(concat('', @class, ''),"
"'list-group-item openstack-panel') and contains(., '%s')]")
_parent_item_locator = (by.By.XPATH, '..')
_menu_list_locator = (by.By.CSS_SELECTOR, 'a')
_expanded_menu_class = ""
_transitioning_menu_class = 'collapsing'
_form_body_locator = (by.By.CSS_SELECTOR, 'body')
def _get_first_level_item_locator(self, text):
return (by.By.XPATH,
self._first_level_item_xpath_template % text)
def _get_second_level_item_locator(self, text):
return (by.By.XPATH,
self._second_level_item_xpath_template % text)
def _get_third_level_item_locator(self, text):
return (by.By.XPATH,
self._third_level_item_xpath_template % text)
def get_first_level_selected_item(self):
if self._is_element_present(*self._first_level_item_selected_locator):
return self._get_element(*self._first_level_item_selected_locator)
else:
return None
def get_second_level_selected_item(self):
if self._is_element_present(*self._second_level_item_selected_locator):
return self._get_element(*self._second_level_item_selected_locator)
else:
return None
@property
def get_form_body(self):
return self._get_element(*self._form_body_locator)
@property
def security_groups(self):
return self._get_element(*self._project_security_groups_locator)
@property
def key_pairs(self):
return self._get_element(*self._project_key_pairs_locator)
@property
def change_password(self):
return self._get_element(*self._settings_change_password_locator)
def _wait_until_transition_ends(self, item, to_be_expanded=False):
def predicate(d):
classes = item.get_attribute('class')
if to_be_expanded:
status = self._expanded_menu_class == classes
else:
status = self._expanded_menu_class is not classes
return status and self._transitioning_menu_class not in classes
self._wait_until(predicate)
def _wait_until_modal_dialog_close(self):
def predicate(d):
classes = self.get_form_body.get_attribute('class')
return classes and "modal-open" not in classes
self._wait_until(predicate)
def _click_menu_item(self, text, loc_craft_func, get_selected_func=None,
src_elem=None):
"""Click on menu item if not selected.
Menu animation that visualize transition from one selection to
another take some time - if clicked on item during this animation
nothing happens, therefore it is necessary to wait for the transition
to complete first.
Third-level menus are handled differently from others. First, they do
not need to collapse other 3rd-level menu item prior to clicking them
(because 3rd-level menus are atomic). Second, clicking them doesn't
initiate an animated transition, hence no need to wait until it
finishes. Also an ambiguity is possible when matching third-level menu
items, because different dashboards have panels with the same name (for
example Volumes/Images/Instances both at Project and Admin dashboards).
To avoid this ambiguity, an argument `src_elem` is used. Whenever
dashboard or a panel group is clicked, its wrapper is returned to be
used as `src_elem` in a subsequent call for clicking third-level item.
This way the set of panel labels being matched is restricted to the
descendants of that particular dashboard or panel group.
"""
is_already_within_required_item = False
selected_item = None
self._wait_until_modal_dialog_close()
if get_selected_func is not None:
selected_item = get_selected_func()
if selected_item:
if text != selected_item.text and selected_item.text:
# In case different item was chosen previously, collapse
# it. Otherwise selenium will complain with
# MoveTargetOutOfBoundsException
selected_item.click()
time.sleep(1)
self._wait_until_transition_ends(
self._get_menu_list_next_to_menu_title(selected_item))
else:
is_already_within_required_item = True
# In case menu item is collapsed, then below code detects the same
# and disable is_already_within_required_item flag.
# For instance, in case of tacker-horizon, selenium report an
# exception ElementNotInteractableException while opening pages
# under 'NFV Orchestration' panel group. This error occurs when
# element is not clickable or it is not visible yet.
# The panel group 'NFV Orchestration' is never clicked/open hence
# requested pages are not visible/clickable.
item = self._get_item(text, loc_craft_func, src_elem)
if "collapsed" == item.get_attribute(
'class') and is_already_within_required_item is True:
is_already_within_required_item = False
if not is_already_within_required_item:
item.click()
if get_selected_func is not None:
self._wait_until_transition_ends(
self._get_menu_list_next_to_menu_title(item),
to_be_expanded=True)
return item
return selected_item
def _get_item(self, text, loc_craft_func, src_elem=None):
item_locator = loc_craft_func(text)
src_elem = src_elem or self.src_elem
return src_elem.find_element(*item_locator)
def _get_menu_list_next_to_menu_title(self, title_item):
parent_item = title_item.find_element(*self._parent_item_locator)
return parent_item.find_element(*self._menu_list_locator)
def click_on_menu_items(self, first_level=None,
second_level=None,
third_level=None):
src_elem = None
if first_level:
src_elem = self._click_menu_item(
first_level,
self._get_first_level_item_locator,
self.get_first_level_selected_item)
if second_level:
src_elem = self._click_menu_item(
second_level,
self._get_second_level_item_locator,
self.get_second_level_selected_item)
if third_level:
# NOTE(tsufiev): possible dashboard/panel group label passed as
# `src_elem` is a sibling of <ul> with all the panels it contains.
# So to get the panel within specified dashboard/panel group, we
# need to traverse upwards first. When `src_elem` is not specified
# (true for Settings pseudo-dashboard), we cannot and should not
# go upward.
if src_elem:
src_elem = src_elem.find_element(*self._parent_item_locator)
self._click_menu_item(third_level,
self._get_third_level_item_locator,
src_elem=src_elem)
class DropDownMenuRegion(baseregion.BaseRegion):
"""Drop down menu region."""
_menu_container_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu')
_menu_items_locator = (by.By.CSS_SELECTOR,
'ul.dropdown-menu > li > *')
_dropdown_locator = (by.By.CSS_SELECTOR, '.dropdown > a')
_active_cls = 'dropdown-toggle'
@property
def menu_items(self):
self.open()
menu_items = self._get_elements(*self._menu_items_locator)
return menu_items
def is_open(self):
"""Returns True if drop down menu is open, otherwise False."""
return "open" in self.src_elem.get_attribute('class')
def open(self):
"""Opens menu by clicking on the first child of the source element."""
if self.is_open() is False:
dropdown = self._get_element(*self._dropdown_locator)
# NOTE(tsufiev): there is an issue with clicking dropdowns too fast
# after page has been loaded - the Bootstrap constructors haven't
# completed yet, so the dropdown never opens in that case. Avoid
# this by waiting for a specific class to appear, which is set in
# horizon.selenium.js for dropdowns after a timeout passes
def predicate(d):
classes = dropdown.get_attribute('class').split()
return self._active_cls in classes
self._wait_until(predicate)
dropdown.click()
self._wait_till_element_visible(self._menu_container_locator)
class UserDropDownMenuRegion(DropDownMenuRegion):
"""Drop down menu located in the right side of the topbar.
This menu contains links to settings and help.
"""
_settings_link_locator = (by.By.CSS_SELECTOR,
'a[href*="/settings/"]')
_help_link_locator = (by.By.CSS_SELECTOR,
'ul#editor_list li:nth-of-type(2) > a')
_logout_link_locator = (by.By.CSS_SELECTOR,
'a[href*="/auth/logout/"]')
def _theme_picker_locator(self, theme_name):
return (by.By.CSS_SELECTOR,
'.theme-picker-item[data-theme="%s"]' % theme_name)
@property
def settings_link(self):
return self._get_element(*self._settings_link_locator)
@property
def help_link(self):
return self._get_element(*self._help_link_locator)
@property
def logout_link(self):
return self._get_element(*self._logout_link_locator)
def theme_picker_link(self, theme_name):
return self._get_element(*self._theme_picker_locator(theme_name))
def click_on_settings(self):
self.open()
self.settings_link.click()
def click_on_help(self):
self.open()
self.help_link.click()
def choose_theme(self, theme_name):
self.open()
self.theme_picker_link(theme_name).click()
def click_on_logout(self):
self.open()
self.logout_link.click()
class TabbedMenuRegion(baseregion.BaseRegion):
_tab_locator = (by.By.CSS_SELECTOR, 'li > a')
_default_src_locator = (by.By.CSS_SELECTOR, 'div > .nav.nav-pills')
def switch_to(self, index=0):
self._get_elements(*self._tab_locator)[index].click()
class WizardMenuRegion(baseregion.BaseRegion):
_step_locator = (by.By.CSS_SELECTOR, 'li > a')
_default_src_locator = (by.By.CSS_SELECTOR, 'div > .nav.nav-pills')
def switch_to(self, index=0):
self._get_elements(*self._step_locator)[index].click()
class ProjectDropDownRegion(DropDownMenuRegion):
_menu_items_locator = (
by.By.CSS_SELECTOR, 'ul.context-selection li > a')
def click_on_project(self, name):
for item in self.menu_items:
if item.text == name:
item.click()
break
else:
raise exceptions.NoSuchElementException(
"Not found element with text: %s" % name)
class MembershipMenuRegion(baseregion.BaseRegion):
_available_members_locator = (
by.By.CSS_SELECTOR, 'ul.available_members > ul.btn-group')
_allocated_members_locator = (
by.By.CSS_SELECTOR, 'ul.members > ul.btn-group')
_add_remove_member_sublocator = (
by.By.CSS_SELECTOR, 'li > a[href="#add_remove"]')
_member_name_sublocator = (
by.By.CSS_SELECTOR, 'li.member > span.display_name')
_member_roles_widget_sublocator = (by.By.CSS_SELECTOR, 'li.role_options')
_member_roles_widget_open_subsublocator = (by.By.CSS_SELECTOR, 'a.btn')
_member_roles_widget_roles_subsublocator = (
by.By.CSS_SELECTOR, 'ul.role_dropdown > li')
def _get_member_name(self, element):
return element.find_element(*self._member_name_sublocator).text
@property
def available_members(self):
return {self._get_member_name(el): el for el in
self._get_elements(*self._available_members_locator)}
@property
def allocated_members(self):
return {self._get_member_name(el): el for el in
self._get_elements(*self._allocated_members_locator)}
def allocate_member(self, name, available_members=None):
# NOTE(tsufiev): available_members here (and allocated_members below)
# are meant to be used for performance optimization to reduce the
# amount of calls to selenium by reusing still valid element reference
if available_members is None:
available_members = self.available_members
available_members[name].find_element(
*self._add_remove_member_sublocator).click()
def deallocate_member(self, name, allocated_members=None):
if allocated_members is None:
allocated_members = self.allocated_members
allocated_members[name].find_element(
*self._add_remove_member_sublocator).click()
def _get_member_roles_widget(self, name, allocated_members=None):
if allocated_members is None:
allocated_members = self.allocated_members
return allocated_members[name].find_element(
*self._member_roles_widget_sublocator)
def _get_member_all_roles(self, name, allocated_members=None):
roles_widget = self._get_member_roles_widget(name, allocated_members)
return roles_widget.find_elements(
*self._member_roles_widget_roles_subsublocator)
@staticmethod
def _is_role_selected(role):
return 'selected' == role.get_attribute('class')
def get_member_available_roles(self, name, allocated_members=None,
strip=True):
roles = self._get_member_all_roles(name, allocated_members)
return [(role.text.strip() if strip else role)
for role in roles if not self._is_role_selected(role)]
def get_member_allocated_roles(self, name, allocated_members=None,
strip=True):
self.open_member_roles_dropdown(name, allocated_members)
roles = self._get_member_all_roles(name, allocated_members)
return [(role.text.strip() if strip else role)
for role in roles if self._is_role_selected(role)]
def open_member_roles_dropdown(self, name, allocated_members=None):
widget = self._get_member_roles_widget(name, allocated_members)
button = widget.find_element(
*self._member_roles_widget_open_subsublocator)
button.click()
def _switch_member_roles(self, name, roles2toggle, method,
allocated_members=None):
self.open_member_roles_dropdown(name, allocated_members)
roles = method(name, allocated_members, False)
roles2toggle = set(roles2toggle)
for role in roles:
role_name = role.text.strip()
if role_name in roles2toggle:
role.click()
roles2toggle.remove(role_name)
if not roles2toggle:
break
def allocate_member_roles(self, name, roles2add, allocated_members=None):
self._switch_member_roles(
name, roles2add, self.get_member_available_roles,
allocated_members=allocated_members)
def deallocate_member_roles(self, name, roles2remove,
allocated_members=None):
self._switch_member_roles(
name, roles2remove, self.get_member_allocated_roles,
allocated_members=allocated_members)
class InstanceAvailableResourceMenuRegion(baseregion.BaseRegion):
_available_table_locator = (
by.By.CSS_SELECTOR,
'div.step:not(.ng-hide) div.transfer-available table')
_available_table_row_locator = (by.By.CSS_SELECTOR,
"tbody > tr.ng-scope:not(.detail-row)")
_available_table_column_locator = (by.By.TAG_NAME, "td")
_action_column_btn_locator = (by.By.CSS_SELECTOR,
"td.actions_column button")
def transfer_available_resource(self, resource_name):
available_table = self._get_element(*self._available_table_locator)
rows = available_table.find_elements(
*self._available_table_row_locator)
for row in rows:
cols = row.find_elements(*self._available_table_column_locator)
if len(cols) > 1 and self._get_column_text(cols) in resource_name:
row_selector_btn = row.find_element(
*self._action_column_btn_locator)
row_selector_btn.click()
break
def _get_column_text(self, cols):
return cols[2].text.strip()
class InstanceFlavorMenuRegion(InstanceAvailableResourceMenuRegion):
_action_column_btn_locator = (by.By.CSS_SELECTOR, "td.action-col button")
def _get_column_text(self, cols):
return cols[1].text.strip()