Do not match table actions by ordering in integration tests

Instead of this match them by action <a> tag's id, which is composed:
* from table name, fixed part and action name for table-level actions and
* from row id, fixed part and action name for row-level actions.

Doing so may make action names in page objects less readable as they
have to be the same as real object name (which are rather terse).

Implements blueprint: integration-tests-hardening
Change-Id: I3f92ef4cfd098d080199350cbf5e6061aa050907
This commit is contained in:
Timur Sufiev 2015-08-28 17:35:43 +03:00
parent f6436bbefd
commit d25d4d2b0d
10 changed files with 88 additions and 51 deletions

View File

@ -23,7 +23,8 @@ class FlavorsPage(basepage.BaseNavigationPage):
_flavors_table_locator = (by.By.ID, 'flavors')
FLAVORS_TABLE_ACTIONS = ("create_flavor", "delete_flavors")
FLAVORS_TABLE_NAME = "flavors"
FLAVORS_TABLE_ACTIONS = ("create", "delete")
FLAVORS_TABLE_ROW_ACTIONS = {
tables.ComplexActionRowRegion.PRIMARY_ACTION: "edit_flavor",
tables.ComplexActionRowRegion.SECONDARY_ACTIONS: (
@ -47,6 +48,7 @@ class FlavorsPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._flavors_table_locator)
return tables.ComplexActionTableRegion(self.driver,
self.conf, src_elem,
self.FLAVORS_TABLE_NAME,
self.FLAVORS_TABLE_ACTIONS,
self.FLAVORS_TABLE_ROW_ACTIONS)
@ -61,7 +63,7 @@ class FlavorsPage(basepage.BaseNavigationPage):
def create_flavor(self, name, id_=DEFAULT_ID, vcpus=None, ram=None,
root_disk=None, ephemeral_disk=None, swap_disk=None):
self.flavors_table.create_flavor.click()
self.flavors_table.create.click()
self.create_flavor_form.name.text = name
if id_ is not None:
self.create_flavor_form.flavor_id.text = id_
@ -76,7 +78,7 @@ class FlavorsPage(basepage.BaseNavigationPage):
def delete_flavor(self, name):
row = self._get_row_with_flavor_name(name)
row.mark()
self.flavors_table.delete_flavors.click()
self.flavors_table.delete.click()
self.confirm_delete_flavors_form.submit.click()
self.wait_till_popups_disappear()

View File

@ -35,7 +35,8 @@ class ProjectsPage(basepage.BaseNavigationPage):
DEFAULT_ENABLED = True
PROJECTS_TABLE_NAME_COLUMN_INDEX = 0
PROJECTS_TABLE_ACTIONS = ("create_project", "delete_projects")
PROJECTS_TABLE_NAME = "tenants"
PROJECTS_TABLE_ACTIONS = ("create", "delete")
PROJECTS_TABLE_ROW_ACTIONS = {
tables.ComplexActionRowRegion.PRIMARY_ACTION: "manage_members",
tables.ComplexActionRowRegion.SECONDARY_ACTIONS: (
@ -58,6 +59,7 @@ class ProjectsPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._projects_table_locator)
return tables.ComplexActionTableRegion(self.driver,
self.conf, src_elem,
self.PROJECTS_TABLE_NAME,
self.PROJECTS_TABLE_ACTIONS,
self.PROJECTS_TABLE_ROW_ACTIONS
)
@ -94,7 +96,7 @@ class ProjectsPage(basepage.BaseNavigationPage):
def create_project(self, project_name, description=None,
is_enabled=DEFAULT_ENABLED):
self.projects_table.create_project.click()
self.projects_table.create.click()
self.create_project_form.name.text = project_name
if description is not None:
self.create_project_form.description.text = description
@ -106,7 +108,7 @@ class ProjectsPage(basepage.BaseNavigationPage):
def delete_project(self, project_name):
row = self._get_row_with_project_name(project_name)
row.mark()
self.projects_table.delete_projects.click()
self.projects_table.delete.click()
self.delete_project_submit_button.click()
self.wait_till_popups_disappear()

View File

@ -23,7 +23,8 @@ class UsersPage(basepage.BaseNavigationPage):
USERS_TABLE_NAME_COLUMN_INDEX = 0
USERS_TABLE_ACTIONS = ("create_user", "delete_users")
USERS_TABLE_NAME = "users"
USERS_TABLE_ACTIONS = ("create", "delete")
USERS_TABLE_ROW_ACTIONS = {
tables.ComplexActionRowRegion.PRIMARY_ACTION: "edit_user",
@ -47,6 +48,7 @@ class UsersPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._users_table_locator)
return tables.ComplexActionTableRegion(self.driver,
self.conf, src_elem,
self.USERS_TABLE_NAME,
self.USERS_TABLE_ACTIONS,
self.USERS_TABLE_ROW_ACTIONS
)
@ -62,7 +64,7 @@ class UsersPage(basepage.BaseNavigationPage):
def create_user(self, name, password,
project, role, email=None):
self.users_table.create_user.click()
self.users_table.create.click()
self.create_user_form.name.text = name
if email is not None:
self.create_user_form.email.text = email
@ -76,7 +78,7 @@ class UsersPage(basepage.BaseNavigationPage):
def delete_user(self, name):
row = self._get_row_with_user_name(name)
row.mark()
self.users_table.delete_users.click()
self.users_table.delete.click()
self.confirm_delete_users_form.submit.click()
self.wait_till_popups_disappear()

View File

@ -30,8 +30,8 @@ class FloatingipsPage(basepage.BaseNavigationPage):
_floatingips_fadein_popup_locator = (
by.By.CSS_SELECTOR, '.alert.alert-success.alert-dismissable.fade.in>p')
FLOATING_IPS_TABLE_ACTIONS = ("allocate_ip_to_project",
"release_floating_ips")
FLOATING_IPS_TABLE_NAME = 'floating_ips'
FLOATING_IPS_TABLE_ACTIONS = ("allocate", "release")
FLOATING_IPS_TABLE_ROW_ACTION = {
tables.ComplexActionRowRegion.PRIMARY_ACTION: "associate",
tables.ComplexActionRowRegion.SECONDARY_ACTIONS: (
@ -51,6 +51,7 @@ class FloatingipsPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._floating_ips_table_locator)
return tables.ComplexActionTableRegion(
self.driver, self.conf, src_elem,
self.FLOATING_IPS_TABLE_NAME,
self.FLOATING_IPS_TABLE_ACTIONS,
self.FLOATING_IPS_TABLE_ROW_ACTION)
@ -59,7 +60,7 @@ class FloatingipsPage(basepage.BaseNavigationPage):
return forms.BaseFormRegion(self.driver, self.conf, None)
def allocate_floatingip(self):
self.floatingips_table.allocate_ip_to_project.click()
self.floatingips_table.allocate.click()
self.floatingip_form.submit.click()
ip = re.compile('(([2][5][0-5]\.)|([2][0-4][0-9]\.)'
+ '|([0-1]?[0-9]?[0-9]\.)){3}(([2][5][0-5])|'
@ -73,7 +74,7 @@ class FloatingipsPage(basepage.BaseNavigationPage):
def release_floatingip(self, floatingip):
row = self._get_row_with_floatingip(floatingip)
row.mark()
self.floatingips_table.release_floating_ips.click()
self.floatingips_table.release.click()
self.floatingip_form.submit.click()
self.wait_till_popups_disappear()

View File

@ -24,9 +24,9 @@ class KeypairsPage(basepage.BaseNavigationPage):
_key_pairs_table_locator = (by.By.ID, 'keypairs')
KEY_PAIRS_TABLE_ACTIONS = ("create_key_pair", "import_key_pair",
"delete_key_pair")
KEY_PAIRS_TABLE_ROW_ACTION = "delete_key_pair"
KEY_PAIRS_TABLE_NAME = "keypairs"
KEY_PAIRS_TABLE_ACTIONS = ("create", "import", "delete")
KEY_PAIRS_TABLE_ROW_ACTION = "delete"
KEY_PAIRS_TABLE_NAME_COLUMN_INDEX = 0
CREATE_KEY_PAIR_FORM_FIELDS = ('name',)
@ -44,6 +44,7 @@ class KeypairsPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._key_pairs_table_locator)
return tables.SimpleActionsTableRegion(self.driver, self.conf,
src_elem,
self.KEY_PAIRS_TABLE_NAME,
self.KEY_PAIRS_TABLE_ACTIONS,
self.KEY_PAIRS_TABLE_ROW_ACTION)
@ -60,12 +61,12 @@ class KeypairsPage(basepage.BaseNavigationPage):
return bool(self._get_row_with_keypair_name(name))
def create_keypair(self, keypair_name):
self.keypairs_table.create_key_pair.click()
self.keypairs_table.create.click()
self.create_keypair_form.name.text = keypair_name
self.create_keypair_form.submit.click()
self.wait_till_popups_disappear()
def delete_keypair(self, name):
self._get_row_with_keypair_name(name).delete_key_pair.click()
self._get_row_with_keypair_name(name).delete.click()
self.delete_keypair_form.submit.click()
self.wait_till_popups_disappear()

View File

@ -29,7 +29,8 @@ class ImagesPage(basepage.BaseNavigationPage):
_images_table_locator = (by.By.ID, 'images')
IMAGES_TABLE_ACTIONS = ("create_image", "delete_images")
IMAGES_TABLE_NAME = "images"
IMAGES_TABLE_ACTIONS = ("create", "delete")
IMAGES_TABLE_ROW_ACTIONS = {
tables.ComplexActionRowRegion.PRIMARY_ACTION: "launch",
tables.ComplexActionRowRegion.SECONDARY_ACTIONS: ("create_volume",)
@ -55,6 +56,7 @@ class ImagesPage(basepage.BaseNavigationPage):
src_elem = self._get_element(*self._images_table_locator)
return tables.ComplexActionTableRegion(self.driver,
self.conf, src_elem,
self.IMAGES_TABLE_NAME,
self.IMAGES_TABLE_ACTIONS,
self.IMAGES_TABLE_ROW_ACTIONS
)
@ -74,7 +76,7 @@ class ImagesPage(basepage.BaseNavigationPage):
image_format=DEFAULT_IMAGE_FORMAT,
is_public=DEFAULT_ACCESSIBILITY,
is_protected=DEFAULT_PROTECTION):
self.images_table.create_image.click()
self.images_table.create.click()
self.create_image_form.name.text = name
if description is not None:
self.create_image_form.description.text = description
@ -98,7 +100,7 @@ class ImagesPage(basepage.BaseNavigationPage):
def delete_image(self, name):
row = self._get_row_with_image_name(name)
row.mark()
self.images_table.delete_images.click()
self.images_table.delete.click()
self.confirm_delete_images_form.submit.click()
self.wait_till_popups_disappear()

View File

@ -20,7 +20,7 @@ class OverviewPage(basepage.BaseNavigationPage):
_usage_table_locator = (by.By.ID, 'project_usage')
_date_form_locator = (by.By.ID, 'date_form')
USAGE_TABLE_ACTIONS = ("download_csv",)
USAGE_TABLE_ACTIONS = ("csv_summary",)
def __init__(self, driver, conf):
super(OverviewPage, self).__init__(driver, conf)

View File

@ -65,31 +65,48 @@ class BaseRegion(basewebobject.BaseWebObject):
class _DynamicProperty(object):
"""Serves as new property holder."""
def __init__(self, method, index=None, name=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
def __init__(self, method, name=None, id_pattern=None):
"""Invocation of `method` should return either single property, or
a dictionary of properties, or a list of them.
In case it's single, neither name, nor id_pattern is required.
In case it's a dictionary, it's expected that it has a value for
the key equal to `name` argument. That's a standard way of
fetching a form field).
In case it's a list, the element with an id equal to the result of
`id_pattern % name` is supposed to be there. That's a standard way
of fetching a table action (either table-wise or row-wise).
"""
self.method = method
self.index = index
self.name = name
self.id_pattern = id_pattern
def __call__(self, *args, **kwargs):
result = self.method()
if isinstance(result, dict):
return result if self.name is None else result[self.name]
if self.name is None:
return result
else:
return result if self.index is None else result[self.index]
if isinstance(result, list) and self.id_pattern is not None:
# NOTE(tsufiev): map table actions to action names using
# action tag's ids
actual_id = self.id_pattern % self.name
result = {self.name: entry for entry in result
if entry.get_attribute('id') == actual_id}
if isinstance(result, dict):
return result[self.name]
return result
def _init_dynamic_properties(self, new_attr_names, method):
def _init_dynamic_properties(self, new_attr_names, method,
id_pattern=None):
"""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)
for new_attr_name in new_attr_names:
self._init_dynamic_property(new_attr_name, method, id_pattern)
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.
def _init_dynamic_property(self, new_attr_name, method, id_pattern=None):
"""Create new object's property at runtime. See _DynamicProperty's
__init__ docstring for a description of arguments.
"""
if (new_attr_name in dir(self) or
new_attr_name in self._dynamic_properties):
@ -97,8 +114,8 @@ class BaseRegion(basewebobject.BaseWebObject):
"The new property could not be "
"created." % (self.__class__.__name__,
new_attr_name))
new_method = self.__class__._DynamicProperty(method, index,
new_attr_name)
new_method = self.__class__._DynamicProperty(
method, new_attr_name, id_pattern)
inst_method = types.MethodType(new_method, self)
self._dynamic_properties[new_attr_name] = inst_method

View File

@ -253,7 +253,6 @@ class FormRegion(BaseFormRegion):
_header_locator = (by.By.CSS_SELECTOR, 'div.modal-header > h3')
_side_info_locator = (by.By.CSS_SELECTOR, 'div.right')
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset > div.form-group')
_input_locator = (by.By.CSS_SELECTOR, 'input,select,textarea')
# private methods
def __init__(self, driver, conf, src_elem, form_field_names):

View File

@ -45,10 +45,13 @@ class BtnActionRowRegion(BaseActionRowRegion):
def __init__(self, driver, conf, src_elem, action_name):
super(BtnActionRowRegion, self).__init__(driver, conf, src_elem)
self.action_name = action_name
self._action_id_pattern = ("%s__action_%%s" %
src_elem.get_attribute('id'))
self._init_action()
def _init_action(self):
self._init_dynamic_property(self.action_name, self._get_action)
self._init_dynamic_property(self.action_name, self._get_action,
self._action_id_pattern)
def _get_action(self):
return self._get_element(*self._action_locator)
@ -78,15 +81,19 @@ class ComplexActionRowRegion(BaseActionRowRegion):
try:
self.primary_action_name = action_names[self.PRIMARY_ACTION]
self.secondary_action_names = action_names[self.SECONDARY_ACTIONS]
self._action_id_pattern = ("%s__action_%%s" %
src_elem.get_attribute('id'))
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._get_primary_action,
self._action_id_pattern)
self._init_dynamic_properties(self.secondary_action_names,
self._get_secondary_actions)
self._get_secondary_actions,
self._action_id_pattern)
def _get_primary_action(self):
return self._get_element(*self._primary_action_locator)
@ -182,8 +189,9 @@ class ActionsTableRegion(BasicTableRegion):
' div.table_actions > a')
# private methods
def __init__(self, driver, conf, src_elm, action_names):
def __init__(self, driver, conf, src_elm, table_name, action_names):
super(ActionsTableRegion, self).__init__(driver, conf, src_elm)
self._action_id_pattern = "%s__action_%%s" % table_name
self.action_names = action_names
self._init_actions()
@ -192,7 +200,8 @@ class ActionsTableRegion(BasicTableRegion):
"""Create new methods that corresponds to picking table's
action buttons.
"""
self._init_dynamic_properties(self.action_names, self._get_actions)
self._init_dynamic_properties(self.action_names, self._get_actions,
self._action_id_pattern)
def _get_actions(self):
return self._get_elements(*self._actions_locator)
@ -206,9 +215,10 @@ class ActionsTableRegion(BasicTableRegion):
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)
def __init__(self, driver, conf, src_elm, table_name, action_names,
row_action_name):
super(SimpleActionsTableRegion, self).__init__(
driver, conf, src_elm, table_name, action_names)
self.row_action_name = row_action_name
def _get_rows(self):
@ -222,9 +232,10 @@ class SimpleActionsTableRegion(ActionsTableRegion):
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)
def __init__(self, driver, conf, src_elm, table_name,
action_names, row_action_names):
super(ComplexActionTableRegion, self).__init__(
driver, conf, src_elm, table_name, action_names)
self.row_action_names = row_action_names
def _get_rows(self):