Merge "Fix instances integration tests"

This commit is contained in:
Zuul 2020-05-20 16:39:05 +00:00 committed by Gerrit Code Review
commit 5ff708aed5
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):
pass
INSTANCES_TABLE_NAME_COLUMN = 'Name'

View File

@ -10,35 +10,32 @@
# License for the specific language governing permissions and limitations
# under the License.
import netaddr
from selenium.webdriver.common import by
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 menus
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):
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')
def launch_instance(self, launch_button):
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')
def delete_instance(self, delete_button):
@ -50,18 +47,23 @@ class InstancesPage(basepage.BaseNavigationPage):
DEFAULT_FLAVOR = 'm1.tiny'
DEFAULT_COUNT = 1
DEFAULT_BOOT_SOURCE = 'Boot from image'
DEFAULT_BOOT_SOURCE = 'Image'
DEFAULT_VOLUME_NAME = None
DEFAULT_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_NETWORK_TYPE = 'shared'
INSTANCES_TABLE_NAME_COLUMN = 'Instance Name'
INSTANCES_TABLE_STATUS_COLUMN = 'Status'
INSTANCES_TABLE_IP_COLUMN = 'IP Address'
INSTANCES_TABLE_IMAGE_NAME_COLUMN = 'Image Name'
SOURCE_STEP_INDEX = 1
FLAVOR_STEP_INDEX = 2
NETWORKS_STEP_INDEX = 3
def __init__(self, driver, conf):
super(InstancesPage, self).__init__(driver, conf)
self._page_title = "Instances"
@ -71,8 +73,10 @@ class InstancesPage(basepage.BaseNavigationPage):
name)
def _get_rows_with_instances_names(self, names):
return [self.instances_table.get_row(
self.INSTANCES_TABLE_IMAGE_NAME_COLUMN, n) for n in names]
return [
self.instances_table.get_row(
self.INSTANCES_TABLE_IMAGE_NAME_COLUMN, n) for n in names
]
@property
def instances_table(self):
@ -96,19 +100,35 @@ class InstancesPage(basepage.BaseNavigationPage):
instance_form = self.instances_table.launch_instance()
instance_form.availability_zone.value = available_zone
instance_form.name.text = instance_name
instance_form.flavor.text = flavor
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,
self.conf.launch_instances)
if not source_name:
source_name = boot_source[1]
boot_source[0].text = source_name
source_name = boot_source
menus.InstanceAvailableResourceMenuRegion(
self.driver, self.conf).transfer_available_resource(source_name)
if device_size:
instance_form.volume_size.value = device_size
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.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):
row = self._get_row_with_instance_name(name)
@ -139,15 +159,14 @@ class InstancesPage(basepage.BaseNavigationPage):
('Active', 'Error'))
return status == 'Active'
def _get_source_name(self, instance, boot_source,
conf):
if 'image' in boot_source:
return instance.image_id, conf.image_name
elif boot_source == 'Boot from volume':
def _get_source_name(self, instance, boot_source, conf):
if 'Image' in boot_source:
return conf.image_name
elif boot_source == 'Volume':
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
elif 'volume snapshot (creates a new volume)' in boot_source:
elif 'Volume Snapshot' in boot_source:
return (instance.volume_snapshot_id,
self.DEFAULT_VOLUME_SNAPSHOT_NAME)

View File

