From d25d4d2b0d631b4d7169f287455baf230dc437ff Mon Sep 17 00:00:00 2001 From: Timur Sufiev Date: Fri, 28 Aug 2015 17:35:43 +0300 Subject: [PATCH] Do not match table actions by ordering in integration tests Instead of this match them by action 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 --- .../pages/admin/system/flavorspage.py | 8 +-- .../pages/identity/projectspage.py | 8 +-- .../pages/identity/userspage.py | 8 +-- .../access_and_security/floatingipspage.py | 9 ++-- .../access_and_security/keypairspage.py | 11 ++-- .../pages/project/compute/imagespage.py | 8 +-- .../pages/project/compute/overviewpage.py | 2 +- .../integration_tests/regions/baseregion.py | 51 ++++++++++++------- .../test/integration_tests/regions/forms.py | 1 - .../test/integration_tests/regions/tables.py | 33 ++++++++---- 10 files changed, 88 insertions(+), 51 deletions(-) diff --git a/openstack_dashboard/test/integration_tests/pages/admin/system/flavorspage.py b/openstack_dashboard/test/integration_tests/pages/admin/system/flavorspage.py index 32625b3eb6..36bf6916d0 100644 --- a/openstack_dashboard/test/integration_tests/pages/admin/system/flavorspage.py +++ b/openstack_dashboard/test/integration_tests/pages/admin/system/flavorspage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/identity/projectspage.py b/openstack_dashboard/test/integration_tests/pages/identity/projectspage.py index c52225fbf1..74a91bc1bc 100644 --- a/openstack_dashboard/test/integration_tests/pages/identity/projectspage.py +++ b/openstack_dashboard/test/integration_tests/pages/identity/projectspage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/identity/userspage.py b/openstack_dashboard/test/integration_tests/pages/identity/userspage.py index 20edea25d5..9e59eb1423 100644 --- a/openstack_dashboard/test/integration_tests/pages/identity/userspage.py +++ b/openstack_dashboard/test/integration_tests/pages/identity/userspage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/floatingipspage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/floatingipspage.py index 86c3480cd8..eeea04fc23 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/floatingipspage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/floatingipspage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py index 8518ca5832..c1d889b2d3 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py index 3b2501f210..57087e8dfe 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py @@ -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() diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/overviewpage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/overviewpage.py index 7e01f8586b..fd2bfd0eeb 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/overviewpage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/overviewpage.py @@ -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) diff --git a/openstack_dashboard/test/integration_tests/regions/baseregion.py b/openstack_dashboard/test/integration_tests/regions/baseregion.py index e36a6830ab..459f90b767 100644 --- a/openstack_dashboard/test/integration_tests/regions/baseregion.py +++ b/openstack_dashboard/test/integration_tests/regions/baseregion.py @@ -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 diff --git a/openstack_dashboard/test/integration_tests/regions/forms.py b/openstack_dashboard/test/integration_tests/regions/forms.py index d24453d04d..c004f6ef22 100644 --- a/openstack_dashboard/test/integration_tests/regions/forms.py +++ b/openstack_dashboard/test/integration_tests/regions/forms.py @@ -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): diff --git a/openstack_dashboard/test/integration_tests/regions/tables.py b/openstack_dashboard/test/integration_tests/regions/tables.py index 9b210dd824..f68bf66339 100644 --- a/openstack_dashboard/test/integration_tests/regions/tables.py +++ b/openstack_dashboard/test/integration_tests/regions/tables.py @@ -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):