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 4743c6c748)
This commit is contained in:
Tatiana Ovchinnikova 2021-09-10 15:24:51 -05:00
parent 98685ed5b7
commit df4037bd81
10 changed files with 496 additions and 249 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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')
]

View File

@ -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)

View File

@ -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")
"name", "description",
"volume_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")
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')
@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_ng('Edit Image')
def edit_image(self, edit_button, row):
edit_button.click()
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_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('launch_image')
@tables.bind_row_action_ng('Launch')
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')
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')
def edit_image(self, edit_button, row):
edit_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.EDIT_IMAGE_FORM_FIELDS)
@tables.bind_row_anchor_column(IMAGES_TABLE_NAME_COLUMN)
def go_to_image_description_page(self, row_link, row):
row_link.click()
return forms.ItemTextDescription(self.driver, self.conf)
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,18 +155,60 @@ 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 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,
visibility=None, protected=None):
row = self._get_row_with_image_name(name)
confirm_edit_images_form = self.images_table.edit_image(row)
if new_name is not None:
confirm_edit_images_form.name.text = new_name
if description is not None:
confirm_edit_images_form.description.text = description
if min_disk is not None:
confirm_edit_images_form.min_disk.value = min_disk
if min_ram is not None:
confirm_edit_images_form.min_ram.value = min_ram
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 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()
def delete_image_via_row_action(self, name):
row = self._get_row_with_image_name(name)
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)
@ -174,41 +229,6 @@ class ImagesPage(basepage.BaseNavigationPage):
matches.append(True)
return matches
def edit_image(self, name, new_name=None, description=None,
min_disk=None, min_ram=None,
public=None, protected=None):
row = self._get_row_with_image_name(name)
confirm_edit_images_form = self.images_table.edit_image(row)
if new_name is not None:
confirm_edit_images_form.name.text = new_name
if description is not None:
confirm_edit_images_form.description.text = description
if min_disk is not None:
confirm_edit_images_form.min_disk.value = min_disk
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 protected is True:
confirm_edit_images_form.protected.mark()
elif protected is False:
confirm_edit_images_form.protected.unmark()
confirm_edit_images_form.submit()
def delete_image_via_row_action(self, name):
row = self._get_row_with_image_name(name)
delete_image_form = self.images_table.delete_image_via_row_action(row)
delete_image_form.submit()
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()

View File

@ -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.

View File

@ -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

View File

@ -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,8 +56,21 @@ 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 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
"""
with helpers.gen_temporary_file() as file_name:
self.image_create(local_file=file_name)
self.image_delete(self.IMAGE_NAME)
# 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:
* creates a new image from horizon.conf http_image
@ -84,19 +81,6 @@ class TestImagesBasic(TestImagesLegacy):
self.image_create()
self.image_delete(self.IMAGE_NAME)
def test_image_create_delete_from_local_file(self):
"""tests the image creation and deletion functionalities:
* downloads image from horizon.conf stated in http_image
* creates the image from the downloaded 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
"""
with helpers.gen_temporary_file() as file_name:
self.image_create(local_file=file_name)
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 TestImagesAdvanced(TestImagesLegacy):
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)
"""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,57 +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()
@pytest.mark.skip(reason="Bug 1774697")
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('')

View File

@ -242,6 +242,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
@ -288,7 +292,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:
@ -299,29 +302,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 1774697")