diff --git a/openstack_dashboard/test/integration_tests/pages/admin/compute/instancespage.py b/openstack_dashboard/test/integration_tests/pages/admin/compute/instancespage.py index 94ed1d6591..679662b9b4 100644 --- a/openstack_dashboard/test/integration_tests/pages/admin/compute/instancespage.py +++ b/openstack_dashboard/test/integration_tests/pages/admin/compute/instancespage.py @@ -15,4 +15,5 @@ from openstack_dashboard.test.integration_tests.pages.project.compute \ class InstancesPage(instancespage.InstancesPage): - pass + + INSTANCES_TABLE_NAME_COLUMN = 'Name' diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/instancespage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/instancespage.py index a4227b23fa..253a2c3b47 100644 --- a/openstack_dashboard/test/integration_tests/pages/project/compute/instancespage.py +++ b/openstack_dashboard/test/integration_tests/pages/project/compute/instancespage.py @@ -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) diff --git a/openstack_dashboard/test/integration_tests/regions/forms.py b/openstack_dashboard/test/integration_tests/regions/forms.py index 7b276a2737..8a52f798b9 100644 --- a/openstack_dashboard/test/integration_tests/regions/forms.py +++ b/openstack_dashboard/test/integration_tests/regions/forms.py @@ -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. diff --git a/openstack_dashboard/test/integration_tests/regions/menus.py b/openstack_dashboard/test/integration_tests/regions/menus.py index 68e580faf7..341c1265ed 100644 --- a/openstack_dashboard/test/integration_tests/regions/menus.py +++ b/openstack_dashboard/test/integration_tests/regions/menus.py @@ -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") diff --git a/openstack_dashboard/test/integration_tests/regions/tables.py b/openstack_dashboard/test/integration_tests/regions/tables.py index 336f57e351..c3c4b39330 100644 --- a/openstack_dashboard/test/integration_tests/regions/tables.py +++ b/openstack_dashboard/test/integration_tests/regions/tables.py @@ -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(), diff --git a/openstack_dashboard/test/integration_tests/tests/test_instances.py b/openstack_dashboard/test/integration_tests/tests/test_instances.py index 7626e690d5..59f1c44a46 100644 --- a/openstack_dashboard/test/integration_tests/tests/test_instances.py +++ b/openstack_dashboard/test/integration_tests/tests/test_instances.py @@ -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).\