summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-03-03 21:28:30 +0000
committerGerrit Code Review <review@openstack.org>2023-03-03 21:28:30 +0000
commit7421a21e18311c85cceea15381efe333ce8e9077 (patch)
treed1aa97b20d8ddcf662336a49abce95958c33edc4
parente472a8faae60d76f9a80f73524b92574471d9f23 (diff)
parentf20cc8faa5a8a252ec09c48f5a76130ca50a567b (diff)
downloadhorizon-7421a21e18311c85cceea15381efe333ce8e9077.tar.gz
Merge "Integration test navigation machinery for Angular pages" into stable/yoga
-rw-r--r--horizon/test/webdriver.py4
-rw-r--r--openstack_dashboard/static/app/core/images/steps/create-image/create-image.html16
-rw-r--r--openstack_dashboard/static/app/core/images/steps/edit-image/edit-image.html4
-rw-r--r--openstack_dashboard/test/integration_tests/config.py6
-rw-r--r--openstack_dashboard/test/integration_tests/horizon.conf5
-rw-r--r--openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py246
-rw-r--r--openstack_dashboard/test/integration_tests/regions/forms.py23
-rw-r--r--openstack_dashboard/test/integration_tests/regions/tables.py157
-rw-r--r--openstack_dashboard/test/integration_tests/tests/test_images.py233
-rw-r--r--openstack_dashboard/test/integration_tests/tests/test_volumes.py24
10 files changed, 483 insertions, 235 deletions
diff --git a/horizon/test/webdriver.py b/horizon/test/webdriver.py
index 3edb71dcb..824bf02e4 100644
--- a/horizon/test/webdriver.py
+++ b/horizon/test/webdriver.py
@@ -37,7 +37,7 @@ class WrapperFindOverride(object):
"""Mixin for overriding find_element methods."""
def find_element(self, by=by.By.ID, value=None):
- repeat = range(2)
+ repeat = range(10)
for i in repeat:
try:
web_el = super().find_element(by, value)
@@ -48,7 +48,7 @@ class WrapperFindOverride(object):
self)
def find_elements(self, by=by.By.ID, value=None):
- repeat = range(2)
+ repeat = range(10)
for i in repeat:
try:
web_els = super().find_elements(by, value)
diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
index 26aa31034..71f70ffbe 100644
--- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
+++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
@@ -76,15 +76,9 @@
<label class="control-label" for="imageForm-image_url">
<translate>File</translate><span class="hz-icon-required fa fa-asterisk"></span>
</label>
- <div class="input-group" ng-hide="ctrl.uploadProgress > -1">
- <span class="input-group-btn">
- <button class="btn btn-primary" ng-model="image_file"
- ngf-select="ctrl.prepareUpload(image_file)"
- name="image_file" ng-required="true"
- ng-disabled="viewModel.isSubmitting"
- id="imageForm-image_file" translate>Browse...</button>
- </span>
- <input type="text" class="form-control" readonly ng-model="image_file.name">
+ <div ng-hide="ctrl.uploadProgress > -1">
+ <input type="file" ng-model="image_file" ngf-select="ctrl.prepareUpload(image_file)"
+ name="image_file" ng-required="true" ng-disabled="viewModel.isSubmitting">
</div>
<div ng-hide="ctrl.uploadProgress < 0" class="progress-text">
<uib-progressbar value="ctrl.uploadProgress"></uib-progressbar>
@@ -239,7 +233,7 @@
<translate>Visibility</translate>
</label>
<div class="form-field">
- <div class="btn-group">
+ <div class="btn-group" name="visibility">
<label class="btn btn-default"
ng-repeat="option in ctrl.imageVisibilityOptions"
ng-model="ctrl.image.visibility"
@@ -254,7 +248,7 @@
<translate>Protected</translate>
</label>
<div class="form-field">
- <div class="btn-group">
+ <div class="btn-group" name="protected">
<label class="btn btn-default"
ng-repeat="option in ctrl.imageProtectedOptions"
ng-model="ctrl.image.protected"
diff --git a/openstack_dashboard/static/app/core/images/steps/edit-image/edit-image.html b/openstack_dashboard/static/app/core/images/steps/edit-image/edit-image.html
index ce75e8e2e..46d493227 100644
--- a/openstack_dashboard/static/app/core/images/steps/edit-image/edit-image.html
+++ b/openstack_dashboard/static/app/core/images/steps/edit-image/edit-image.html
@@ -126,7 +126,7 @@
<div class="form-group">
<label class="control-label required" translate>Visibility</label>
<div class="form-field">
- <div class="btn-group">
+ <div class="btn-group" name="visibility">
<label class="btn btn-default"
ng-repeat="option in ctrl.imageVisibilityOptions"
ng-model="ctrl.image.visibility"
@@ -139,7 +139,7 @@
<div class="form-group">
<label class="control-label required" translate>Protected</label>
<div class="form-field">
- <div class="btn-group">
+ <div class="btn-group" name="protected">
<label class="btn btn-default"
ng-repeat="option in ctrl.imageProtectedOptions"
ng-model="ctrl.image.protected"
diff --git a/openstack_dashboard/test/integration_tests/config.py b/openstack_dashboard/test/integration_tests/config.py
index fa90b1152..475bf098b 100644
--- a/openstack_dashboard/test/integration_tests/config.py
+++ b/openstack_dashboard/test/integration_tests/config.py
@@ -75,11 +75,11 @@ ImageGroup = [
default='angular',
help='type/version of images panel'),
cfg.StrOpt('http_image',
- default='http://download.cirros-cloud.net/0.3.1/'
- 'cirros-0.3.1-x86_64-uec.tar.gz',
+ default='http://download.cirros-cloud.net/0.5.2/'
+ 'cirros-0.5.2-x86_64-uec.tar.gz',
help='http accessible image'),
cfg.ListOpt('images_list',
- default=['cirros-0.3.5-x86_64-disk'],
+ default=['cirros-0.5.2-x86_64-disk'],
help='default list of images')
]
diff --git a/openstack_dashboard/test/integration_tests/horizon.conf b/openstack_dashboard/test/integration_tests/horizon.conf
index 1b0301e6d..52db2a69c 100644
--- a/openstack_dashboard/test/integration_tests/horizon.conf
+++ b/openstack_dashboard/test/integration_tests/horizon.conf
@@ -43,8 +43,9 @@ panel_type=legacy
[image]
# http accessible image (string value)
panel_type=angular
-http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
-images_list=cirros-0.3.5-x86_64-disk
+http_image=http://download.cirros-cloud.net/0.5.2/cirros-0.5.2-x86_64-uec.tar.gz
+images_list=cirros-0.5.2-x86_64-disk
+
[identity]
# Username to use for non-admin API requests. (string value)
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 304d506d8..117c64b5c 100644
--- a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py
+++ b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py
@@ -13,120 +13,133 @@ 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
from openstack_dashboard.test.integration_tests.pages.project.compute.\
instancespage import InstancesPage
-from openstack_dashboard.test.integration_tests.pages.project.volumes.\
- volumespage import VolumesPage
# TODO(bpokorny): Set the default source back to 'url' once Glance removes
# the show_multiple_locations option, and if the default devstack policies
# allow setting locations.
DEFAULT_IMAGE_SOURCE = 'file'
-DEFAULT_IMAGE_FORMAT = 'qcow2'
+DEFAULT_IMAGE_FORMAT = 'string:raw'
DEFAULT_ACCESSIBILITY = False
DEFAULT_PROTECTION = False
-IMAGES_TABLE_NAME_COLUMN = 'name'
-IMAGES_TABLE_STATUS_COLUMN = 'status'
-IMAGES_TABLE_FORMAT_COLUMN = 'disk_format'
+IMAGES_TABLE_NAME_COLUMN = 'Name'
+IMAGES_TABLE_STATUS_COLUMN = 'Status'
+IMAGES_TABLE_FORMAT_COLUMN = 'Disk Format'
-class ImagesTable(tables.TableRegion):
+class ImagesTable(tables.TableRegionNG):
name = "images"
CREATE_IMAGE_FORM_FIELDS = (
- "name", "description", "image_file", "kernel", "ramdisk",
- "disk_format", "architecture", "min_disk", "min_ram",
- "is_public", "protected"
+ "name", "description", "image_file", "kernel", "ramdisk", "format",
+ "architecture", "min_disk", "min_ram", "visibility", "protected"
)
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
- "name", "description", "image_source",
- "type", "size", "availability_zone")
-
- LAUNCH_INSTANCE_FROM_FIELDS = ((
- "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")
+ "name", "description",
+ "volume_size",
+ "availability_zone")
+
+ LAUNCH_INSTANCE_FORM_FIELDS = (
+ ("name", "count", "availability_zone"),
+ ("boot_source_type", "volume_size"),
+ {
+ 'flavor': menus.InstanceFlavorMenuRegion
+ },
+ {
+ 'network': menus.InstanceAvailableResourceMenuRegion
+ },
)
EDIT_IMAGE_FORM_FIELDS = (
- "name", "description", "disk_format", "min_disk",
- "min_ram", "public", "protected"
+ "name", "description", "format", "min_disk",
+ "min_ram", "visibility", "protected"
)
- @tables.bind_table_action('create')
+ @tables.bind_table_action_ng('Create Image')
def create_image(self, create_button):
create_button.click()
- return forms.FormRegion(self.driver, self.conf,
- field_mappings=self.CREATE_IMAGE_FORM_FIELDS)
+ return forms.FormRegionNG(self.driver, self.conf,
+ field_mappings=self.CREATE_IMAGE_FORM_FIELDS)
- @tables.bind_table_action('delete')
+ @tables.bind_table_action_ng('Delete Images')
def delete_image(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
- @tables.bind_row_action('create_volume_from_image')
- def create_volume(self, create_volume, row):
- create_volume.click()
- return forms.FormRegion(
- self.driver, self.conf,
- field_mappings=self.CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS)
-
- @tables.bind_row_action('launch_image')
- def launch_instance(self, launch_instance, row):
- launch_instance.click()
- return forms.TabbedFormRegion(
- self.driver, self.conf,
- field_mappings=self.LAUNCH_INSTANCE_FROM_FIELDS)
-
- @tables.bind_row_action('update_metadata')
- def update_metadata(self, metadata_button, row):
- metadata_button.click()
- return forms.MetadataFormRegion(self.driver, self.conf)
-
- @tables.bind_row_action('delete')
+ @tables.bind_row_action_ng('Delete Image')
def delete_image_via_row_action(self, delete_button, row):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
- @tables.bind_row_action('edit')
+ @tables.bind_row_action_ng('Edit Image')
def edit_image(self, edit_button, row):
edit_button.click()
- return forms.FormRegion(self.driver, self.conf,
- field_mappings=self.EDIT_IMAGE_FORM_FIELDS)
+ return forms.FormRegionNG(self.driver, self.conf,
+ field_mappings=self.EDIT_IMAGE_FORM_FIELDS)
+
+ @tables.bind_row_action_ng('Update Metadata')
+ def update_metadata(self, metadata_button, row):
+ metadata_button.click()
+ return forms.MetadataFormRegion(self.driver, self.conf)
- @tables.bind_row_anchor_column(IMAGES_TABLE_NAME_COLUMN)
+ @tables.bind_row_anchor_column_ng(IMAGES_TABLE_NAME_COLUMN)
def go_to_image_description_page(self, row_link, row):
row_link.click()
return forms.ItemTextDescription(self.driver, self.conf)
+ @tables.bind_row_action_ng('Create Volume')
+ def create_volume(self, create_volume, row):
+ create_volume.click()
+ return forms.FormRegion(
+ self.driver, self.conf,
+ field_mappings=self.CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS)
+
+ @tables.bind_row_action_ng('Launch')
+ def launch_instance(self, launch_instance, row):
+ launch_instance.click()
+ return forms.WizardFormRegion(
+ self.driver, self.conf, self.LAUNCH_INSTANCE_FORM_FIELDS)
+
class ImagesPage(basepage.BaseNavigationPage):
+ _resource_page_header_locator = (by.By.CSS_SELECTOR,
+ 'hz-resource-panel hz-page-header h1')
+ _default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
+ _search_field_locator = (by.By.CSS_SELECTOR,
+ 'magic-search.form-control input.search-input')
+ _search_button_locator = (by.By.CSS_SELECTOR,
+ 'hz-magic-search-bar span.fa-search')
+ _search_option_locator = (by.By.CSS_SELECTOR,
+ 'magic-search.form-control span.search-entry')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Images"
- def _get_row_with_image_name(self, name):
- return self.images_table.get_row(IMAGES_TABLE_NAME_COLUMN, name)
+ @property
+ def header(self):
+ return self._get_element(*self._resource_page_header_locator)
@property
def images_table(self):
return ImagesTable(self.driver, self.conf)
+ def wizard_getter(self):
+ return self._get_element(*self._default_form_locator)
+
+ def _get_row_with_image_name(self, name):
+ return self.images_table.get_row(IMAGES_TABLE_NAME_COLUMN, name)
+
def create_image(self, name, description=None,
image_source_type=DEFAULT_IMAGE_SOURCE,
location=None, image_file=None,
- image_format=DEFAULT_IMAGE_FORMAT,
- is_public=DEFAULT_ACCESSIBILITY,
- is_protected=DEFAULT_PROTECTION):
+ image_format=DEFAULT_IMAGE_FORMAT):
create_image_form = self.images_table.create_image()
create_image_form.name.text = name
if description is not None:
@@ -142,41 +155,27 @@ class ImagesPage(basepage.BaseNavigationPage):
create_image_form.image_url.text = location
else:
create_image_form.image_file.choose(image_file)
- create_image_form.disk_format.value = image_format
- if is_public:
- create_image_form.is_public.mark()
- if is_protected:
- create_image_form.protected.mark()
+ create_image_form.format.value = image_format
create_image_form.submit()
+ self.wait_till_element_disappears(self.wizard_getter)
def delete_image(self, name):
row = self._get_row_with_image_name(name)
row.mark()
confirm_delete_images_form = self.images_table.delete_image()
confirm_delete_images_form.submit()
+ self.wait_till_spinner_disappears()
- def add_custom_metadata(self, name, metadata):
- row = self._get_row_with_image_name(name)
- update_metadata_form = self.images_table.update_metadata(row)
- for field_name, value in metadata.items():
- update_metadata_form.add_custom_field(field_name, value)
- update_metadata_form.submit()
-
- def check_image_details(self, name, dict_with_details):
- row = self._get_row_with_image_name(name)
- matches = []
- description_page = self.images_table.go_to_image_description_page(row)
- content = description_page.get_content()
-
- for name, value in content.items():
- if name in dict_with_details:
- if dict_with_details[name] in value:
- matches.append(True)
- return matches
+ def delete_images(self, images_names):
+ for image_name in images_names:
+ self._get_row_with_image_name(image_name).mark()
+ confirm_delete_images_form = self.images_table.delete_image()
+ confirm_delete_images_form.submit()
+ self.wait_till_spinner_disappears()
def edit_image(self, name, new_name=None, description=None,
min_disk=None, min_ram=None,
- public=None, protected=None):
+ visibility=None, protected=None):
row = self._get_row_with_image_name(name)
confirm_edit_images_form = self.images_table.edit_image(row)
@@ -192,15 +191,17 @@ class ImagesPage(basepage.BaseNavigationPage):
if min_ram is not None:
confirm_edit_images_form.min_ram.value = min_ram
- if public is True:
- confirm_edit_images_form.public.mark()
- elif public is False:
- confirm_edit_images_form.public.unmark()
+ if visibility is not None:
+ if visibility is True:
+ confirm_edit_images_form.visibility.pick('Shared')
+ elif visibility is False:
+ confirm_edit_images_form.visibility.pick('Private')
- if protected is True:
- confirm_edit_images_form.protected.mark()
- elif protected is False:
- confirm_edit_images_form.protected.unmark()
+ if protected is not None:
+ if protected is True:
+ confirm_edit_images_form.protected.pick('Yes')
+ elif protected is False:
+ confirm_edit_images_form.protected.pick('No')
confirm_edit_images_form.submit()
@@ -209,6 +210,25 @@ class ImagesPage(basepage.BaseNavigationPage):
delete_image_form = self.images_table.delete_image_via_row_action(row)
delete_image_form.submit()
+ def add_custom_metadata(self, name, metadata):
+ row = self._get_row_with_image_name(name)
+ update_metadata_form = self.images_table.update_metadata(row)
+ for field_name, value in metadata.items():
+ update_metadata_form.add_custom_field(field_name, value)
+ update_metadata_form.submit()
+
+ def check_image_details(self, name, dict_with_details):
+ row = self._get_row_with_image_name(name)
+ matches = []
+ description_page = self.images_table.go_to_image_description_page(row)
+ content = description_page.get_content()
+
+ for name, value in content.items():
+ if name in dict_with_details:
+ if dict_with_details[name] in value:
+ matches.append(True)
+ return matches
+
def is_image_present(self, name):
return bool(self._get_row_with_image_name(name))
@@ -222,10 +242,27 @@ class ImagesPage(basepage.BaseNavigationPage):
def wait_until_image_active(self, name):
self._wait_until(lambda x: self.is_image_active(name))
+ def wait_until_image_present(self, name):
+ self._wait_until(lambda x: self.is_image_present(name))
+
def get_image_format(self, name):
row = self._get_row_with_image_name(name)
return row.cells[IMAGES_TABLE_FORMAT_COLUMN].text
+ def filter(self, value):
+ self._set_search_field(value)
+ self._click_search_btn()
+ self.driver.implicitly_wait(5)
+
+ def _set_search_field(self, value):
+ srch_field = self._get_element(*self._search_field_locator)
+ srch_field.clear()
+ srch_field.send_keys(value)
+
+ def _click_search_btn(self):
+ btn = self._get_element(*self._search_button_locator)
+ btn.click()
+
def create_volume_from_image(self, name, volume_name=None,
description=None,
volume_size=None):
@@ -236,32 +273,29 @@ class ImagesPage(basepage.BaseNavigationPage):
if description is not None:
create_volume_form.description.text = description
create_volume_form.image_source = name
- create_volume_form.size.value = volume_size if volume_size \
+ create_volume_form.volume_size.value = volume_size if volume_size \
else self.conf.volume.volume_size
create_volume_form.availability_zone.value = \
self.conf.launch_instances.available_zone
create_volume_form.submit()
- return VolumesPage(self.driver, self.conf)
def launch_instance_from_image(self, name, instance_name,
instance_count=1, flavor=None):
+ instance_page = InstancesPage(self.driver, self.conf)
row = self._get_row_with_image_name(name)
- launch_instance = self.images_table.launch_instance(row)
- launch_instance.availability_zone.value = \
+ instance_form = self.images_table.launch_instance(row)
+ instance_form.availability_zone.value = \
self.conf.launch_instances.available_zone
- launch_instance.name.text = instance_name
+ instance_form.name.text = instance_name
+ instance_form.count.value = instance_count
+ instance_form.switch_to(instance_page.SOURCE_STEP_INDEX)
+ instance_page.vol_delete_on_instance_delete_click()
+ instance_form.switch_to(instance_page.FLAVOR_STEP_INDEX)
if flavor is None:
flavor = self.conf.launch_instances.flavor
- launch_instance.flavor.text = flavor
- launch_instance.count.value = instance_count
- launch_instance.submit()
- return InstancesPage(self.driver, self.conf)
-
-
-class ImagesPageNG(ImagesPage):
- _resource_page_header_locator = (by.By.CSS_SELECTOR,
- 'hz-resource-panel hz-page-header h1')
-
- @property
- def header(self):
- return self._get_element(*self._resource_page_header_locator)
+ instance_form.flavor.transfer_available_resource(flavor)
+ instance_form.switch_to(instance_page.NETWORKS_STEP_INDEX)
+ instance_form.network.transfer_available_resource(
+ instance_page.DEFAULT_NETWORK_TYPE)
+ instance_form.submit()
+ instance_form.wait_till_wizard_disappears()
diff --git a/openstack_dashboard/test/integration_tests/regions/forms.py b/openstack_dashboard/test/integration_tests/regions/forms.py
index 2476bbf19..2df2c50f8 100644
--- a/openstack_dashboard/test/integration_tests/regions/forms.py
+++ b/openstack_dashboard/test/integration_tests/regions/forms.py
@@ -231,6 +231,22 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
self.driver.execute_script(js_cmd)
+class ButtonGroupFormFieldRegion(BaseFormFieldRegion):
+ """Select button group."""
+
+ _element_locator_str_suffix = 'div.btn-group'
+ _button_label_locator = (by.By.CSS_SELECTOR, 'label.btn')
+
+ @property
+ def options(self):
+ options = self._get_elements(*self._button_label_locator)
+ results = {opt.text: opt for opt in options}
+ return results
+
+ def pick(self, option):
+ return self.options[option].click()
+
+
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field."""
@@ -422,6 +438,13 @@ class FormRegion(BaseFormRegion):
return self._get_form_fields()
+class FormRegionNG(FormRegion):
+ """Angular-based form."""
+
+ _fields_locator = (by.By.CSS_SELECTOR, 'div.content')
+ _submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary.finish')
+
+
class TabbedFormRegion(FormRegion):
"""Forms that are divided with tabs.
diff --git a/openstack_dashboard/test/integration_tests/regions/tables.py b/openstack_dashboard/test/integration_tests/regions/tables.py
index 9778950b3..6d9334edd 100644
--- a/openstack_dashboard/test/integration_tests/regions/tables.py
+++ b/openstack_dashboard/test/integration_tests/regions/tables.py
@@ -45,6 +45,12 @@ class RowRegion(baseregion.BaseRegion):
chck_box.click()
+class RowRegionNG(RowRegion):
+ """Angular-based table row."""
+
+ _cell_locator = (by.By.CSS_SELECTOR, 'td > hz-cell')
+
+
class TableRegion(baseregion.BaseRegion):
"""Basic class representing table object."""
@@ -253,6 +259,38 @@ class TableRegion(baseregion.BaseRegion):
self.assertDictEqual(actual_table, expected_table_definition)
+class TableRegionNG(TableRegion):
+ """Basic class representing angular-based table object."""
+
+ _heading_locator = (by.By.CSS_SELECTOR,
+ 'hz-resource-panel hz-page-header h1')
+ _empty_table_locator = (by.By.CSS_SELECTOR, 'tbody > tr > td.no-rows-help')
+
+ def _table_locator(self, table_name):
+ return by.By.CSS_SELECTOR, 'hz-dynamic-table'
+
+ @property
+ def _next_locator(self):
+ return by.By.CSS_SELECTOR, 'a[ng-click^="selectPage(currentPage + 1)"]'
+
+ @property
+ def _prev_locator(self):
+ return by.By.CSS_SELECTOR, 'a[ng-click^="selectPage(currentPage - 1)"]'
+
+ @property
+ def column_names(self):
+ names = []
+ for element in self._get_elements(*self._columns_names_locator):
+ if element.text:
+ names.append(element.text)
+ return names
+
+ def _get_rows(self, *args):
+ return [RowRegionNG(self.driver, self.conf, elem, self.column_names)
+ for elem in self._get_elements(*self._rows_locator)
+ if elem.text and elem.text != '']
+
+
def bind_table_action(action_name):
"""Decorator to bind table region method to an actual table action button.
@@ -290,6 +328,44 @@ def bind_table_action(action_name):
return decorator
+def bind_table_action_ng(action_name):
+ """Decorator to bind table region method to an actual table action button.
+
+ This decorator works with angular-based tables.
+ Many table actions when started (by clicking a corresponding button
+ in UI) lead to some form showing up. To further interact with this form,
+ a Python/ Selenium wrapper needs to be created for it. It is very
+ convenient to return this newly created wrapper in the same method that
+ initiates clicking an actual table action button. Binding the method to a
+ button is performed behind the scenes in this decorator.
+
+ .. param:: action_name
+
+ Part of the action button id which is specific to action itself. It
+ is safe to use action `name` attribute from the dashboard tables.py
+ code.
+ """
+ _actions_locator = (by.By.CSS_SELECTOR,
+ 'actions.hz-dynamic-table-actions > action-list')
+
+ def decorator(method):
+ @functools.wraps(method)
+ def wrapper(table):
+ actions = table._get_elements(*_actions_locator)
+ action_element = None
+ for action in actions:
+ if action.text == action_name:
+ action_element = action
+ break
+ if action_element is None:
+ msg = "Could not bind method '%s' to action control '%s'" % (
+ method.__name__, action_name)
+ raise ValueError(msg)
+ return method(table, action_element)
+ return wrapper
+ return decorator
+
+
def bind_row_action(action_name):
"""A decorator to bind table region method to an actual row action button.
@@ -347,11 +423,67 @@ def bind_row_action(action_name):
return decorator
+def bind_row_action_ng(action_name):
+ """A decorator to bind table region method to an actual row action button.
+
+ This decorator works with angular-based tables.
+ Many table actions when started (by clicking a corresponding button
+ in UI) lead to some form showing up. To further interact with this form,
+ a Python/ Selenium wrapper needs to be created for it. It is very
+ convenient to return this newly created wrapper in the same method that
+ initiates clicking an actual action button. Row action could be
+ either primary (if its name is written right away on row action
+ button) or secondary (if its name is inside of a button drop-down). Binding
+ the method to a button and toggling the button drop-down open (in case
+ a row action is secondary) is performed behind the scenes in this
+ decorator.
+
+ .. param:: action_name
+
+ Part of the action button id which is specific to action itself. It
+ is safe to use action `name` attribute from the dashboard tables.py
+ code.
+ """
+ primary_action_locator = (
+ by.By.CSS_SELECTOR,
+ 'td.actions_column > actions > action-list > button.split-button')
+ secondary_actions_opener_locator = (
+ by.By.CSS_SELECTOR,
+ 'td.actions_column > actions > action-list > button.split-caret')
+ secondary_actions_locator = (
+ by.By.CSS_SELECTOR,
+ 'td.actions_column > actions > action-list > ul > li > a')
+
+ def decorator(method):
+ @functools.wraps(method)
+ def wrapper(table, row):
+ def find_action(element):
+ pattern = action_name
+ return element.text.endswith(pattern)
+
+ action_element = row._get_element(*primary_action_locator)
+ if not find_action(action_element):
+ action_element = None
+ row._get_element(*secondary_actions_opener_locator).click()
+ for element in row._get_elements(*secondary_actions_locator):
+ if find_action(element):
+ action_element = element
+ break
+
+ if action_element is None:
+ msg = "Could not bind method '%s' to action control '%s'" % (
+ method.__name__, action_name)
+ raise ValueError(msg)
+ return method(table, action_element, row)
+ return wrapper
+ return decorator
+
+
def bind_row_anchor_column(column_name):
"""A decorator to bind table region method to a anchor in a column.
- Typical examples of such tables are Project -> Compute -> Images, Admin
- -> System -> Flavors, Project -> Compute -> Instancies.
+ Typical examples of such tables are Project -> Compute -> Instances, Admin
+ -> System -> Flavors.
The method can be used to follow the link in the anchor by the click.
"""
@@ -365,3 +497,24 @@ def bind_row_anchor_column(column_name):
return wrapper
return decorator
+
+
+def bind_row_anchor_column_ng(column_name):
+ """A decorator to bind table region method to a anchor in a column.
+
+ This decorator works with angular-based tables.
+ Typical examples of such tables are Project -> Compute -> Images,
+ Admin -> Compute -> Images.
+ The method can be used to follow the link in the anchor by the click.
+ """
+
+ def decorator(method):
+ @functools.wraps(method)
+ def wrapper(table, row):
+ cell = row.cells[column_name]
+ action_element = cell.find_element(
+ by.By.CSS_SELECTOR, 'td > hz-cell > a')
+ return method(table, action_element, row)
+
+ return wrapper
+ return decorator
diff --git a/openstack_dashboard/test/integration_tests/tests/test_images.py b/openstack_dashboard/test/integration_tests/tests/test_images.py
index f2d9f109a..8065eaa92 100644
--- a/openstack_dashboard/test/integration_tests/tests/test_images.py
+++ b/openstack_dashboard/test/integration_tests/tests/test_images.py
@@ -11,14 +11,16 @@
# under the License.
import pytest
-from openstack_dashboard.test.integration_tests import decorators
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
+from openstack_dashboard.test.integration_tests.pages.project.\
+ compute.instancespage import InstancesPage
+from openstack_dashboard.test.integration_tests.pages.project.\
+ volumes.volumespage import VolumesPage
-@decorators.config_option_required('image.panel_type', 'legacy',
- message="Angular Panels not tested")
-class TestImagesLegacy(helpers.TestCase):
+
+class TestImagesBasicAngular(helpers.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.IMAGE_NAME = helpers.gen_random_resource_name("image")
@@ -27,30 +29,10 @@ class TestImagesLegacy(helpers.TestCase):
def images_page(self):
return self.home_pg.go_to_project_compute_imagespage()
-
-@decorators.config_option_required('image.panel_type', 'angular',
- message="Legacy Panels not tested")
-class TestImagesAngular(helpers.TestCase):
- @property
- def images_page(self):
- # FIXME(tsufiev): had to return angularized version of Images Page
- # object with the horrendous hack below because it's not so easy to
- # wire into the Navigation machinery and tell it to return an '*NG'
- # version of ImagesPage class if one adds '_ng' suffix to
- # 'go_to_compute_imagespage()' method. Yet that's how it should work
- # (or rewrite Navigation module completely).
- from openstack_dashboard.test.integration_tests.pages.project.\
- compute.imagespage import ImagesPageNG
- self.home_pg.go_to_project_compute_imagespage()
- return ImagesPageNG(self.driver, self.CONFIG)
-
def test_basic_image_browse(self):
images_page = self.images_page
self.assertEqual(images_page.header.text, 'Images')
-
-class TestImagesBasic(TestImagesLegacy):
- """Login as demo user"""
def image_create(self, local_file=None, **kwargs):
images_page = self.images_page
if local_file:
@@ -58,8 +40,10 @@ class TestImagesBasic(TestImagesLegacy):
image_file=local_file,
**kwargs)
else:
- images_page.create_image(self.IMAGE_NAME, **kwargs)
- self.assertTrue(images_page.find_message_and_dismiss(messages.INFO))
+ images_page.create_image(self.IMAGE_NAME,
+ image_source_type='url',
+ **kwargs)
+ self.assertTrue(images_page.find_message_and_dismiss(messages.SUCCESS))
self.assertFalse(images_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(images_page.is_image_present(self.IMAGE_NAME))
self.assertTrue(images_page.is_image_active(self.IMAGE_NAME))
@@ -72,30 +56,30 @@ class TestImagesBasic(TestImagesLegacy):
self.assertFalse(images_page.find_message_and_dismiss(messages.ERROR))
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
- @pytest.mark.skip(reason="Bug 1595335")
- def test_image_create_delete(self):
+ def test_image_create_delete_from_local_file(self):
"""tests the image creation and deletion functionalities:
- * creates a new image from horizon.conf http_image
+ * creates a new image from a generated file
* verifies the image appears in the images table as active
* deletes the newly created image
* verifies the image does not appear in the table after deletion
"""
- self.image_create()
- self.image_delete(self.IMAGE_NAME)
+ with helpers.gen_temporary_file() as file_name:
+ self.image_create(local_file=file_name)
+ self.image_delete(self.IMAGE_NAME)
- def test_image_create_delete_from_local_file(self):
+ # Run when Glance configuration and policies allow setting locations.
+ @pytest.mark.skip(reason="IMAGES_ALLOW_LOCATION = False")
+ def test_image_create_delete_from_url(self):
"""tests the image creation and deletion functionalities:
- * downloads image from horizon.conf stated in http_image
- * creates the image from the downloaded file
+ * creates a new image from horizon.conf http_image
* verifies the image appears in the images table as active
* deletes the newly created image
* verifies the image does not appear in the table after deletion
"""
- with helpers.gen_temporary_file() as file_name:
- self.image_create(local_file=file_name)
- self.image_delete(self.IMAGE_NAME)
+ self.image_create()
+ self.image_delete(self.IMAGE_NAME)
def test_images_pagination(self):
"""This test checks images pagination
@@ -115,23 +99,51 @@ class TestImagesBasic(TestImagesLegacy):
9) Click 'Prev' and check results (should be the same as for step5)
10) Go to user settings page and restore 'Items Per Page'
"""
+
default_image_list = self.CONFIG.image.images_list
+
+ images_page = self.images_page
+
+ # delete any old images except default ones
+ images_page.wait_until_image_present(default_image_list[0])
+ image_list = images_page.images_table.get_column_data(
+ name_column='Name')
+ garbage = [i for i in image_list if i not in default_image_list]
+ if garbage:
+ images_page.delete_images(garbage)
+ self.assertTrue(
+ images_page.find_message_and_dismiss(messages.SUCCESS))
+
items_per_page = 1
+ images_count = 2
+ images_names = ["{0}_{1}".format(self.IMAGE_NAME, item)
+ for item in range(images_count)]
+ for image_name in images_names:
+ with helpers.gen_temporary_file() as file_name:
+ images_page.create_image(image_name, image_file=file_name)
+ self.assertTrue(
+ images_page.find_message_and_dismiss(messages.SUCCESS))
+ self.assertFalse(
+ images_page.find_message_and_dismiss(messages.ERROR))
+ self.assertTrue(images_page.is_image_present(image_name))
+
first_page_definition = {'Next': True, 'Prev': False,
'Count': items_per_page,
'Names': [default_image_list[0]]}
second_page_definition = {'Next': True, 'Prev': True,
'Count': items_per_page,
- 'Names': [default_image_list[1]]}
+ 'Names': [images_names[0]]}
third_page_definition = {'Next': False, 'Prev': True,
'Count': items_per_page,
- 'Names': [default_image_list[2]]}
+ 'Names': [images_names[1]]}
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(items_per_page)
settings_page.find_message_and_dismiss(messages.SUCCESS)
images_page = self.images_page
+ if not images_page.is_image_present(default_image_list[0]):
+ images_page.wait_until_image_present(default_image_list[0])
images_page.images_table.assert_definition(first_page_definition)
images_page.images_table.turn_next_page()
@@ -150,6 +162,20 @@ class TestImagesBasic(TestImagesLegacy):
settings_page.change_pagesize()
settings_page.find_message_and_dismiss(messages.SUCCESS)
+ images_page = self.images_page
+ images_page.wait_until_image_present(default_image_list[0])
+ images_page.delete_images(images_names)
+ self.assertTrue(images_page.find_message_and_dismiss(messages.SUCCESS))
+ self.assertFalse(images_page.find_message_and_dismiss(messages.ERROR))
+
+
+class TestImagesAdminAngular(helpers.AdminTestCase, TestImagesBasicAngular):
+ """Login as admin user"""
+
+ @property
+ def images_page(self):
+ return self.home_pg.go_to_admin_compute_imagespage()
+
def test_update_image_metadata(self):
"""Test update image metadata
@@ -168,9 +194,6 @@ class TestImagesBasic(TestImagesLegacy):
'metadata2': helpers.gen_random_resource_name("value")}
with helpers.gen_temporary_file() as file_name:
- # TODO(tsufiev): had to add non-empty description to an image,
- # because description is now considered a metadata and we want
- # the metadata in a newly created image to be valid
images_page = self.image_create(local_file=file_name,
description='test description')
images_page.add_custom_metadata(self.IMAGE_NAME, new_metadata)
@@ -203,21 +226,24 @@ class TestImagesBasic(TestImagesLegacy):
# Check that Delete action is not available in the action list.
# The below action will generate exception since the bind fails.
# But only ValueError with message below is expected here.
- with self.assertRaisesRegex(ValueError, 'Could not bind method'):
+ message = "Could not bind method 'delete_image_via_row_action' " \
+ "to action control 'Delete Image'"
+ with self.assertRaisesRegex(ValueError, message):
images_page.delete_image_via_row_action(self.IMAGE_NAME)
- # Try to delete image. That should not be possible now.
- images_page.delete_image(self.IMAGE_NAME)
- self.assertFalse(
- images_page.find_message_and_dismiss(messages.SUCCESS))
- self.assertTrue(
- images_page.find_message_and_dismiss(messages.ERROR))
- self.assertTrue(images_page.is_image_present(self.IMAGE_NAME))
+ # Edit image to make it not protected again and delete it.
+ images_page = self.images_page
images_page.edit_image(self.IMAGE_NAME, protected=False)
self.assertTrue(
images_page.find_message_and_dismiss(messages.SUCCESS))
+ self.assertFalse(
+ images_page.find_message_and_dismiss(messages.ERROR))
+
self.image_delete(self.IMAGE_NAME)
+ self.assertFalse(
+ images_page.find_message_and_dismiss(messages.ERROR))
+ self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
def test_edit_image_description_and_name(self):
"""tests that image description is editable
@@ -264,9 +290,53 @@ class TestImagesBasic(TestImagesLegacy):
self.assertSequenceTrue(results)
self.image_delete(new_image_name)
+ self.assertFalse(
+ images_page.find_message_and_dismiss(messages.ERROR))
+ self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
+ def test_filter_images(self):
+ """This test checks filtering of images
+
+ Steps:
+ 1) Login to Horizon dashboard as admin user
+ 2) Go to Admin -> Compute -> Images
+ 3) Use filter by Image Name
+ 4) Check that filtered table has one image only (which name is
+ equal to filter value)
+ 5) Check that no other images in the table
+ 6) Clear filter and set nonexistent image name. Check that 0 rows
+ are displayed
+ """
+ default_image_list = self.CONFIG.image.images_list
+ images_page = self.images_page
+
+ images_page.filter(default_image_list[0])
+ self.assertTrue(images_page.is_image_present(default_image_list[0]))
+ for image in default_image_list[1:]:
+ self.assertFalse(images_page.is_image_present(image))
+
+ images_page = self.images_page
+ nonexistent_image_name = "{0}_test".format(self.IMAGE_NAME)
+ images_page.filter(nonexistent_image_name)
+ self.assertEqual(images_page.images_table.rows, [])
+
+ images_page.filter('')
+
+
+class TestImagesAdvancedAngular(helpers.TestCase):
+
+ @property
+ def images_page(self):
+ return self.home_pg.go_to_project_compute_imagespage()
+
+ def volumes_page(self):
+ self.home_pg.go_to_project_volumes_volumespage()
+ return VolumesPage(self.driver, self.CONFIG)
+
+ def instances_page(self):
+ self.home_pg.go_to_project_compute_instancespage()
+ return InstancesPage(self.driver, self.CONFIG)
-class TestImagesAdvanced(TestImagesLegacy):
"""Login as demo user"""
def test_create_volume_from_image(self):
"""This test case checks create volume from image functionality:
@@ -282,18 +352,23 @@ class TestImagesAdvanced(TestImagesLegacy):
source_image = self.CONFIG.image.images_list[0]
target_volume = "created_from_{0}".format(source_image)
- volumes_page = images_page.create_volume_from_image(
+ images_page.create_volume_from_image(
source_image, volume_name=target_volume)
self.assertTrue(
- volumes_page.find_message_and_dismiss(messages.INFO))
+ images_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(
- volumes_page.find_message_and_dismiss(messages.ERROR))
+ images_page.find_message_and_dismiss(messages.ERROR))
+
+ volumes_page = self.volumes_page()
+
self.assertTrue(volumes_page.is_volume_present(target_volume))
self.assertTrue(volumes_page.is_volume_status(target_volume,
'Available'))
volumes_page.delete_volume(target_volume)
volumes_page.find_message_and_dismiss(messages.SUCCESS)
volumes_page.find_message_and_dismiss(messages.ERROR)
+
+ volumes_page = self.volumes_page()
self.assertTrue(volumes_page.is_volume_deleted(target_volume))
def test_launch_instance_from_image(self):
@@ -310,56 +385,22 @@ class TestImagesAdvanced(TestImagesLegacy):
images_page = self.images_page
source_image = self.CONFIG.image.images_list[0]
target_instance = "created_from_{0}".format(source_image)
- instances_page = images_page.launch_instance_from_image(
- source_image, target_instance)
+
+ images_page.launch_instance_from_image(source_image, target_instance)
self.assertTrue(
- instances_page.find_message_and_dismiss(messages.SUCCESS))
+ images_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(
- instances_page.find_message_and_dismiss(messages.ERROR))
+ images_page.find_message_and_dismiss(messages.ERROR))
+
+ instances_page = self.instances_page()
self.assertTrue(instances_page.is_instance_active(target_instance))
+ instances_page = self.instances_page()
actual_image_name = instances_page.get_image_name(target_instance)
self.assertEqual(source_image, actual_image_name)
instances_page.delete_instance(target_instance)
self.assertTrue(
- instances_page.find_message_and_dismiss(messages.SUCCESS))
+ 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(target_instance))
-
-
-class TestImagesAdmin(helpers.AdminTestCase, TestImagesLegacy):
- """Login as admin user"""
- @property
- def images_page(self):
- return self.home_pg.go_to_admin_compute_imagespage()
-
- def test_image_create_delete(self):
- super().test_image_create_delete()
-
- def test_filter_images(self):
- """This test checks filtering of images
-
- Steps:
- 1) Login to Horizon dashboard as admin user
- 2) Go to Admin -> Compute -> Images
- 3) Use filter by Image Name
- 4) Check that filtered table has one image only (which name is
- equal to filter value)
- 5) Check that no other images in the table
- 6) Clear filter and set nonexistent image name. Check that 0 rows
- are displayed
- """
- images_list = self.CONFIG.image.images_list
- images_page = self.images_page
-
- images_page.images_table.filter(images_list[0])
- self.assertTrue(images_page.is_image_present(images_list[0]))
- for image in images_list[1:]:
- self.assertFalse(images_page.is_image_present(image))
-
- nonexistent_image_name = "{0}_test".format(self.IMAGE_NAME)
- images_page.images_table.filter(nonexistent_image_name)
- self.assertEqual(images_page.images_table.rows, [])
-
- images_page.images_table.filter('')
diff --git a/openstack_dashboard/test/integration_tests/tests/test_volumes.py b/openstack_dashboard/test/integration_tests/tests/test_volumes.py
index 92c88b66b..28bd3dfd0 100644
--- a/openstack_dashboard/test/integration_tests/tests/test_volumes.py
+++ b/openstack_dashboard/test/integration_tests/tests/test_volumes.py
@@ -241,6 +241,10 @@ class TestVolumesActions(helpers.TestCase):
def volumes_page(self):
return self.home_pg.go_to_project_volumes_volumespage()
+ @property
+ def images_page(self):
+ return self.home_pg.go_to_project_compute_imagespage()
+
def setUp(self):
super().setUp()
volumes_page = self.volumes_page
@@ -287,7 +291,6 @@ class TestVolumesActions(helpers.TestCase):
new_size = volumes_page.get_size(self.VOLUME_NAME)
self.assertLess(orig_size, new_size)
- @pytest.mark.skip(reason="Bug 1847715")
def test_volume_upload_to_image(self):
"""This test case checks upload volume to image functionality:
@@ -298,29 +301,28 @@ class TestVolumesActions(helpers.TestCase):
4. Delete the image
5. Repeat actions for all disk formats
"""
- self.volumes_page = self.home_pg.go_to_project_volumes_volumespage()
- all_formats = {"qcow2": 'QCOW2', "raw": 'Raw', "vdi": 'VDI',
+ volumes_page = self.volumes_page
+ all_formats = {"qcow2": 'QCOW2', "raw": 'RAW', "vdi": 'VDI',
"vmdk": 'VMDK'}
for disk_format in all_formats:
- self.volumes_page.upload_volume_to_image(self.VOLUME_NAME,
- self.IMAGE_NAME,
- disk_format)
+ volumes_page.upload_volume_to_image(
+ self.VOLUME_NAME, self.IMAGE_NAME, disk_format)
self.assertFalse(
- self.volumes_page.find_message_and_dismiss(messages.ERROR))
- self.assertTrue(self.volumes_page.is_volume_status(
+ volumes_page.find_message_and_dismiss(messages.ERROR))
+ self.assertTrue(volumes_page.is_volume_status(
self.VOLUME_NAME, 'Available'))
- images_page = self.home_pg.go_to_project_compute_imagespage()
+ images_page = self.images_page
self.assertTrue(images_page.is_image_present(self.IMAGE_NAME))
self.assertTrue(images_page.is_image_active(self.IMAGE_NAME))
self.assertEqual(images_page.get_image_format(self.IMAGE_NAME),
all_formats[disk_format])
images_page.delete_image(self.IMAGE_NAME)
self.assertTrue(images_page.find_message_and_dismiss(
- messages.INFO))
+ messages.SUCCESS))
self.assertFalse(images_page.find_message_and_dismiss(
messages.ERROR))
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
- self.volumes_page = \
+ volumes_page = \
self.home_pg.go_to_project_volumes_volumespage()
@pytest.mark.skip(reason="Bug 1930420")