Add regions module to integration tests

Regions module should contains basic functionality that should be
shared among multiple page objects: common tables, forms,
notifications etc.. Basically every piece of code that can be reused by
multiple classes in a form of a new object should be placed in here.

* I would suggest that already created regions should be moved into this
  module.

* I would consider region everything that can be reused by at least
  two page objects and aim for maximal code reusability in the future.

* Every new region should inherit from class BaseRegion located in
  baseregion.py.

Partially implements blueprint: selenium-integration-testing

Change-Id: Ib8c20d8833e0830c85030633091663d33de6e3ab
This commit is contained in:
Tomas Novacik 2014-06-24 15:05:27 +02:00 committed by Tomáš Nováčik
parent eab348a7a3
commit 6107972e53
9 changed files with 508 additions and 48 deletions

View File

@ -22,14 +22,14 @@ class BaseWebObject(object):
def _is_element_present(self, *locator):
try:
self.driver.find_element(*locator)
self._get_element(*locator)
return True
except Exceptions.NoSuchElementException:
return False
def _is_element_visible(self, *locator):
try:
return self.driver.find_element(*locator).is_displayed()
return self._get_element(*locator).is_displayed()
except (Exceptions.NoSuchElementException,
Exceptions.ElementNotVisibleException):
return False
@ -37,6 +37,9 @@ class BaseWebObject(object):
def _get_element(self, *locator):
return self.driver.find_element(*locator)
def _get_elements(self, *locator):
return self.driver.find_elements(*locator)
def _fill_field_element(self, data, field_element):
field_element.clear()
field_element.send_keys(data)

View File

