summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTatiana Ovchinnikova <t.v.ovtchinnikova@gmail.com>2021-09-10 15:24:51 -0500
committerTatiana Ovchinnikova <t.v.ovtchinnikova@gmail.com>2023-01-18 17:44:10 +0000
commitf5ca3b202f597918da61dc8694a1ea0d6ef70740 (patch)
tree3ad04f6c331b9a9259c04236a8f039ecfe321c9f
parent4cc580d94b1e579c307583d2f8eb2a5cb699b14f (diff)
downloadhorizon-f5ca3b202f597918da61dc8694a1ea0d6ef70740.tar.gz
Integration test navigation machinery for Angular pages
There's an implementation of navigation modules for pages written in Angular. Legacy tests for images page are rewritten to work with the new navigation. Change-Id: Ib8a40f0adb0a46c9d646c6796a4242988eefa6e3 (cherry picked from commit 4743c6c748be79c3a80a350d05aeed7bbe63c577)
-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 595e23d97..5c57f7482 100644
--- a/horizon/test/webdriver.py
+++ b/horizon/test/webdriver.py
@@ -40,7 +40,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)
@@ -51,7 +51,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 002f502a5..3d8727b70 100644
--- a/openstack_dashboard/test/integration_tests/tests/test_volumes.py
+++ b/openstack_dashboard/test/integration_tests/tests/test_volumes.py
@@ -250,6 +250,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
@@ -296,7 +300,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:
@@ -307,29 +310,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")