@ -77,6 +77,10 @@ class BaseFormFieldRegion(baseregion.BaseRegion,
def name(self):
return self.element.get_attribute('name')
@property
def id(self):
return self.element.get_attribute('id')
def is_required(self):
classes = self.driver.get_attribute('class')
return 'required' in classes
@ -113,7 +117,7 @@ class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
"""Choose file field."""
_element_locator_str_suffix = 'div > input[type=file]'
_element_locator_str_suffix = 'input[type=file]'
def choose(self, path):
self.element.send_keys(os.path.join(os.getcwd(), path))
@ -136,31 +140,31 @@ class TextInputFormFieldRegion(BaseTextFormFieldRegion):
"""Text input box."""
_element_locator_str_suffix = \
'div > input[type=text], div > input[type=None]'
'input[type=text], input[type=None]'
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
"""Password text input box."""
_element_locator_str_suffix = 'div > input[type=password]'
_element_locator_str_suffix = 'input[type=password]'
class EmailInputFormFieldRegion(BaseTextFormFieldRegion):
"""Email text input box."""
_element_locator_str_suffix = 'div > input[type=email]'
_element_locator_str_suffix = 'input[type=email]'
class TextAreaFormFieldRegion(BaseTextFormFieldRegion):
"""Multi-line text input box."""
_element_locator_str_suffix = 'div > textarea'
_element_locator_str_suffix = 'textarea'
class IntegerFormFieldRegion(BaseFormFieldRegion):
"""Integer input box."""
_element_locator_str_suffix = 'div > input[type=number]'
_element_locator_str_suffix = 'input[type=number]'
@property
def value(self):
@ -174,7 +178,7 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
class SelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field."""
_element_locator_str_suffix = 'div > select.form-control'
_element_locator_str_suffix = 'select.form-control'
def is_displayed(self):
return self.element._el.is_displayed()
@ -201,6 +205,10 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
def name(self):
return self.element._el.get_attribute('name')
@property
def id(self):
return self.element._el.get_attribute('id')
@property
def text(self):
return self.element.first_selected_option.text
@ -226,7 +234,7 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field."""
_element_locator_str_suffix = 'div > .themable-select'
_element_locator_str_suffix = '.themable-select'
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
_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')
_side_info_locator = (by.By.CSS_SELECTOR, 'div.right')
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset')
_step_locator = (by.By.CSS_SELECTOR, 'div.step')
# private methods
def __init__(self, driver, conf, src_elem=None, field_mappings=None):
@ -359,9 +368,15 @@ class FormRegion(BaseFormRegion):
def _get_form_fields(self):
factory = FieldFactory(self.driver, self.conf, self.fields_src_elem)
fields = {}
try:
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:
self._turn_on_implicit_wait()
@ -465,6 +480,61 @@ class TabbedFormRegion(FormRegion):
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):
"""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()
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):
_menu_items_locator = (
by.By.CSS_SELECTOR, 'ul.context-selection li > a')
@ -414,3 +423,30 @@ class MembershipMenuRegion(baseregion.BaseRegion):
self._switch_member_roles(
name, roles2remove, self.get_member_allocated_roles,
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
from django.utils import html
from selenium.common import exceptions
from selenium.webdriver.common import by
@ -60,6 +61,7 @@ class TableRegion(baseregion.BaseRegion):
'div.table_search > .themable-select')
_cell_progress_bar_locator = (by.By.CSS_SELECTOR, 'div.progress-bar')
_warning_cell_locator = (by.By.CSS_SELECTOR, 'td.warning')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
marker_name = 'marker'
prev_marker_name = 'prev_marker'
@ -84,6 +86,9 @@ class TableRegion(baseregion.BaseRegion):
def _warning_cell_getter(self):
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):
self._default_src_locator = self._table_locator(self.__class__.name)
super(TableRegion, self).__init__(driver, conf)
@ -115,12 +120,13 @@ class TableRegion(baseregion.BaseRegion):
def filter(self, value):
self._set_search_field(value)
self._click_search_btn()
self.driver.implicitly_wait(5)
def set_filter_value(self, value):
search_menu = self._get_element(*self._search_option_locator)
search_menu.click()
item_locator = self._search_menu_value_locator(value)
search_menu.find_element(*item_locator).click()
self.wait_till_element_disappears(self._form_getter)
js_cmd = ("$('ul.dropdown-menu').find(\"a[data-select-value='%s']\")."
"click();" % (html.escape(value)))
self.driver.execute_script(js_cmd)
def get_row(self, column_name, text, exact_match=True):
"""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.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.
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
:return:
"""
names = [row.cells['Name'].text for row in self.rows]
names = self.get_column_data(name_column)
if sorting:
names.sort()
actual_table = {'Next': self.is_next_link_available(),

View File

@ -23,7 +23,10 @@ class TestInstances(helpers.TestCase):
def instances_page(self):
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):
"""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.create_instance(self.INSTANCE_NAME)
self.assertTrue(
instances_page.find_message_and_dismiss(messages.SUCCESS))
self.assertTrue(instances_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(
instances_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(instances_page.is_instance_active(self.INSTANCE_NAME))
instances_page = self.instances_page
instances_page.delete_instance(self.INSTANCE_NAME)
self.assertTrue(
instances_page.find_message_and_dismiss(messages.SUCCESS))
self.assertTrue(instances_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(
instances_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(instances_page.is_instance_deleted(self.INSTANCE_NAME))
@ -242,8 +243,13 @@ class TestAdminInstances(helpers.AdminTestCase, TestInstances):
@property
def instances_page(self):
self.home_pg.go_to_admin_overviewpage()
return self.home_pg.go_to_admin_compute_instancespage()
@property
def instance_table_name_column(self):
return 'Name'
@pytest.mark.skip(reason="Bug 1774697")
def test_instances_pagination_and_filtration(self):
super(TestAdminInstances, self).\