Fix instances integration tests

The aim of this PS is to fix existing skipped instances
integration tests listed below:
- TestAdminInstances.test_create_delete_instance
- TestInstances.test_create_delete_instance

This PS also resolves Partial-Bug: #1774697

Change-Id: I9c2385274a2725409f61137ae201d868d24a56b4
This commit is contained in:
Pallav Gupta
2020-02-06 22:54:25 -06:00
parent ad46562ed6
commit ad41fe5106
6 changed files with 198 additions and 54 deletions

View File

@@ -15,4 +15,5 @@ from openstack_dashboard.test.integration_tests.pages.project.compute \
class InstancesPage(instancespage.InstancesPage): class InstancesPage(instancespage.InstancesPage):
pass
INSTANCES_TABLE_NAME_COLUMN = 'Name'

View File

@@ -10,35 +10,32 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import netaddr import netaddr
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import forms from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import menus
from openstack_dashboard.test.integration_tests.regions import tables from openstack_dashboard.test.integration_tests.regions import tables
class LaunchInstanceForm(forms.TabbedFormRegion):
field_mappings = ((
"availability_zone", "name", "flavor",
"count", "source_type", "instance_snapshot_id",
"volume_id", "volume_snapshot_id", "image_id", "volume_size",
"vol_delete_on_instance_delete"),
("keypair", "groups"),
("script_source", "script_upload", "script_data"),
("disk_config", "config_drive")
)
def __init__(self, driver, conf):
super(LaunchInstanceForm, self).__init__(
driver, conf, field_mappings=self.field_mappings)
class InstancesTable(tables.TableRegion): class InstancesTable(tables.TableRegion):
name = "instances" name = "instances"
LAUNCH_INSTANCE_FORM_FIELDS = (
("name", "count", "availability_zone"),
("boot_source_type", "volume_size"),
{
'flavor': menus.InstanceFlavorMenuRegion
},
{
'network': menus.InstanceAvailableResourceMenuRegion
},
)
@tables.bind_table_action('launch-ng') @tables.bind_table_action('launch-ng')
def launch_instance(self, launch_button): def launch_instance(self, launch_button):
launch_button.click() launch_button.click()
return LaunchInstanceForm(self.driver, self.conf) return forms.WizardFormRegion(self.driver, self.conf,
self.LAUNCH_INSTANCE_FORM_FIELDS)
@tables.bind_table_action('delete') @tables.bind_table_action('delete')
def delete_instance(self, delete_button): def delete_instance(self, delete_button):
@@ -50,18 +47,23 @@ class InstancesPage(basepage.BaseNavigationPage):
DEFAULT_FLAVOR = 'm1.tiny' DEFAULT_FLAVOR = 'm1.tiny'
DEFAULT_COUNT = 1 DEFAULT_COUNT = 1
DEFAULT_BOOT_SOURCE = 'Boot from image' DEFAULT_BOOT_SOURCE = 'Image'
DEFAULT_VOLUME_NAME = None DEFAULT_VOLUME_NAME = None
DEFAULT_SNAPSHOT_NAME = None DEFAULT_SNAPSHOT_NAME = None
DEFAULT_VOLUME_SNAPSHOT_NAME = None DEFAULT_VOLUME_SNAPSHOT_NAME = None
DEFAULT_VOL_DELETE_ON_INSTANCE_DELETE = False DEFAULT_VOL_DELETE_ON_INSTANCE_DELETE = True
DEFAULT_SECURITY_GROUP = True DEFAULT_SECURITY_GROUP = True
DEFAULT_NETWORK_TYPE = 'shared'
INSTANCES_TABLE_NAME_COLUMN = 'Instance Name' INSTANCES_TABLE_NAME_COLUMN = 'Instance Name'
INSTANCES_TABLE_STATUS_COLUMN = 'Status' INSTANCES_TABLE_STATUS_COLUMN = 'Status'
INSTANCES_TABLE_IP_COLUMN = 'IP Address' INSTANCES_TABLE_IP_COLUMN = 'IP Address'
INSTANCES_TABLE_IMAGE_NAME_COLUMN = 'Image Name' INSTANCES_TABLE_IMAGE_NAME_COLUMN = 'Image Name'
SOURCE_STEP_INDEX = 1
FLAVOR_STEP_INDEX = 2
NETWORKS_STEP_INDEX = 3
def __init__(self, driver, conf): def __init__(self, driver, conf):
super(InstancesPage, self).__init__(driver, conf) super(InstancesPage, self).__init__(driver, conf)
self._page_title = "Instances" self._page_title = "Instances"
@@ -71,8 +73,10 @@ class InstancesPage(basepage.BaseNavigationPage):
name) name)
def _get_rows_with_instances_names(self, names): def _get_rows_with_instances_names(self, names):
return [self.instances_table.get_row( return [
self.INSTANCES_TABLE_IMAGE_NAME_COLUMN, n) for n in names] self.instances_table.get_row(
self.INSTANCES_TABLE_IMAGE_NAME_COLUMN, n) for n in names
]
@property @property
def instances_table(self): def instances_table(self):
@@ -96,19 +100,35 @@ class InstancesPage(basepage.BaseNavigationPage):
instance_form = self.instances_table.launch_instance() instance_form = self.instances_table.launch_instance()
instance_form.availability_zone.value = available_zone instance_form.availability_zone.value = available_zone
instance_form.name.text = instance_name instance_form.name.text = instance_name
instance_form.flavor.text = flavor
instance_form.count.value = instance_count instance_form.count.value = instance_count
instance_form.source_type.text = boot_source instance_form.switch_to(self.SOURCE_STEP_INDEX)
instance_form.boot_source_type.text = boot_source
boot_source = self._get_source_name(instance_form, boot_source, boot_source = self._get_source_name(instance_form, boot_source,
self.conf.launch_instances) self.conf.launch_instances)
if not source_name: if not source_name:
source_name = boot_source[1] source_name = boot_source
boot_source[0].text = source_name menus.InstanceAvailableResourceMenuRegion(
self.driver, self.conf).transfer_available_resource(source_name)
if device_size: if device_size:
instance_form.volume_size.value = device_size instance_form.volume_size.value = device_size
if vol_delete_on_instance_delete: if vol_delete_on_instance_delete:
instance_form.vol_delete_on_instance_delete.mark() self.vol_delete_on_instance_delete_click()
instance_form.switch_to(self.FLAVOR_STEP_INDEX)
instance_form.flavor.transfer_available_resource(flavor)
instance_form.switch_to(self.NETWORKS_STEP_INDEX)
instance_form.network.transfer_available_resource(
self.DEFAULT_NETWORK_TYPE)
instance_form.submit() instance_form.submit()
instance_form.wait_till_wizard_disappears()
def vol_delete_on_instance_delete_click(self):
locator = (
by.By.XPATH,
'//label[contains(@ng-model, "vol_delete_on_instance_delete")]')
elements = self._get_elements(*locator)
for ele in elements:
if ele.text == 'Yes':
ele.click()
def delete_instance(self, name): def delete_instance(self, name):
row = self._get_row_with_instance_name(name) row = self._get_row_with_instance_name(name)
@@ -139,15 +159,14 @@ class InstancesPage(basepage.BaseNavigationPage):
('Active', 'Error')) ('Active', 'Error'))
return status == 'Active' return status == 'Active'
def _get_source_name(self, instance, boot_source, def _get_source_name(self, instance, boot_source, conf):
conf): if 'Image' in boot_source:
if 'image' in boot_source: return conf.image_name
return instance.image_id, conf.image_name elif boot_source == 'Volume':
elif boot_source == 'Boot from volume':
return instance.volume_id, self.DEFAULT_VOLUME_NAME return instance.volume_id, self.DEFAULT_VOLUME_NAME
elif boot_source == 'Boot from snapshot': elif boot_source == 'Instance Snapshot':
return instance.instance_snapshot_id, self.DEFAULT_SNAPSHOT_NAME return instance.instance_snapshot_id, self.DEFAULT_SNAPSHOT_NAME
elif 'volume snapshot (creates a new volume)' in boot_source: elif 'Volume Snapshot' in boot_source:
return (instance.volume_snapshot_id, return (instance.volume_snapshot_id,
self.DEFAULT_VOLUME_SNAPSHOT_NAME) self.DEFAULT_VOLUME_SNAPSHOT_NAME)

