-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
5634152
commit bace1f5
Showing
13 changed files
with
444 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
331 changes: 331 additions & 0 deletions
331
openstack_dashboard/test/integration_tests/pages/navigation.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.