@ -14,12 +14,15 @@ from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests import basewebobject
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 messages
class BasePage(pageobject.PageObject):
"""Base class for all dashboard page objects."""
_heading_locator = (by.By.CSS_SELECTOR, "div.page-header > h2")
_heading_locator = (by.By.CSS_SELECTOR, 'div.page-header > h2')
_error_msg_locator = (by.By.CSS_SELECTOR, 'div.alert-danger.alert')
@property
def heading(self):
@ -27,7 +30,7 @@ class BasePage(pageobject.PageObject):
@property
def topbar(self):
return BasePage.TopBarRegion(self.driver, self.conf)
return bars.TopBarRegion(self.driver, self.conf)
@property
def is_logged_in(self):
@ -37,6 +40,13 @@ class BasePage(pageobject.PageObject):
def navaccordion(self):
return BasePage.NavigationAccordionRegion(self.driver, self.conf)
def error_message(self):
src_elem = self._get_element(*self._error_msg_locator)
return messages.ErrorMessageRegion(self.driver, self.conf, src_elem)
def is_error_message_present(self):
return self._is_element_present(*self._error_msg_locator)
def go_to_login_page(self):
self.driver.get(self.login_url)
@ -52,50 +62,6 @@ class BasePage(pageobject.PageObject):
self.topbar.user_dropdown_menu.click()
self.topbar.help_link.click()
class TopBarRegion(basewebobject.BaseWebObject):
_user_dropdown_menu_locator = (by.By.CSS_SELECTOR,
'div#profile_editor_switcher'
' > button')
_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/"]')
_openstack_brand_locator = (by.By.CSS_SELECTOR, 'a[href*="/home/"]')
@property
def user(self):
return self._get_element(*self._user_dropdown_menu_locator)
@property
def brand(self):
return self._get_element(*self._openstack_brand_locator)
@property
def logout_link(self):
return self._get_element(*self._logout_link_locator)
@property
def user_dropdown_menu(self):
return self._get_element(*self._user_dropdown_menu_locator)
@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 is_logout_visible(self):
return self._is_element_visible(*self._logout_link_locator)
@property
def is_logged_in(self):
return self._is_element_visible(*self._user_dropdown_menu_locator)
class NavigationAccordionRegion(basewebobject.BaseWebObject):
# TODO(sunlim): change Xpath to CSS
_project_bar_locator = (

View File

@ -9,13 +9,22 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import accesssecuritypage
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages import settingspage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class ProjectPage(basepage.BasePage):
_usage_table_locator = (by.By.CSS_SELECTOR, 'table#project_usage')
_date_form_locator = (by.By.CSS_SELECTOR, 'form#date_form')
USAGE_TABLE_ACTIONS = ("download_csv",)
def __init__(self, driver, conf):
super(ProjectPage, self).__init__(driver, conf)
self._page_title = 'Instance Overview'
@ -33,3 +42,14 @@ class ProjectPage(basepage.BasePage):
self.navaccordion.access_security.click()
return accesssecuritypage.AccessSecurityPage(
self.driver, self.conf)
@property
def usage_table(self):
src_elem = self._get_element(*self._usage_table_locator)
return tables.ActionsTableRegion(self.driver, self.conf, src_elem,
self.USAGE_TABLE_ACTIONS)
@property
def date_form(self):
src_elem = self._get_element(*self._date_form_locator)
return forms.DateFormRegion(self.driver, self.conf, src_elem)

View File

@ -0,0 +1,60 @@
# 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.
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class TopBarRegion(baseregion.BaseRegion):
_user_dropdown_menu_locator = (by.By.CSS_SELECTOR,
'div#profile_editor_switcher'
' > button')
_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/"]')
_openstack_brand_locator = (by.By.CSS_SELECTOR, 'a[href*="/home/"]')
@property
def user(self):
return self._get_element(*self._user_dropdown_menu_locator)
@property
def brand(self):
return self._get_element(*self._openstack_brand_locator)
@property
def logout_link(self):
return self._get_element(*self._logout_link_locator)
@property
def user_dropdown_menu(self):
return self._get_element(*self._user_dropdown_menu_locator)
@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 is_logout_visible(self):
return self._is_element_visible(*self._logout_link_locator)
@property
def is_logged_in(self):
return self._is_element_visible(*self._user_dropdown_menu_locator)

View File

@ -0,0 +1,94 @@
# 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 types
from openstack_dashboard.test.integration_tests import basewebobject
class BaseRegion(basewebobject.BaseWebObject):
"""Base class for region module
* there is necessity to override some basic methods for obtaining elements
as in content of regions it is required to do relative searches
* self.driver cannot be easily replaced with self.src_elem because that
would result in functionality loss, self.driver is WebDriver and
src_elem is WebElement its usage is different.
* this does not mean that self.src_elem cannot be self.driver
"""
# private methods
def __init__(self, driver, conf, src_elem=None):
super(BaseRegion, self).__init__(driver, conf)
self.src_elem = src_elem or driver
# variable for storing names of dynamic properties and
# associated 'getters' - meaning method that are supplying
# regions or web elements
self._dynamic_properties = {}
def __getattr__(self, name):
"""It is not possible to create property bounded just to object
and not class at runtime, therefore it is necessary to
override __getattr__ and make fake 'properties' by storing them in
the protected attribute _dynamic_attributes and returning result
of the method associated with the specified attribute.
This way the feeling of having regions accessed as 'properties'
is created, which is one of the requirement of page object pattern.
"""
try:
return self._dynamic_properties[name]()
except KeyError:
msg = "'{0}' object has no attribute '{1}'"
raise AttributeError(msg.format(type(self).__name__, name))
# protected methods and classes
class _DynamicProperty(object):
"""Serves as new property holder."""
def __init__(self, method, index=None):
"""In case object was created with index != None,
it is assumed that the result of self.method should be tuple()
and just certain index should be returned
"""
self.method = method
self.index = index
def __call__(self, *args, **kwargs):
result = self.method()
return result if self.index is None else result[self.index]
def _init_dynamic_properties(self, new_attr_names, method):
"""Create new object's 'properties' at runtime."""
for index, new_attr_name in enumerate(new_attr_names):
self._init_dynamic_property(new_attr_name, method, index)
def _init_dynamic_property(self, new_attr_name, method, index=None):
"""Create new object's property at runtime. If index argument is
supplied it is assumed that method returns tuple() and only element
on ${index} position is returned.
"""
if (new_attr_name in dir(self) or
new_attr_name in self._dynamic_properties):
raise AttributeError("%s class has already attribute %s."
"The new property could not be "
"created." % (self.__class__.__name__,
new_attr_name))
new_method = self.__class__._DynamicProperty(method, index)
inst_method = types.MethodType(new_method, self)
self._dynamic_properties[new_attr_name] = inst_method
def _get_element(self, *locator):
return self.src_elem.find_element(*locator)
def _get_elements(self, *locator):
return self.src_elem.find_elements(*locator)

View File

@ -0,0 +1,73 @@
# 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.
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class BaseFormFieldRegion(baseregion.BaseRegion):
"""Base class for form fields classes."""
_label_locator = None
_element_locator = None
@property
def label(self):
return self._get_element(*self._label_locator)
@property
def element(self):
return self._get_element(*self._element_locator)
def is_required(self):
classes = self.driver.get_attribute('class')
return 'required' in classes
class BaseFormRegion(baseregion.BaseRegion):
"""Base class for forms."""
_submit_locator = (by.By.CSS_SELECTOR, 'button.btn.btn-primary,'
' a.btn.btn-primary')
def submit(self):
self._get_element(*self._submit_locator).click()
class DateFormRegion(BaseFormRegion):
"""Form that queries data to table that is regularly below the form,
typical example is located on Project/Compute/Overview page.
"""
_from_field_locator = (by.By.CSS_SELECTOR, 'input#id_start')
_to_field_locator = (by.By.CSS_SELECTOR, 'input#id_end')
@property
def from_date(self):
return self._get_element(*self._from_field_locator)
@property
def to_date(self):
return self._get_element(*self._to_field_locator)
def query(self, start, end):
self._set_from_field(start)
self._set_to_field(end)
self.submit()
def _set_from_field(self, value):
self._fill_field_element(value, self.from_date)
def _set_to_field(self, value):
self._fill_field_element(value, self.to_date)

View File

@ -0,0 +1,23 @@
# 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.
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class ErrorMessageRegion(baseregion.BaseRegion):
_close_locator = (by.By.CSS_SELECTOR, 'a.close')
def close(self):
self._get_element(*self._close_locator).click()

View File

@ -0,0 +1,221 @@
# 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.
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class RowRegion(baseregion.BaseRegion):
"""Classic table row."""
_cell_locator = (by.By.CSS_SELECTOR, 'td.normal_column')
@property
def cells(self):
return self._get_elements(*self._cell_locator)
class BaseActionRowRegion(RowRegion):
"""Base class for creating ActionRow class derivative."""
_row_checkbox_locator = (by.By.CSS_SELECTOR, 'td > input')
def mark(self):
chck_box = self._get_element(*self._row_checkbox_locator)
chck_box.click()
class BtnActionRowRegion(BaseActionRowRegion):
"""Row with buttons in action column."""
_action_locator = (by.By.CSS_SELECTOR, 'td.actions_column > button')
def __init__(self, driver, conf, src_elem, action_name):
super(BtnActionRowRegion, self).__init__(driver, conf, src_elem)
self.action_name = action_name
self._init_action()
def _init_action(self):
self._init_dynamic_property(self.action_name, self._get_action)
def _get_action(self):
return self._get_element(*self._action_locator)
@property
def action(self):
return self._get_action()
class ComplexActionRowRegion(BaseActionRowRegion):
"""Row with button and select box in action column."""
_primary_action_locator = (by.By.CSS_SELECTOR,
'td.actions_column > div.btn-group > *.btn')
_secondary_actions_dropdown_locator = (by.By.CSS_SELECTOR,
'div.btn-group')
PRIMARY_ACTION = "primary_action"
SECONDARY_ACTIONS = "secondary_actions"
ACTIONS_ERROR_MSG = ("Actions must be supplied in dictionary:"
" {%s: 'action_name', '%s': ('action_name',...)}"
% (PRIMARY_ACTION, SECONDARY_ACTIONS))
def __init__(self, driver, conf, src_elem, action_names):
super(ComplexActionRowRegion, self).__init__(driver, conf, src_elem)
try:
self.primary_action_name = action_names[self.PRIMARY_ACTION]
self.secondary_action_names = action_names[self.SECONDARY_ACTIONS]
self._init_actions()
except (TypeError, KeyError):
raise AttributeError(self.ACTIONS_ERROR_MSG)
def _init_actions(self):
self._init_dynamic_property(self.primary_action_name,
self._get_primary_action)
self._init_dynamic_properties(self.secondary_action_names,
self._get_secondary_actions)
def _get_primary_action(self):
return self._get_element(*self._primary_action_locator)
def _get_secondary_actions(self):
return self._get_elements(*self._secondary_actions_dropdown_locator)
@property
def primary_action(self):
return self._get_primary_action()
@property
def secondary_actions(self):
return self._get_secondary_actions()
class BasicTableRegion(baseregion.BaseRegion):
"""Basic class representing table object."""
_heading_locator = (by.By.CSS_SELECTOR, 'h3.table_title')
_columns_names_locator = (by.By.CSS_SELECTOR, 'thead > tr > th')
_footer_locator = (by.By.CSS_SELECTOR, 'tfoot > tr > td > span')
_rows_locator = (by.By.CSS_SELECTOR, 'tbody > tr')
_empty_table_locator = (by.By.CSS_SELECTOR, 'tbody > tr.empty')
_search_field_locator = (by.By.CSS_SELECTOR,
'div.table_search.client > input')
_search_button_locator = (by.By.CSS_SELECTOR,
'div.table_search.client > button')
@property
def heading(self):
return self._get_element(*self._heading_locator)
@property
def rows(self):
if self._is_element_present(*self._empty_table_locator):
return []
else:
return self._get_rows()
@property
def column_names(self):
return self._get_elements(*self._columns_names_locator)
@property
def footer(self):
return self._get_element(*self._footer_locator)
def filter(self, value):
self._set_search_field(value)
self._click_search_btn()
def get_row(self, column_index, text):
"""Get row that contains in specified column specified text."""
for row in self.rows:
if text in row.cells[column_index].text:
return row
return None
def _set_search_field(self, value):
srch_field = self._get_element(*self._search_field_locator)
srch_field.send_keys(value)
def _click_search_btn(self):
btn = self._get_element(*self._search_button_locator)
btn.click()
def _get_rows(self):
rows = []
for elem in self._get_elements(*self._rows_locator):
rows.append(RowRegion(self.driver, self.conf, elem))
class ActionsTableRegion(BasicTableRegion):
"""Base class for creating derivative of BasicTableRegion that
has some actions.
"""
_actions_locator = (by.By.CSS_SELECTOR, 'div.table_actions > button,'
' div.table_actions > a')
# private methods
def __init__(self, driver, conf, src_elm, action_names):
super(ActionsTableRegion, self).__init__(driver, conf, src_elm)
self.action_names = action_names
self._init_actions()
# protected methods
def _init_actions(self):
"""Create new methods that corresponds to picking table's
action buttons.
"""
self._init_dynamic_properties(self.action_names, self._get_actions)
def _get_actions(self):
return self._get_elements(*self._actions_locator)
# properties
@property
def actions(self):
return self._get_actions()
class SimpleActionsTableRegion(ActionsTableRegion):
"""Table which rows has buttons in action column."""
def __init__(self, driver, conf, src_elm, action_names, row_action_name):
super(SimpleActionsTableRegion, self).__init__(driver, conf, src_elm,
action_names)
self.row_action_name = row_action_name
def _get_rows(self):
rows = []
for elem in self._get_elements(*self._rows_locator):
rows.append(BtnActionRowRegion(self.driver, self.conf, elem,
self.row_action_name))
return rows
class ComplexActionTableRegion(ActionsTableRegion):
"""Table which has button and selectbox in the action column."""
def __init__(self, driver, conf, src_elm, action_names, row_action_names):
super(ComplexActionTableRegion, self).__init__(driver, conf, src_elm,
action_names)
self.row_action_names = row_action_names
def _get_rows(self):
rows = []
for elem in self._get_elements(*self._rows_locator):
rows.append(ComplexActionRowRegion(self.driver, self.conf, elem,
self.row_action_names))
return rows