View File

@@ -77,6 +77,10 @@ class BaseFormFieldRegion(baseregion.BaseRegion,
def name(self): def name(self):
return self.element.get_attribute('name') return self.element.get_attribute('name')
@property
def id(self):
return self.element.get_attribute('id')
def is_required(self): def is_required(self):
classes = self.driver.get_attribute('class') classes = self.driver.get_attribute('class')
return 'required' in classes return 'required' in classes
@@ -113,7 +117,7 @@ class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
class ChooseFileFormFieldRegion(BaseFormFieldRegion): class ChooseFileFormFieldRegion(BaseFormFieldRegion):
"""Choose file field.""" """Choose file field."""
_element_locator_str_suffix = 'div > input[type=file]' _element_locator_str_suffix = 'input[type=file]'
def choose(self, path): def choose(self, path):
self.element.send_keys(os.path.join(os.getcwd(), path)) self.element.send_keys(os.path.join(os.getcwd(), path))
@@ -136,31 +140,31 @@ class TextInputFormFieldRegion(BaseTextFormFieldRegion):
"""Text input box.""" """Text input box."""
_element_locator_str_suffix = \ _element_locator_str_suffix = \
'div > input[type=text], div > input[type=None]' 'input[type=text], input[type=None]'
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion): class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
"""Password text input box.""" """Password text input box."""
_element_locator_str_suffix = 'div > input[type=password]' _element_locator_str_suffix = 'input[type=password]'
class EmailInputFormFieldRegion(BaseTextFormFieldRegion): class EmailInputFormFieldRegion(BaseTextFormFieldRegion):
"""Email text input box.""" """Email text input box."""
_element_locator_str_suffix = 'div > input[type=email]' _element_locator_str_suffix = 'input[type=email]'
class TextAreaFormFieldRegion(BaseTextFormFieldRegion): class TextAreaFormFieldRegion(BaseTextFormFieldRegion):
"""Multi-line text input box.""" """Multi-line text input box."""
_element_locator_str_suffix = 'div > textarea' _element_locator_str_suffix = 'textarea'
class IntegerFormFieldRegion(BaseFormFieldRegion): class IntegerFormFieldRegion(BaseFormFieldRegion):
"""Integer input box.""" """Integer input box."""
_element_locator_str_suffix = 'div > input[type=number]' _element_locator_str_suffix = 'input[type=number]'
@property @property
def value(self): def value(self):
@@ -174,7 +178,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
class SelectFormFieldRegion(BaseFormFieldRegion): class SelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field.""" """Select box field."""
_element_locator_str_suffix = 'div > select.form-control' _element_locator_str_suffix = 'select.form-control'
def is_displayed(self): def is_displayed(self):
return self.element._el.is_displayed() return self.element._el.is_displayed()
@@ -201,6 +205,10 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
def name(self): def name(self):
return self.element._el.get_attribute('name') return self.element._el.get_attribute('name')
@property
def id(self):
return self.element._el.get_attribute('id')
@property @property
def text(self): def text(self):
return self.element.first_selected_option.text return self.element.first_selected_option.text
@@ -226,7 +234,7 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
class ThemableSelectFormFieldRegion(BaseFormFieldRegion): class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field.""" """Select box field."""
_element_locator_str_suffix = 'div > .themable-select' _element_locator_str_suffix = '.themable-select'
_raw_select_locator = (by.By.CSS_SELECTOR, 'select') _raw_select_locator = (by.By.CSS_SELECTOR, 'select')
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title') _selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a') _dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
@@ -332,6 +340,7 @@ class FormRegion(BaseFormRegion):
_header_locator = (by.By.CSS_SELECTOR, 'div.modal-header > h3') _header_locator = (by.By.CSS_SELECTOR, 'div.modal-header > h3')
_side_info_locator = (by.By.CSS_SELECTOR, 'div.right') _side_info_locator = (by.By.CSS_SELECTOR, 'div.right')
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset') _fields_locator = (by.By.CSS_SELECTOR, 'fieldset')
_step_locator = (by.By.CSS_SELECTOR, 'div.step')
# private methods # private methods
def __init__(self, driver, conf, src_elem=None, field_mappings=None): def __init__(self, driver, conf, src_elem=None, field_mappings=None):
@@ -359,9 +368,15 @@ class FormRegion(BaseFormRegion):
def _get_form_fields(self): def _get_form_fields(self):
factory = FieldFactory(self.driver, self.conf, self.fields_src_elem) factory = FieldFactory(self.driver, self.conf, self.fields_src_elem)
fields = {}
try: try:
self._turn_off_implicit_wait() self._turn_off_implicit_wait()
return {field.name: field for field in factory.fields()} for field in factory.fields():
if hasattr(field, 'name') and field.name is not None:
fields.update({field.name.replace('-', '_'): field})
elif hasattr(field, 'id') and field.id is not None:
fields.update({field.id.replace('-', '_'): field})
return fields
finally: finally:
self._turn_on_implicit_wait() self._turn_on_implicit_wait()
@@ -465,6 +480,61 @@ class TabbedFormRegion(FormRegion):
src_elem=self.src_elem) src_elem=self.src_elem)
class WizardFormRegion(FormRegion):
"""Form consists of sequence of steps."""
_submit_locator = (by.By.CSS_SELECTOR,
'*.btn.btn-primary.finish[type=button]')
def __init__(self, driver, conf, field_mappings=None, default_step=0):
self.current_step = default_step
super(WizardFormRegion, self).__init__(driver,
conf,
field_mappings=field_mappings)
def _form_getter(self):
return self.driver.find_element(*self._default_form_locator)
def _prepare_mappings(self, field_mappings):
return [
super(WizardFormRegion, self)._prepare_mappings(step_mappings)
for step_mappings in field_mappings
]
def _init_form_fields(self):
self.switch_to(self.current_step)
def _init_step_fields(self, step_index):
steps = self._get_elements(*self._step_locator)
self.fields_src_elem = steps[step_index]
fields = self._get_form_fields()
current_step_mappings = self.field_mappings[step_index]
for accessor_name, accessor_expr in current_step_mappings.items():
if isinstance(accessor_expr, str):
self._dynamic_properties[accessor_name] = fields[accessor_expr]
else: # it is a class
self._dynamic_properties[accessor_name] = accessor_expr(
self.driver, self.conf)
def switch_to(self, step_index=0):
self.steps.switch_to(index=step_index)
self._init_step_fields(step_index)
def wait_till_wizard_disappears(self):
try:
self.wait_till_element_disappears(self._form_getter)
except exceptions.StaleElementReferenceException:
# The form might be absent already by the time the first check
# occurs. So just suppress the exception here.
pass
@property
def steps(self):
return menus.WizardMenuRegion(self.driver,
self.conf,
src_elem=self.src_elem)
class DateFormRegion(BaseFormRegion): class DateFormRegion(BaseFormRegion):
"""Form that queries data to table that is regularly below the form. """Form that queries data to table that is regularly below the form.

