Skip to content

Commit

Permalink
Add navigation among pages
Browse files Browse the repository at this point in the history
Add navigation module that provides navigation across pages.
Modify existing test cases so they can benefit from the navigation module.

* Navigation class - navigation mixin that adds navigation
  functionality to PageObject class

* BaseNavigationPage - base class for pages that want to use
  navigation

Partially implements blueprint: selenium-integration-testing

Change-Id: I7a2145a599fe22613a6d5bfd6feaabd116ec0279
  • Loading branch information
tnovacik-redhat authored and dkorn committed Dec 21, 2014
1 parent 5634152 commit bace1f5
Show file tree
Hide file tree
Showing 13 changed files with 444 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from openstack_dashboard.test.integration_tests.pages import basepage


class OverviewPage(basepage.BasePage):
class OverviewPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super(OverviewPage, self).__init__(driver, conf)
self._page_title = "Usage Overview"
8 changes: 7 additions & 1 deletion openstack_dashboard/test/integration_tests/pages/basepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from selenium.webdriver.common import by

from openstack_dashboard.test.integration_tests.pages import navigation
from openstack_dashboard.test.integration_tests.pages import pageobject
from openstack_dashboard.test.integration_tests.regions import bars
from openstack_dashboard.test.integration_tests.regions import menus
Expand Down Expand Up @@ -40,6 +41,7 @@ def is_logged_in(self):
def navaccordion(self):
return menus.NavigationAccordionRegion(self.driver, self.conf)

@property
def error_message(self):
src_elem = self._get_element(*self._error_msg_locator)
return messages.ErrorMessageRegion(self.driver, self.conf, src_elem)
Expand All @@ -58,4 +60,8 @@ def log_out(self):
return self.go_to_login_page()

def go_to_help_page(self):
self.topbar.user_dropdown_menu.click_on_help()
self.topbar.user_dropdown_menu.click_on_help()


class BaseNavigationPage(BasePage, navigation.Navigation):
pass
5 changes: 2 additions & 3 deletions openstack_dashboard/test/integration_tests/pages/loginpage.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@


class LoginPage(pageobject.PageObject):

