Merge "Fix instances integration tests"
This commit is contained in:
commit
5ff708aed5
|
@ -15,4 +15,5 @@ from openstack_dashboard.test.integration_tests.pages.project.compute \
|
|||
|
||||
|
||||
class InstancesPage(instancespage.InstancesPage):
|
||||
pass
|
||||
|
||||
INSTANCES_TABLE_NAME_COLUMN = 'Name'
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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).\
|
||||
|
|
Loading…
Reference in New Issue