View File

@@ -292,6 +292,15 @@ class TabbedMenuRegion(baseregion.BaseRegion):
self._get_elements(*self._tab_locator)[index].click() self._get_elements(*self._tab_locator)[index].click()
class WizardMenuRegion(baseregion.BaseRegion):
_step_locator = (by.By.CSS_SELECTOR, 'li > a')
_default_src_locator = (by.By.CSS_SELECTOR, 'div > .nav.nav-pills')
def switch_to(self, index=0):
self._get_elements(*self._step_locator)[index].click()
class ProjectDropDownRegion(DropDownMenuRegion): class ProjectDropDownRegion(DropDownMenuRegion):
_menu_items_locator = ( _menu_items_locator = (
by.By.CSS_SELECTOR, 'ul.context-selection li > a') by.By.CSS_SELECTOR, 'ul.context-selection li > a')
@@ -414,3 +423,30 @@ class MembershipMenuRegion(baseregion.BaseRegion):
self._switch_member_roles( self._switch_member_roles(
name, roles2remove, self.get_member_allocated_roles, name, roles2remove, self.get_member_allocated_roles,
allocated_members=allocated_members) allocated_members=allocated_members)
class InstanceAvailableResourceMenuRegion(baseregion.BaseRegion):
_available_table_locator = (
by.By.CSS_SELECTOR,
'div.step:not(.ng-hide) div.transfer-available table')
_available_table_row_locator = (by.By.CSS_SELECTOR,
"tbody > tr.ng-scope:not(.detail-row)")
_available_table_column_locator = (by.By.TAG_NAME, "td")
_action_column_btn_locator = (by.By.CSS_SELECTOR,
"td.actions_column button")
def transfer_available_resource(self, resource_name):
available_table = self._get_element(*self._available_table_locator)
rows = available_table.find_elements(
*self._available_table_row_locator)
for row in rows:
cols = row.find_elements(*self._available_table_column_locator)
if len(cols) > 1 and cols[1].text.strip() in resource_name:
row_selector_btn = row.find_element(
*self._action_column_btn_locator)
row_selector_btn.click()
break
class InstanceFlavorMenuRegion(InstanceAvailableResourceMenuRegion):
_action_column_btn_locator = (by.By.CSS_SELECTOR, "td.action-col button")

