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."""
|
"""Mixin for overriding find_element methods."""
|
||||||
|
|
||||||
def find_element(self, by=by.By.ID, value=None):
|
def find_element(self, by=by.By.ID, value=None):
|
||||||
repeat = range(2)
|
repeat = range(10)
|
||||||
for i in repeat:
|
for i in repeat:
|
||||||
try:
|
try:
|
||||||
web_el = super().find_element(by, value)
|
web_el = super().find_element(by, value)
|
||||||
@ -51,7 +51,7 @@ class WrapperFindOverride(object):
|
|||||||
self)
|
self)
|
||||||
|
|
||||||
def find_elements(self, by=by.By.ID, value=None):
|
def find_elements(self, by=by.By.ID, value=None):
|
||||||
repeat = range(2)
|
repeat = range(10)
|
||||||
for i in repeat:
|
for i in repeat:
|
||||||
try:
|
try:
|
||||||
web_els = super().find_elements(by, value)
|
web_els = super().find_elements(by, value)
|
||||||
|
@ -76,15 +76,9 @@
|
|||||||
<label class="control-label" for="imageForm-image_url">
|
<label class="control-label" for="imageForm-image_url">
|
||||||
<translate>File</translate><span class="hz-icon-required fa fa-asterisk"></span>
|
<translate>File</translate><span class="hz-icon-required fa fa-asterisk"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="input-group" ng-hide="ctrl.uploadProgress > -1">
|
<div ng-hide="ctrl.uploadProgress > -1">
|
||||||
<span class="input-group-btn">
|
<input type="file" ng-model="image_file" ngf-select="ctrl.prepareUpload(image_file)"
|
||||||
<button class="btn btn-primary" ng-model="image_file"
|
name="image_file" ng-required="true" ng-disabled="viewModel.isSubmitting">
|
||||||
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>
|
</div>
|
||||||
<div ng-hide="ctrl.uploadProgress < 0" class="progress-text">
|
<div ng-hide="ctrl.uploadProgress < 0" class="progress-text">
|
||||||
<uib-progressbar value="ctrl.uploadProgress"></uib-progressbar>
|
<uib-progressbar value="ctrl.uploadProgress"></uib-progressbar>
|
||||||
@ -239,7 +233,7 @@
|
|||||||
<translate>Visibility</translate>
|
<translate>Visibility</translate>
|
||||||
</label>
|
</label>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="btn-group">
|
<div class="btn-group" name="visibility">
|
||||||
<label class="btn btn-default"
|
<label class="btn btn-default"
|
||||||
ng-repeat="option in ctrl.imageVisibilityOptions"
|
ng-repeat="option in ctrl.imageVisibilityOptions"
|
||||||
ng-model="ctrl.image.visibility"
|
ng-model="ctrl.image.visibility"
|
||||||
@ -254,7 +248,7 @@
|
|||||||
<translate>Protected</translate>
|
<translate>Protected</translate>
|
||||||
</label>
|
</label>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="btn-group">
|
<div class="btn-group" name="protected">
|
||||||
<label class="btn btn-default"
|
<label class="btn btn-default"
|
||||||
ng-repeat="option in ctrl.imageProtectedOptions"
|
ng-repeat="option in ctrl.imageProtectedOptions"
|
||||||
ng-model="ctrl.image.protected"
|
ng-model="ctrl.image.protected"
|
||||||
|
@ -126,7 +126,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label required" translate>Visibility</label>
|
<label class="control-label required" translate>Visibility</label>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="btn-group">
|
<div class="btn-group" name="visibility">
|
||||||
<label class="btn btn-default"
|
<label class="btn btn-default"
|
||||||
ng-repeat="option in ctrl.imageVisibilityOptions"
|
ng-repeat="option in ctrl.imageVisibilityOptions"
|
||||||
ng-model="ctrl.image.visibility"
|
ng-model="ctrl.image.visibility"
|
||||||
@ -139,7 +139,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label required" translate>Protected</label>
|
<label class="control-label required" translate>Protected</label>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<div class="btn-group">
|
<div class="btn-group" name="protected">
|
||||||
<label class="btn btn-default"
|
<label class="btn btn-default"
|
||||||
ng-repeat="option in ctrl.imageProtectedOptions"
|
ng-repeat="option in ctrl.imageProtectedOptions"
|
||||||
ng-model="ctrl.image.protected"
|
ng-model="ctrl.image.protected"
|
||||||
|
@ -75,11 +75,11 @@ ImageGroup = [
|
|||||||
default='angular',
|
default='angular',
|
||||||
help='type/version of images panel'),
|
help='type/version of images panel'),
|
||||||
cfg.StrOpt('http_image',
|
cfg.StrOpt('http_image',
|
||||||
default='http://download.cirros-cloud.net/0.3.1/'
|
default='http://download.cirros-cloud.net/0.5.2/'
|
||||||
'cirros-0.3.1-x86_64-uec.tar.gz',
|
'cirros-0.5.2-x86_64-uec.tar.gz',
|
||||||
help='http accessible image'),
|
help='http accessible image'),
|
||||||
cfg.ListOpt('images_list',
|
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')
|
help='default list of images')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -43,8 +43,9 @@ panel_type=legacy
|
|||||||
[image]
|
[image]
|
||||||
# http accessible image (string value)
|
# http accessible image (string value)
|
||||||
panel_type=angular
|
panel_type=angular
|
||||||
http_image=http://download.cirros-cloud.net/0.3.1/cirros-0.3.1-x86_64-uec.tar.gz
|
http_image=http://download.cirros-cloud.net/0.5.2/cirros-0.5.2-x86_64-uec.tar.gz
|
||||||
images_list=cirros-0.3.5-x86_64-disk
|
images_list=cirros-0.5.2-x86_64-disk
|
||||||
|
|
||||||
|
|
||||||
[identity]
|
[identity]
|
||||||
# Username to use for non-admin API requests. (string value)
|
# 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.pages import basepage
|
||||||
from openstack_dashboard.test.integration_tests.regions import forms
|
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.regions import tables
|
||||||
|
|
||||||
from openstack_dashboard.test.integration_tests.pages.project.compute.\
|
from openstack_dashboard.test.integration_tests.pages.project.compute.\
|
||||||
instancespage import InstancesPage
|
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
|
# TODO(bpokorny): Set the default source back to 'url' once Glance removes
|
||||||
# the show_multiple_locations option, and if the default devstack policies
|
# the show_multiple_locations option, and if the default devstack policies
|
||||||
# allow setting locations.
|
# allow setting locations.
|
||||||
DEFAULT_IMAGE_SOURCE = 'file'
|
DEFAULT_IMAGE_SOURCE = 'file'
|
||||||
DEFAULT_IMAGE_FORMAT = 'qcow2'
|
DEFAULT_IMAGE_FORMAT = 'string:raw'
|
||||||
DEFAULT_ACCESSIBILITY = False
|
DEFAULT_ACCESSIBILITY = False
|
||||||
DEFAULT_PROTECTION = False
|
DEFAULT_PROTECTION = False
|
||||||
IMAGES_TABLE_NAME_COLUMN = 'name'
|
IMAGES_TABLE_NAME_COLUMN = 'Name'
|
||||||
IMAGES_TABLE_STATUS_COLUMN = 'status'
|
IMAGES_TABLE_STATUS_COLUMN = 'Status'
|
||||||
IMAGES_TABLE_FORMAT_COLUMN = 'disk_format'
|
IMAGES_TABLE_FORMAT_COLUMN = 'Disk Format'
|
||||||
|
|
||||||
|
|
||||||
class ImagesTable(tables.TableRegion):
|
class ImagesTable(tables.TableRegionNG):
|
||||||
name = "images"
|
name = "images"
|
||||||
|
|
||||||
CREATE_IMAGE_FORM_FIELDS = (
|
CREATE_IMAGE_FORM_FIELDS = (
|
||||||
"name", "description", "image_file", "kernel", "ramdisk",
|
"name", "description", "image_file", "kernel", "ramdisk", "format",
|
||||||
"disk_format", "architecture", "min_disk", "min_ram",
|
"architecture", "min_disk", "min_ram", "visibility", "protected"
|
||||||
"is_public", "protected"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
|
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
|
||||||
"name", "description", "image_source",
|
"name", "description",
|
||||||
"type", "size", "availability_zone")
|
"volume_size",
|
||||||
|
"availability_zone")
|
||||||
|
|
||||||
LAUNCH_INSTANCE_FROM_FIELDS = ((
|
LAUNCH_INSTANCE_FORM_FIELDS = (
|
||||||
"availability_zone", "name", "flavor",
|
("name", "count", "availability_zone"),
|
||||||
"count", "source_type", "instance_snapshot_id",
|
("boot_source_type", "volume_size"),
|
||||||
"volume_id", "volume_snapshot_id", "image_id", "volume_size",
|
{
|
||||||
"vol_delete_on_instance_delete"),
|
'flavor': menus.InstanceFlavorMenuRegion
|
||||||
("keypair", "groups"),
|
},
|
||||||
("script_source", "script_upload", "script_data"),
|
{
|
||||||
("disk_config", "config_drive")
|
'network': menus.InstanceAvailableResourceMenuRegion
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
EDIT_IMAGE_FORM_FIELDS = (
|
EDIT_IMAGE_FORM_FIELDS = (
|
||||||
"name", "description", "disk_format", "min_disk",
|
"name", "description", "format", "min_disk",
|
||||||
"min_ram", "public", "protected"
|
"min_ram", "visibility", "protected"
|
||||||
)
|
)
|
||||||
|
|
||||||
@tables.bind_table_action('create')
|
@tables.bind_table_action_ng('Create Image')
|
||||||
def create_image(self, create_button):
|
def create_image(self, create_button):
|
||||||
create_button.click()
|
create_button.click()
|
||||||
return forms.FormRegion(self.driver, self.conf,
|
return forms.FormRegionNG(self.driver, self.conf,
|
||||||
field_mappings=self.CREATE_IMAGE_FORM_FIELDS)
|
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):
|
def delete_image(self, delete_button):
|
||||||
delete_button.click()
|
delete_button.click()
|
||||||
return forms.BaseFormRegion(self.driver, self.conf)
|
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):
|
def create_volume(self, create_volume, row):
|
||||||
create_volume.click()
|
create_volume.click()
|
||||||
return forms.FormRegion(
|
return forms.FormRegion(
|
||||||
self.driver, self.conf,
|
self.driver, self.conf,
|
||||||
field_mappings=self.CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS)
|
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):
|
def launch_instance(self, launch_instance, row):
|
||||||
launch_instance.click()
|
launch_instance.click()
|
||||||
return forms.TabbedFormRegion(
|
return forms.WizardFormRegion(
|
||||||
self.driver, self.conf,
|
self.driver, self.conf, self.LAUNCH_INSTANCE_FORM_FIELDS)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class ImagesPage(basepage.BaseNavigationPage):
|
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):
|
def __init__(self, driver, conf):
|
||||||
super().__init__(driver, conf)
|
super().__init__(driver, conf)
|
||||||
self._page_title = "Images"
|
self._page_title = "Images"
|
||||||
|
|
||||||
def _get_row_with_image_name(self, name):
|
@property
|
||||||
return self.images_table.get_row(IMAGES_TABLE_NAME_COLUMN, name)
|
def header(self):
|
||||||
|
return self._get_element(*self._resource_page_header_locator)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def images_table(self):
|
def images_table(self):
|
||||||
return ImagesTable(self.driver, self.conf)
|
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,
|
def create_image(self, name, description=None,
|
||||||
image_source_type=DEFAULT_IMAGE_SOURCE,
|
image_source_type=DEFAULT_IMAGE_SOURCE,
|
||||||
location=None, image_file=None,
|
location=None, image_file=None,
|
||||||
image_format=DEFAULT_IMAGE_FORMAT,
|
image_format=DEFAULT_IMAGE_FORMAT):
|
||||||
is_public=DEFAULT_ACCESSIBILITY,
|
|
||||||
is_protected=DEFAULT_PROTECTION):
|
|
||||||
create_image_form = self.images_table.create_image()
|
create_image_form = self.images_table.create_image()
|
||||||
create_image_form.name.text = name
|
create_image_form.name.text = name
|
||||||
if description is not None:
|
if description is not None:
|
||||||
@ -142,18 +155,60 @@ class ImagesPage(basepage.BaseNavigationPage):
|
|||||||
create_image_form.image_url.text = location
|
create_image_form.image_url.text = location
|
||||||
else:
|
else:
|
||||||
create_image_form.image_file.choose(image_file)
|
create_image_form.image_file.choose(image_file)
|
||||||
create_image_form.disk_format.value = image_format
|
create_image_form.format.value = image_format
|
||||||
if is_public:
|
|
||||||
create_image_form.is_public.mark()
|
|
||||||
if is_protected:
|
|
||||||
create_image_form.protected.mark()
|
|
||||||
create_image_form.submit()
|
create_image_form.submit()
|
||||||
|
self.wait_till_element_disappears(self.wizard_getter)
|
||||||
|
|
||||||
def delete_image(self, name):
|
def delete_image(self, name):
|
||||||
row = self._get_row_with_image_name(name)
|
row = self._get_row_with_image_name(name)
|
||||||
row.mark()
|
row.mark()
|
||||||
confirm_delete_images_form = self.images_table.delete_image()
|
confirm_delete_images_form = self.images_table.delete_image()
|
||||||
confirm_delete_images_form.submit()
|
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):
|
def add_custom_metadata(self, name, metadata):
|
||||||
row = self._get_row_with_image_name(name)
|
row = self._get_row_with_image_name(name)
|
||||||
@ -174,41 +229,6 @@ class ImagesPage(basepage.BaseNavigationPage):
|
|||||||
matches.append(True)
|
matches.append(True)
|
||||||
return matches
|
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):
|
def is_image_present(self, name):
|
||||||
return bool(self._get_row_with_image_name(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):
|
def wait_until_image_active(self, name):
|
||||||
self._wait_until(lambda x: self.is_image_active(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):
|
def get_image_format(self, name):
|
||||||
row = self._get_row_with_image_name(name)
|
row = self._get_row_with_image_name(name)
|
||||||
return row.cells[IMAGES_TABLE_FORMAT_COLUMN].text
|
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,
|
def create_volume_from_image(self, name, volume_name=None,
|
||||||
description=None,
|
description=None,
|
||||||
volume_size=None):
|
volume_size=None):
|
||||||
@ -236,32 +273,29 @@ class ImagesPage(basepage.BaseNavigationPage):
|
|||||||
if description is not None:
|
if description is not None:
|
||||||
create_volume_form.description.text = description
|
create_volume_form.description.text = description
|
||||||
create_volume_form.image_source = name
|
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
|
else self.conf.volume.volume_size
|
||||||
create_volume_form.availability_zone.value = \
|
create_volume_form.availability_zone.value = \
|
||||||
self.conf.launch_instances.available_zone
|
self.conf.launch_instances.available_zone
|
||||||
create_volume_form.submit()
|
create_volume_form.submit()
|
||||||
return VolumesPage(self.driver, self.conf)
|
|
||||||
|
|
||||||
def launch_instance_from_image(self, name, instance_name,
|
def launch_instance_from_image(self, name, instance_name,
|
||||||
instance_count=1, flavor=None):
|
instance_count=1, flavor=None):
|
||||||
|
instance_page = InstancesPage(self.driver, self.conf)
|
||||||
row = self._get_row_with_image_name(name)
|
row = self._get_row_with_image_name(name)
|
||||||
launch_instance = self.images_table.launch_instance(row)
|
instance_form = self.images_table.launch_instance(row)
|
||||||
launch_instance.availability_zone.value = \
|
instance_form.availability_zone.value = \
|
||||||
self.conf.launch_instances.available_zone
|
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:
|
if flavor is None:
|
||||||
flavor = self.conf.launch_instances.flavor
|
flavor = self.conf.launch_instances.flavor
|
||||||
launch_instance.flavor.text = flavor
|
instance_form.flavor.transfer_available_resource(flavor)
|
||||||
launch_instance.count.value = instance_count
|
instance_form.switch_to(instance_page.NETWORKS_STEP_INDEX)
|
||||||
launch_instance.submit()
|
instance_form.network.transfer_available_resource(
|
||||||
return InstancesPage(self.driver, self.conf)
|
instance_page.DEFAULT_NETWORK_TYPE)
|
||||||
|
instance_form.submit()
|
||||||
|
instance_form.wait_till_wizard_disappears()
|
||||||
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)
|
|
||||||
|
@ -231,6 +231,22 @@ class SelectFormFieldRegion(BaseFormFieldRegion):
|
|||||||
self.driver.execute_script(js_cmd)
|
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):
|
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
|
||||||
"""Select box field."""
|
"""Select box field."""
|
||||||
|
|
||||||
@ -422,6 +438,13 @@ class FormRegion(BaseFormRegion):
|
|||||||
return self._get_form_fields()
|
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):
|
class TabbedFormRegion(FormRegion):
|
||||||
"""Forms that are divided with tabs.
|
"""Forms that are divided with tabs.
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ class RowRegion(baseregion.BaseRegion):
|
|||||||
chck_box.click()
|
chck_box.click()
|
||||||
|
|
||||||
|
|
||||||
|
class RowRegionNG(RowRegion):
|
||||||
|
"""Angular-based table row."""
|
||||||
|
|
||||||
|
_cell_locator = (by.By.CSS_SELECTOR, 'td > hz-cell')
|
||||||
|
|
||||||
|
|
||||||
class TableRegion(baseregion.BaseRegion):
|
class TableRegion(baseregion.BaseRegion):
|
||||||
"""Basic class representing table object."""
|
"""Basic class representing table object."""
|
||||||
|
|
||||||
@ -253,6 +259,38 @@ class TableRegion(baseregion.BaseRegion):
|
|||||||
self.assertDictEqual(actual_table, expected_table_definition)
|
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):
|
def bind_table_action(action_name):
|
||||||
"""Decorator to bind table region method to an actual table action button.
|
"""Decorator to bind table region method to an actual table action button.
|
||||||
|
|
||||||
@ -290,6 +328,44 @@ def bind_table_action(action_name):
|
|||||||
return decorator
|
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):
|
def bind_row_action(action_name):
|
||||||
"""A decorator to bind table region method to an actual row action button.
|
"""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
|
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):
|
def bind_row_anchor_column(column_name):
|
||||||
"""A decorator to bind table region method to a anchor in a column.
|
"""A decorator to bind table region method to a anchor in a column.
|
||||||
|
|
||||||
Typical examples of such tables are Project -> Compute -> Images, Admin
|
Typical examples of such tables are Project -> Compute -> Instances, Admin
|
||||||
-> System -> Flavors, Project -> Compute -> Instancies.
|
-> System -> Flavors.
|
||||||
The method can be used to follow the link in the anchor by the click.
|
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 wrapper
|
||||||
return decorator
|
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.
|
# under the License.
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from openstack_dashboard.test.integration_tests import decorators
|
|
||||||
from openstack_dashboard.test.integration_tests import helpers
|
from openstack_dashboard.test.integration_tests import helpers
|
||||||
from openstack_dashboard.test.integration_tests.regions import messages
|
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 TestImagesBasicAngular(helpers.TestCase):
|
||||||
class TestImagesLegacy(helpers.TestCase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.IMAGE_NAME = helpers.gen_random_resource_name("image")
|
self.IMAGE_NAME = helpers.gen_random_resource_name("image")
|
||||||
@ -27,30 +29,10 @@ class TestImagesLegacy(helpers.TestCase):
|
|||||||
def images_page(self):
|
def images_page(self):
|
||||||
return self.home_pg.go_to_project_compute_imagespage()
|
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):
|
def test_basic_image_browse(self):
|
||||||
images_page = self.images_page
|
images_page = self.images_page
|
||||||
self.assertEqual(images_page.header.text, 'Images')
|
self.assertEqual(images_page.header.text, 'Images')
|
||||||
|
|
||||||
|
|
||||||
class TestImagesBasic(TestImagesLegacy):
|
|
||||||
"""Login as demo user"""
|
|
||||||
def image_create(self, local_file=None, **kwargs):
|
def image_create(self, local_file=None, **kwargs):
|
||||||
images_page = self.images_page
|
images_page = self.images_page
|
||||||
if local_file:
|
if local_file:
|
||||||
@ -58,8 +40,10 @@ class TestImagesBasic(TestImagesLegacy):
|
|||||||
image_file=local_file,
|
image_file=local_file,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
else:
|
else:
|
||||||
images_page.create_image(self.IMAGE_NAME, **kwargs)
|
images_page.create_image(self.IMAGE_NAME,
|
||||||
self.assertTrue(images_page.find_message_and_dismiss(messages.INFO))
|
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.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_present(self.IMAGE_NAME))
|
||||||
self.assertTrue(images_page.is_image_active(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.find_message_and_dismiss(messages.ERROR))
|
||||||
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
|
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Bug 1595335")
|
def test_image_create_delete_from_local_file(self):
|
||||||
def test_image_create_delete(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:
|
"""tests the image creation and deletion functionalities:
|
||||||
|
|
||||||
* creates a new image from horizon.conf http_image
|
* creates a new image from horizon.conf http_image
|
||||||
@ -84,19 +81,6 @@ class TestImagesBasic(TestImagesLegacy):
|
|||||||
self.image_create()
|
self.image_create()
|
||||||
self.image_delete(self.IMAGE_NAME)
|
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):
|
def test_images_pagination(self):
|
||||||
"""This test checks images pagination
|
"""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)
|
9) Click 'Prev' and check results (should be the same as for step5)
|
||||||
10) Go to user settings page and restore 'Items Per Page'
|
10) Go to user settings page and restore 'Items Per Page'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
default_image_list = self.CONFIG.image.images_list
|
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
|
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,
|
first_page_definition = {'Next': True, 'Prev': False,
|
||||||
'Count': items_per_page,
|
'Count': items_per_page,
|
||||||
'Names': [default_image_list[0]]}
|
'Names': [default_image_list[0]]}
|
||||||
second_page_definition = {'Next': True, 'Prev': True,
|
second_page_definition = {'Next': True, 'Prev': True,
|
||||||
'Count': items_per_page,
|
'Count': items_per_page,
|
||||||
'Names': [default_image_list[1]]}
|
'Names': [images_names[0]]}
|
||||||
third_page_definition = {'Next': False, 'Prev': True,
|
third_page_definition = {'Next': False, 'Prev': True,
|
||||||
'Count': items_per_page,
|
'Count': items_per_page,
|
||||||
'Names': [default_image_list[2]]}
|
'Names': [images_names[1]]}
|
||||||
|
|
||||||
settings_page = self.home_pg.go_to_settings_usersettingspage()
|
settings_page = self.home_pg.go_to_settings_usersettingspage()
|
||||||
settings_page.change_pagesize(items_per_page)
|
settings_page.change_pagesize(items_per_page)
|
||||||
settings_page.find_message_and_dismiss(messages.SUCCESS)
|
settings_page.find_message_and_dismiss(messages.SUCCESS)
|
||||||
|
|
||||||
images_page = self.images_page
|
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.assert_definition(first_page_definition)
|
||||||
|
|
||||||
images_page.images_table.turn_next_page()
|
images_page.images_table.turn_next_page()
|
||||||
@ -150,6 +162,20 @@ class TestImagesBasic(TestImagesLegacy):
|
|||||||
settings_page.change_pagesize()
|
settings_page.change_pagesize()
|
||||||
settings_page.find_message_and_dismiss(messages.SUCCESS)
|
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):
|
def test_update_image_metadata(self):
|
||||||
"""Test update image metadata
|
"""Test update image metadata
|
||||||
|
|
||||||
@ -168,9 +194,6 @@ class TestImagesBasic(TestImagesLegacy):
|
|||||||
'metadata2': helpers.gen_random_resource_name("value")}
|
'metadata2': helpers.gen_random_resource_name("value")}
|
||||||
|
|
||||||
with helpers.gen_temporary_file() as file_name:
|
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,
|
images_page = self.image_create(local_file=file_name,
|
||||||
description='test description')
|
description='test description')
|
||||||
images_page.add_custom_metadata(self.IMAGE_NAME, new_metadata)
|
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.
|
# Check that Delete action is not available in the action list.
|
||||||
# The below action will generate exception since the bind fails.
|
# The below action will generate exception since the bind fails.
|
||||||
# But only ValueError with message below is expected here.
|
# 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)
|
images_page.delete_image_via_row_action(self.IMAGE_NAME)
|
||||||
|
|
||||||
# Try to delete image. That should not be possible now.
|
# Edit image to make it not protected again and delete it.
|
||||||
images_page.delete_image(self.IMAGE_NAME)
|
images_page = self.images_page
|
||||||
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))
|
|
||||||
|
|
||||||
images_page.edit_image(self.IMAGE_NAME, protected=False)
|
images_page.edit_image(self.IMAGE_NAME, protected=False)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
images_page.find_message_and_dismiss(messages.SUCCESS))
|
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.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):
|
def test_edit_image_description_and_name(self):
|
||||||
"""tests that image description is editable
|
"""tests that image description is editable
|
||||||
@ -264,9 +290,53 @@ class TestImagesBasic(TestImagesLegacy):
|
|||||||
self.assertSequenceTrue(results)
|
self.assertSequenceTrue(results)
|
||||||
|
|
||||||
self.image_delete(new_image_name)
|
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"""
|
"""Login as demo user"""
|
||||||
def test_create_volume_from_image(self):
|
def test_create_volume_from_image(self):
|
||||||
"""This test case checks create volume from image functionality:
|
"""This test case checks create volume from image functionality:
|
||||||
@ -282,18 +352,23 @@ class TestImagesAdvanced(TestImagesLegacy):
|
|||||||
source_image = self.CONFIG.image.images_list[0]
|
source_image = self.CONFIG.image.images_list[0]
|
||||||
target_volume = "created_from_{0}".format(source_image)
|
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)
|
source_image, volume_name=target_volume)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
volumes_page.find_message_and_dismiss(messages.INFO))
|
images_page.find_message_and_dismiss(messages.INFO))
|
||||||
self.assertFalse(
|
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_present(target_volume))
|
||||||
self.assertTrue(volumes_page.is_volume_status(target_volume,
|
self.assertTrue(volumes_page.is_volume_status(target_volume,
|
||||||
'Available'))
|
'Available'))
|
||||||
volumes_page.delete_volume(target_volume)
|
volumes_page.delete_volume(target_volume)
|
||||||
volumes_page.find_message_and_dismiss(messages.SUCCESS)
|
volumes_page.find_message_and_dismiss(messages.SUCCESS)
|
||||||
volumes_page.find_message_and_dismiss(messages.ERROR)
|
volumes_page.find_message_and_dismiss(messages.ERROR)
|
||||||
|
|
||||||
|
volumes_page = self.volumes_page()
|
||||||
self.assertTrue(volumes_page.is_volume_deleted(target_volume))
|
self.assertTrue(volumes_page.is_volume_deleted(target_volume))
|
||||||
|
|
||||||
def test_launch_instance_from_image(self):
|
def test_launch_instance_from_image(self):
|
||||||
@ -310,56 +385,22 @@ class TestImagesAdvanced(TestImagesLegacy):
|
|||||||
images_page = self.images_page
|
images_page = self.images_page
|
||||||
source_image = self.CONFIG.image.images_list[0]
|
source_image = self.CONFIG.image.images_list[0]
|
||||||
target_instance = "created_from_{0}".format(source_image)
|
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(
|
self.assertTrue(
|
||||||
instances_page.find_message_and_dismiss(messages.SUCCESS))
|
images_page.find_message_and_dismiss(messages.INFO))
|
||||||
self.assertFalse(
|
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))
|
self.assertTrue(instances_page.is_instance_active(target_instance))
|
||||||
|
instances_page = self.instances_page()
|
||||||
actual_image_name = instances_page.get_image_name(target_instance)
|
actual_image_name = instances_page.get_image_name(target_instance)
|
||||||
self.assertEqual(source_image, actual_image_name)
|
self.assertEqual(source_image, actual_image_name)
|
||||||
|
|
||||||
instances_page.delete_instance(target_instance)
|
instances_page.delete_instance(target_instance)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
instances_page.find_message_and_dismiss(messages.SUCCESS))
|
instances_page.find_message_and_dismiss(messages.INFO))
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
instances_page.find_message_and_dismiss(messages.ERROR))
|
instances_page.find_message_and_dismiss(messages.ERROR))
|
||||||
self.assertTrue(instances_page.is_instance_deleted(target_instance))
|
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):
|
def volumes_page(self):
|
||||||
return self.home_pg.go_to_project_volumes_volumespage()
|
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):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
volumes_page = self.volumes_page
|
volumes_page = self.volumes_page
|
||||||
@ -296,7 +300,6 @@ class TestVolumesActions(helpers.TestCase):
|
|||||||
new_size = volumes_page.get_size(self.VOLUME_NAME)
|
new_size = volumes_page.get_size(self.VOLUME_NAME)
|
||||||
self.assertLess(orig_size, new_size)
|
self.assertLess(orig_size, new_size)
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Bug 1847715")
|
|
||||||
def test_volume_upload_to_image(self):
|
def test_volume_upload_to_image(self):
|
||||||
"""This test case checks upload volume to image functionality:
|
"""This test case checks upload volume to image functionality:
|
||||||
|
|
||||||
@ -307,29 +310,28 @@ class TestVolumesActions(helpers.TestCase):
|
|||||||
4. Delete the image
|
4. Delete the image
|
||||||
5. Repeat actions for all disk formats
|
5. Repeat actions for all disk formats
|
||||||
"""
|
"""
|
||||||
self.volumes_page = self.home_pg.go_to_project_volumes_volumespage()
|
volumes_page = self.volumes_page
|
||||||
all_formats = {"qcow2": 'QCOW2', "raw": 'Raw', "vdi": 'VDI',
|
all_formats = {"qcow2": 'QCOW2', "raw": 'RAW', "vdi": 'VDI',
|
||||||
"vmdk": 'VMDK'}
|
"vmdk": 'VMDK'}
|
||||||
for disk_format in all_formats:
|
for disk_format in all_formats:
|
||||||
self.volumes_page.upload_volume_to_image(self.VOLUME_NAME,
|
volumes_page.upload_volume_to_image(
|
||||||
self.IMAGE_NAME,
|
self.VOLUME_NAME, self.IMAGE_NAME, disk_format)
|
||||||
disk_format)
|
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
self.volumes_page.find_message_and_dismiss(messages.ERROR))
|
volumes_page.find_message_and_dismiss(messages.ERROR))
|
||||||
self.assertTrue(self.volumes_page.is_volume_status(
|
self.assertTrue(volumes_page.is_volume_status(
|
||||||
self.VOLUME_NAME, 'Available'))
|
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_present(self.IMAGE_NAME))
|
||||||
self.assertTrue(images_page.is_image_active(self.IMAGE_NAME))
|
self.assertTrue(images_page.is_image_active(self.IMAGE_NAME))
|
||||||
self.assertEqual(images_page.get_image_format(self.IMAGE_NAME),
|
self.assertEqual(images_page.get_image_format(self.IMAGE_NAME),
|
||||||
all_formats[disk_format])
|
all_formats[disk_format])
|
||||||
images_page.delete_image(self.IMAGE_NAME)
|
images_page.delete_image(self.IMAGE_NAME)
|
||||||
self.assertTrue(images_page.find_message_and_dismiss(
|
self.assertTrue(images_page.find_message_and_dismiss(
|
||||||
messages.INFO))
|
messages.SUCCESS))
|
||||||
self.assertFalse(images_page.find_message_and_dismiss(
|
self.assertFalse(images_page.find_message_and_dismiss(
|
||||||
messages.ERROR))
|
messages.ERROR))
|
||||||
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
|
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
|
||||||
self.volumes_page = \
|
volumes_page = \
|
||||||
self.home_pg.go_to_project_volumes_volumespage()
|
self.home_pg.go_to_project_volumes_volumespage()
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Bug 1930420")
|
@pytest.mark.skip(reason="Bug 1930420")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user