_login_username_field_locator = (by.By.CSS_SELECTOR, '#id_username')
_login_password_field_locator = (by.By.CSS_SELECTOR, '#id_password')
_login_submit_button_locator = (by.By.CSS_SELECTOR,
Expand All @@ -32,8 +31,8 @@ def __init__(self, driver, conf):
self._page_title = "Login"

def is_login_page(self):
return self.is_the_current_page() and \
self._is_element_visible(*self._login_submit_button_locator)
return (self.is_the_current_page() and
self.is_element_visible(*self._login_submit_button_locator))

@property
def username(self):
Expand Down
331 changes: 331 additions & 0 deletions openstack_dashboard/test/integration_tests/pages/navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
# 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 importlib
import types


class Navigation(object):
"""Provide basic navigation among pages.
* allows navigation among pages without need to import pageobjects
* shortest path possible should be always used for navigation to
specific page as user would navigate in the same manner
* go_to_*some_page* method respects following pattern:
* go_to_{sub_menu}_{pagename}page
* go_to_*some_page* methods are generated at runtime at module import
* go_to_*some_page* method returns pageobject
* pages must be located in the correct directories for the navigation
to work correctly
* pages modules and class names must respect following pattern
to work correctly with navigation module:
* module name consist of menu item in lower case without spaces and '&'
* page class begins with capital letter and ends with 'Page'
Examples:
In order to go to Project/Compute/Overview page, one would have to call
method go_to_compute_overviewpage()
In order to go to Admin/System/Overview page, one would have to call
method go_to_system_overviewpage()
"""
# constants
MAX_SUB_LEVEL = 4
MIN_SUB_LEVEL = 2
SIDE_MENU_MAX_LEVEL = 3
PAGES_IMPORT_PATH = "openstack_dashboard.test.integration_tests.pages.%s"
ITEMS = "__items__"

PAGE_STRUCTURE = \
{
"Project":
{
"Compute":
{
"Access & Security":
{
ITEMS:
(
"Security Groups",
"Key Pairs",
"Floating IPs",
"API Access"
),
},
"Volumes":
{
ITEMS:
(
"Volumes",
"Volume Snapshots"
)
},
ITEMS:
(
"Overview",
"Instances",
"Images",
)
},
"Network":
{
ITEMS:
(
"Network Topology",
"Networks",
"Routers"
)
},
"Object Store":
{
ITEMS:
(
"Containers",
)
},
"Data Processing":
{
ITEMS:
(
"Clusters",
"Cluster Templates",
"Node Group Templates",
"Job Executions",
"Jobs",
"Job Binaries",
"Data Sources",
"Image Registry",
"Plugins"
),
},
"Orchestration":
{
ITEMS:
(
"Stacks",
)
}
},
"Admin":
{
"System":
{
"Resource Usage":
{
ITEMS:
(
"Daily Report",
"Stats"
)
},
"System info":
{
ITEMS:
(
"Services",
"Compute Services",
"Block Storage Services",
"Network Agents",
"Default Quotas"
)
},
"Volumes":
{
ITEMS:
(
"Volumes",
"Volume Types",
"Volume Snapshots"
)
},
ITEMS:
(
"Overview",
"Hypervisors",
"Host Aggregates",
"Instances",
"Flavors",
"Images",
"Networks",
"Routers"
)
},
},
"Settings":
{
ITEMS:
(
"User Settings",
"Change Password"
)
},
"Identity":
{
ITEMS:
(
"Projects",
"Users",
"Groups",
"Domains",
"Roles"
)
}
}

# protected methods
def _go_to_page(self, path, page_class=None):
"""Go to page specified via path parameter.
* page_class parameter overrides basic process for receiving
pageobject
"""
path_len = len(path)
if path_len < self.MIN_SUB_LEVEL or path_len > self.MAX_SUB_LEVEL:
raise ValueError("Navigation path length should be in the interval"
" between %s and %s, but its length is %s" %
(self.MIN_SUB_LEVEL, self.MAX_SUB_LEVEL,
path_len))

if path_len == self.MIN_SUB_LEVEL:
# menu items that do not contain second layer of menu
if path[0] == "Settings":
self._go_to_settings_page(path[1])
else:
self._go_to_side_menu_page([path[0], None, path[1]])
else:
# side menu contains only three sub-levels
self._go_to_side_menu_page(path[:self.SIDE_MENU_MAX_LEVEL])

if path_len == self.MAX_SUB_LEVEL:
# apparently there is tabbed menu,
# because another extra sub level is present
self._go_to_tab_menu_page(path[self.MAX_SUB_LEVEL - 1])

# if there is some nonstandard pattern in page object naming
return self._get_page_class(path, page_class)(self.driver, self.conf)

def _go_to_tab_menu_page(self, item_text):
self.driver.find_element_by_link_text(item_text).click()

def _go_to_settings_page(self, item_text):
"""Go to page that is located under the settings tab."""
self.topbar.user_dropdown_menu.click_on_settings()
self.navaccordion.click_on_menu_items(third_level=item_text)

def _go_to_side_menu_page(self, menu_items):
"""Go to page that is located in the side menu (navaccordion)."""
self.navaccordion.click_on_menu_items(*menu_items)

def _get_page_cls_name(self, filename):
"""Gather page class name from path.
* take last item from path (should be python filename
without extension)
* make the first letter capital
* append 'Page'
"""
cls_name = "".join((filename.capitalize(), "Page"))
return cls_name

def _get_page_class(self, path, page_cls_name):

# last module name does not contain '_'
final_module = self.unify_page_path(path[-1],
preserve_spaces=False)
page_cls_path = ".".join(path[:-1] + (final_module,))
page_cls_path = self.unify_page_path(page_cls_path)
# append 'page' as every page module ends with this keyword
page_cls_path += "page"

page_cls_name = page_cls_name or self._get_page_cls_name(final_module)

# return imported class
module = importlib.import_module(self.PAGES_IMPORT_PATH %
page_cls_path)
return getattr(module, page_cls_name)

class GoToMethodFactory(object):
"""Represent the go_to_some_page method."""

METHOD_NAME_PREFIX = "go_to_"
METHOD_NAME_SUFFIX = "page"
METHOD_NAME_DELIMITER = "_"

# private methods
def __init__(self, path, page_class=None):
self.path = path
self.page_class = page_class
self._name = self._create_name()

def __call__(self, *args, **kwargs):
return Navigation._go_to_page(args[0], self.path, self.page_class)

# protected methods
def _create_name(self):
"""Create method name.
* consist of 'go_to_subsubmenu_menuitem_page'
"""
submenu, menu_item = self.path[-2:]

name = "".join((self.METHOD_NAME_PREFIX, submenu,
self.METHOD_NAME_DELIMITER, menu_item,
self.METHOD_NAME_SUFFIX))
name = Navigation.unify_page_path(name, preserve_spaces=False)
return name

# properties
@property
def name(self):
return self._name

# classmethods
@classmethod
def _initialize_go_to_methods(cls):
"""Create all navigation methods based on the PAGE_STRUCTURE."""

def rec(items, sub_menus):
if isinstance(items, dict):
for sub_menu, sub_item in items.iteritems():
rec(sub_item, sub_menus + (sub_menu,))
elif isinstance(items, tuple):
# exclude ITEMS element from sub_menus
paths = (sub_menus[:-1] + (menu_item,) for menu_item in items)
for path in paths:
cls._create_go_to_method(path)

rec(cls.PAGE_STRUCTURE, ())

@classmethod
def _create_go_to_method(cls, path, class_name=None):
go_to_method = Navigation.GoToMethodFactory(path, class_name)
inst_method = types.MethodType(go_to_method, None, Navigation)
setattr(Navigation, inst_method.name, inst_method)

@classmethod
def unify_page_path(cls, path, preserve_spaces=True):
"""Unify path to page.
Replace '&' in path with 'and', remove spaces (if not specified
otherwise) and convert path to lower case.
"""
path = path.replace("&", "and")
path = path.lower()
if preserve_spaces:
path = path.replace(" ", "_")
else:
path = path.replace(" ", "")
return path


Navigation._initialize_go_to_methods()
Loading

0 comments on commit bace1f5

Please sign in to comment.