View File

@@ -12,6 +12,7 @@
import functools import functools
from django.utils import html
from selenium.common import exceptions from selenium.common import exceptions
from selenium.webdriver.common import by from selenium.webdriver.common import by
@@ -60,6 +61,7 @@ class TableRegion(baseregion.BaseRegion):
'div.table_search > .themable-select') 'div.table_search > .themable-select')
_cell_progress_bar_locator = (by.By.CSS_SELECTOR, 'div.progress-bar') _cell_progress_bar_locator = (by.By.CSS_SELECTOR, 'div.progress-bar')
_warning_cell_locator = (by.By.CSS_SELECTOR, 'td.warning') _warning_cell_locator = (by.By.CSS_SELECTOR, 'td.warning')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
marker_name = 'marker' marker_name = 'marker'
prev_marker_name = 'prev_marker' prev_marker_name = 'prev_marker'
@@ -84,6 +86,9 @@ class TableRegion(baseregion.BaseRegion):
def _warning_cell_getter(self): def _warning_cell_getter(self):
return self.driver.find_element(*self._warning_cell_locator) return self.driver.find_element(*self._warning_cell_locator)
def _form_getter(self):
return self.driver.find_element(*self._default_form_locator)
def __init__(self, driver, conf): def __init__(self, driver, conf):
self._default_src_locator = self._table_locator(self.__class__.name) self._default_src_locator = self._table_locator(self.__class__.name)
super(TableRegion, self).__init__(driver, conf) super(TableRegion, self).__init__(driver, conf)
@@ -115,12 +120,13 @@ class TableRegion(baseregion.BaseRegion):
def filter(self, value): def filter(self, value):
self._set_search_field(value) self._set_search_field(value)
self._click_search_btn() self._click_search_btn()
self.driver.implicitly_wait(5)
def set_filter_value(self, value): def set_filter_value(self, value):
search_menu = self._get_element(*self._search_option_locator) self.wait_till_element_disappears(self._form_getter)
search_menu.click() js_cmd = ("$('ul.dropdown-menu').find(\"a[data-select-value='%s']\")."
item_locator = self._search_menu_value_locator(value) "click();" % (html.escape(value)))
search_menu.find_element(*item_locator).click() self.driver.execute_script(js_cmd)
def get_row(self, column_name, text, exact_match=True): def get_row(self, column_name, text, exact_match=True):
"""Get row that contains specified text in specified column. """Get row that contains specified text in specified column.
@@ -222,7 +228,13 @@ class TableRegion(baseregion.BaseRegion):
lnk = self._get_element(*self._prev_locator) lnk = self._get_element(*self._prev_locator)
lnk.click() lnk.click()
def assert_definition(self, expected_table_definition, sorting=False): def get_column_data(self, name_column='Name'):
return [row.cells[name_column].text for row in self.rows]
def assert_definition(self,
expected_table_definition,
sorting=False,
name_column='Name'):
"""Checks that actual table is expected one. """Checks that actual table is expected one.
Items to compare: 'next' and 'prev' links, count of rows and names of Items to compare: 'next' and 'prev' links, count of rows and names of
@@ -231,7 +243,7 @@ class TableRegion(baseregion.BaseRegion):
:param sorting: boolean arg specifying whether to sort actual names :param sorting: boolean arg specifying whether to sort actual names
:return: :return:
""" """
names = [row.cells['Name'].text for row in self.rows] names = self.get_column_data(name_column)
if sorting: if sorting:
names.sort() names.sort()
actual_table = {'Next': self.is_next_link_available(), actual_table = {'Next': self.is_next_link_available(),

View File

@@ -23,7 +23,10 @@ class TestInstances(helpers.TestCase):
def instances_page(self): def instances_page(self):
return self.home_pg.go_to_project_compute_instancespage() return self.home_pg.go_to_project_compute_instancespage()
@pytest.mark.skip(reason="Bug 1774697") @property
def instance_table_name_column(self):
return 'Instance Name'
def test_create_delete_instance(self): def test_create_delete_instance(self):
"""tests the instance creation and deletion functionality: """tests the instance creation and deletion functionality:
@@ -35,16 +38,14 @@ class TestInstances(helpers.TestCase):
instances_page = self.home_pg.go_to_project_compute_instancespage() instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.create_instance(self.INSTANCE_NAME) instances_page.create_instance(self.INSTANCE_NAME)
self.assertTrue( self.assertTrue(instances_page.find_message_and_dismiss(messages.INFO))
instances_page.find_message_and_dismiss(messages.SUCCESS))
self.assertFalse( self.assertFalse(
instances_page.find_message_and_dismiss(messages.ERROR)) instances_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(instances_page.is_instance_active(self.INSTANCE_NAME)) self.assertTrue(instances_page.is_instance_active(self.INSTANCE_NAME))
instances_page = self.instances_page instances_page = self.instances_page
instances_page.delete_instance(self.INSTANCE_NAME) instances_page.delete_instance(self.INSTANCE_NAME)
self.assertTrue( self.assertTrue(instances_page.find_message_and_dismiss(messages.INFO))
instances_page.find_message_and_dismiss(messages.SUCCESS))
self.assertFalse( self.assertFalse(
instances_page.find_message_and_dismiss(messages.ERROR)) instances_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(instances_page.is_instance_deleted(self.INSTANCE_NAME)) self.assertTrue(instances_page.is_instance_deleted(self.INSTANCE_NAME))
@@ -242,8 +243,13 @@ class TestAdminInstances(helpers.AdminTestCase, TestInstances):
@property @property
def instances_page(self): def instances_page(self):
self.home_pg.go_to_admin_overviewpage()
return self.home_pg.go_to_admin_compute_instancespage() return self.home_pg.go_to_admin_compute_instancespage()
@property
def instance_table_name_column(self):
return 'Name'
@pytest.mark.skip(reason="Bug 1774697") @pytest.mark.skip(reason="Bug 1774697")
def test_instances_pagination_and_filtration(self): def test_instances_pagination_and_filtration(self):
super(TestAdminInstances, self).\ super(TestAdminInstances, self).\