Merge "Integration test navigation machinery for Angular pages"
This commit is contained in:
commit
535171fbf9
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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')
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,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('')
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user