Added update image metadata test
Updated forms.py with MetadataFormRegion. Updated tables.py with method to bind anchor row column. Added test for update image metadata. Implements blueprint: horizon-integration-tests-coverage Change-Id: Idd3651955b8f0e1a0c08dd43abd657aafa5a3bc2
This commit is contained in:
parent
baee24dc21
commit
d44eebecfa
openstack_dashboard/test/integration_tests
@ -66,6 +66,15 @@ def gen_temporary_file(name='', suffix='.qcow2', size=10485760):
|
||||
yield tmp_file.name
|
||||
|
||||
|
||||
class AssertsMixin(object):
|
||||
|
||||
def assertSequenceTrue(self, actual):
|
||||
return self.assertEqual(list(actual), [True] * len(actual))
|
||||
|
||||
def assertSequenceFalse(self, actual):
|
||||
return self.assertEqual(list(actual), [False] * len(actual))
|
||||
|
||||
|
||||
class BaseTestCase(testtools.TestCase):
|
||||
|
||||
CONFIG = config.get_config()
|
||||
@ -197,7 +206,7 @@ class BaseTestCase(testtools.TestCase):
|
||||
super(BaseTestCase, self).tearDown()
|
||||
|
||||
|
||||
class TestCase(BaseTestCase):
|
||||
class TestCase(BaseTestCase, AssertsMixin):
|
||||
|
||||
TEST_USER_NAME = BaseTestCase.CONFIG.identity.username
|
||||
TEST_PASSWORD = BaseTestCase.CONFIG.identity.password
|
||||
@ -225,7 +234,7 @@ class TestCase(BaseTestCase):
|
||||
super(TestCase, self).tearDown()
|
||||
|
||||
|
||||
class AdminTestCase(TestCase):
|
||||
class AdminTestCase(TestCase, AssertsMixin):
|
||||
|
||||
TEST_USER_NAME = TestCase.CONFIG.identity.admin_username
|
||||
TEST_PASSWORD = TestCase.CONFIG.identity.admin_password
|
||||
|
@ -9,7 +9,6 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
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 tables
|
||||
@ -20,6 +19,15 @@ from openstack_dashboard.test.integration_tests.pages.project.compute.\
|
||||
volumes.volumespage import VolumesPage
|
||||
|
||||
|
||||
DEFAULT_IMAGE_SOURCE = 'url'
|
||||
DEFAULT_IMAGE_FORMAT = 'qcow2'
|
||||
DEFAULT_ACCESSIBILITY = False
|
||||
DEFAULT_PROTECTION = False
|
||||
IMAGES_TABLE_NAME_COLUMN = 'name'
|
||||
IMAGES_TABLE_STATUS_COLUMN = 'status'
|
||||
IMAGES_TABLE_FORMAT_COLUMN = 'disk_format'
|
||||
|
||||
|
||||
class ImagesTable(tables.TableRegion):
|
||||
name = "images"
|
||||
|
||||
@ -69,23 +77,25 @@ class ImagesTable(tables.TableRegion):
|
||||
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_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):
|
||||
|
||||
DEFAULT_IMAGE_SOURCE = 'url'
|
||||
DEFAULT_IMAGE_FORMAT = 'qcow2'
|
||||
DEFAULT_ACCESSIBILITY = False
|
||||
DEFAULT_PROTECTION = False
|
||||
IMAGES_TABLE_NAME_COLUMN = 'name'
|
||||
IMAGES_TABLE_STATUS_COLUMN = 'status'
|
||||
IMAGES_TABLE_FORMAT_COLUMN = 'disk_format'
|
||||
|
||||
def __init__(self, driver, conf):
|
||||
super(ImagesPage, self).__init__(driver, conf)
|
||||
self._page_title = "Images"
|
||||
|
||||
def _get_row_with_image_name(self, name):
|
||||
return self.images_table.get_row(self.IMAGES_TABLE_NAME_COLUMN, name)
|
||||
return self.images_table.get_row(IMAGES_TABLE_NAME_COLUMN, name)
|
||||
|
||||
@property
|
||||
def images_table(self):
|
||||
@ -123,20 +133,39 @@ class ImagesPage(basepage.BaseNavigationPage):
|
||||
confirm_delete_images_form = self.images_table.delete_image()
|
||||
confirm_delete_images_form.submit()
|
||||
|
||||
def add_custom_metadata(self, name, metadata):
|
||||
row = self._get_row_with_image_name(name)
|
||||
update_metadata_form = self.images_table.update_metadata(row)
|
||||
for field_name, value in metadata.iteritems():
|
||||
update_metadata_form.add_custom_field(field_name, value)
|
||||
update_metadata_form.submit()
|
||||
|
||||
def check_image_details(self, name, dict_with_details):
|
||||
row = self._get_row_with_image_name(name)
|
||||
matches = []
|
||||
description_page = self.images_table.go_to_image_description_page(row)
|
||||
content = description_page.get_content()
|
||||
|
||||
for name, value in content.iteritems():
|
||||
if name in dict_with_details:
|
||||
if dict_with_details[name] in value:
|
||||
matches.append(True)
|
||||
return matches
|
||||
|
||||
def is_image_present(self, name):
|
||||
return bool(self._get_row_with_image_name(name))
|
||||
|
||||
def is_image_active(self, name):
|
||||
row = self._get_row_with_image_name(name)
|
||||
return self.images_table.is_cell_status(
|
||||
lambda: row.cells[self.IMAGES_TABLE_STATUS_COLUMN], 'Active')
|
||||
lambda: row.cells[IMAGES_TABLE_STATUS_COLUMN], 'Active')
|
||||
|
||||
def wait_until_image_active(self, name):
|
||||
self._wait_until(lambda x: self.is_image_active(name))
|
||||
|
||||
def get_image_format(self, name):
|
||||
row = self._get_row_with_image_name(name)
|
||||
return row.cells[self.IMAGES_TABLE_FORMAT_COLUMN].text
|
||||
return row.cells[IMAGES_TABLE_FORMAT_COLUMN].text
|
||||
|
||||
def create_volume_from_image(self, name, volume_name=None,
|
||||
description=None,
|
||||
|
@ -11,6 +11,7 @@
|
||||
# under the License.
|
||||
import six
|
||||
|
||||
from selenium.common import exceptions
|
||||
from selenium.webdriver.common import by
|
||||
import selenium.webdriver.support.ui as Support
|
||||
|
||||
@ -403,3 +404,70 @@ class DateFormRegion(BaseFormRegion):
|
||||
|
||||
def _set_to_field(self, value):
|
||||
self._fill_field_element(value, self.to_date)
|
||||
|
||||
|
||||
class MetadataFormRegion(BaseFormRegion):
|
||||
|
||||
_input_fields = (by.By.CSS_SELECTOR, 'div.input-group')
|
||||
_custom_input_field = (by.By.XPATH, "//input[@name='customItem']")
|
||||
_custom_input_button = (by.By.CSS_SELECTOR, 'span.input-group-btn > .btn')
|
||||
_submit_locator = (by.By.CSS_SELECTOR, '.modal-footer > .btn.btn-primary')
|
||||
_cancel_locator = (by.By.CSS_SELECTOR, '.modal-footer > .btn.btn-default')
|
||||
|
||||
def _form_getter(self):
|
||||
return self.driver.find_element(*self._default_form_locator)
|
||||
|
||||
@property
|
||||
def custom_field_value(self):
|
||||
return self._get_element(*self._custom_input_field)
|
||||
|
||||
@property
|
||||
def add_button(self):
|
||||
return self._get_element(*self._custom_input_button)
|
||||
|
||||
def add_custom_field(self, field_name, field_value):
|
||||
self.custom_field_value.send_keys(field_name)
|
||||
self.add_button.click()
|
||||
for div in self._get_elements(*self._input_fields):
|
||||
if div.text in field_name:
|
||||
field = div.find_element(by.By.CSS_SELECTOR, 'input')
|
||||
if not hasattr(self, field_name):
|
||||
self._dynamic_properties[field_name] = field
|
||||
self.set_field_value(field_name, field_value)
|
||||
|
||||
def set_field_value(self, field_name, field_value):
|
||||
if hasattr(self, field_name):
|
||||
field = getattr(self, field_name)
|
||||
field.send_keys(field_value)
|
||||
else:
|
||||
raise AttributeError("Unknown form field '{}'.".format(field_name))
|
||||
|
||||
def wait_till_spinner_disappears(self):
|
||||
# No spinner is invoked after the 'Save' button click
|
||||
# Will wait till the form itself disappears
|
||||
try:
|
||||
self.wait_till_element_disappears(self._form_getter)
|
||||
except exceptions.StaleElementReferenceException:
|
||||
# The form might be absent already by the time the first check
|
||||
# occurs. So just suppress the exception here.
|
||||
pass
|
||||
|
||||
|
||||
class ItemTextDescription(baseregion.BaseRegion):
|
||||
|
||||
_separator_locator = (by.By.CSS_SELECTOR, 'dl.dl-horizontal')
|
||||
_key_locator = (by.By.CSS_SELECTOR, 'dt')
|
||||
_value_locator = (by.By.CSS_SELECTOR, 'dd')
|
||||
|
||||
def __init__(self, driver, conf, src=None):
|
||||
super(ItemTextDescription, self).__init__(driver, conf, src)
|
||||
|
||||
def get_content(self):
|
||||
keys = []
|
||||
values = []
|
||||
for section in self._get_elements(*self._separator_locator):
|
||||
keys.extend([x.text for x in
|
||||
section.find_elements(*self._key_locator)])
|
||||
values.extend([x.text for x in
|
||||
section.find_elements(*self._value_locator)])
|
||||
return dict(zip(keys, values))
|
||||
|
@ -301,3 +301,23 @@ def bind_row_action(action_name):
|
||||
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.
|
||||
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.%s > a' % NORMAL_COLUMN_CLASS)
|
||||
return method(table, action_element, row)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
@ -117,6 +117,30 @@ class TestImagesBasic(helpers.TestCase):
|
||||
settings_page.change_pagesize()
|
||||
settings_page.find_message_and_dismiss(messages.SUCCESS)
|
||||
|
||||
def test_update_image_metadata(self):
|
||||
"""Test update image metadata
|
||||
* logs in as admin user
|
||||
* creates image from locally downloaded file
|
||||
* verifies the image appears in the images table as active
|
||||
* invokes action 'Update Metadata' for the image
|
||||
* adds custom filed 'metadata'
|
||||
* adds value 'image' for the custom filed 'metadata'
|
||||
* gets the actual description of the image
|
||||
* verifies that custom filed is present in the image description
|
||||
* deletes the image
|
||||
* verifies the image does not appear in the table after deletion
|
||||
"""
|
||||
new_metadata = {'metadata1': helpers.gen_random_resource_name("value"),
|
||||
'metadata2': helpers.gen_random_resource_name("value")}
|
||||
|
||||
with helpers.gen_temporary_file() as file_name:
|
||||
images_page = self.image_create(local_file=file_name)
|
||||
images_page.add_custom_metadata(self.IMAGE_NAME, new_metadata)
|
||||
results = images_page.check_image_details(self.IMAGE_NAME,
|
||||
new_metadata)
|
||||
self.image_delete()
|
||||
self.assertSequenceTrue(results) # custom matcher
|
||||
|
||||
|
||||
class TestImagesAdvanced(helpers.TestCase):
|
||||
"""Login as demo user"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user