Revert "Remove all dependencies/connections of old integration test code"

This reverts commit 49e5fe185a.

reason:
- these changes are breaking the Manila UI plugin job
- there is a good chance other Horizon plugin jobs would also fail.
- more testing across as many plugin jobs will be needed before a revisit.

Change-Id: I76018c5eff71ed82b89e202957ab3f6ee9452369
Signed-off-by: Owen McGonagle <omcgonag@redhat.com>
This commit is contained in:
Owen McGonagle
2025-10-28 13:41:04 -04:00
parent 36fead9ac5
commit 83117a2184
100 changed files with 9577 additions and 9 deletions

View File

@@ -32,6 +32,9 @@
<script type="text/javascript">
horizon.modals.MODAL_BACKDROP = "{% firstof HORIZON_CONFIG.modal_backdrop 'static' %}";
</script>
{% if HORIZON_CONFIG.integration_tests_support %}
<script src="{{ STATIC_URL }}horizon/js/horizon.selenium.js"></script>
{% endif %}
<script src="{{ STATIC_URL }}horizon/js/horizon.tables.js"></script>
<script src="{{ STATIC_URL }}horizon/js/horizon.tabs.js"></script>
<script src="{{ STATIC_URL }}horizon/js/horizon.templates.js"></script>

View File

@@ -0,0 +1,31 @@
Horizon Integration Tests
=========================
Horizon's integration tests treat Horizon as a black box.
Running the integration tests
-----------------------------
#. Set up an OpenStack server
#. Prepare the configuration file at `local-horizon.conf` if you need
to change the default configurations.
Note that `horizon.conf` can be used for the same purpose too
from the historical reason.
You can generate a sample configuration file by the following command::
$ oslo-config-generator \
--namespace openstack_dashboard_integration_tests
--output-file openstack_dashboard/test/integration_tests/horizon.conf.sample
#. Run the tests. ::
$ tox -e integration
More information
----------------
https://wiki.openstack.org/wiki/Horizon/Testing/UI
https://wiki.mozilla.org/QA/Execution/Web_Testing/Docs/Automation/StyleGuide#Page_Objects

View File

@@ -0,0 +1,169 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import unittest
import selenium.common.exceptions as Exceptions
from selenium.webdriver.common import by
from selenium.webdriver.remote import webelement
import selenium.webdriver.support.ui as Support
from selenium.webdriver.support import wait
class BaseWebObject(unittest.TestCase):
"""Base class for all web objects."""
_spinner_locator = (by.By.CSS_SELECTOR, '.modal-body > .loader')
def __init__(self, driver, conf):
self.driver = driver
self.conf = conf
if conf is not None and conf.get('selenium', None) is not None:
self.explicit_wait = self.conf.selenium.explicit_wait
else:
self.explicit_wait = 0
super().__init__()
def _is_element_present(self, *locator):
with self.waits_disabled():
try:
self._get_element(*locator)
return True
except Exceptions.NoSuchElementException:
return False
def _is_element_visible(self, *locator):
try:
return self._get_element(*locator).is_displayed()
except (Exceptions.NoSuchElementException,
Exceptions.ElementNotVisibleException):
return False
def _is_element_displayed(self, element):
if element is None:
return False
try:
if isinstance(element, webelement.WebElement):
return element.is_displayed()
else:
return element.src_elem.is_displayed()
except (Exceptions.ElementNotVisibleException,
Exceptions.StaleElementReferenceException):
return False
def _is_text_visible(self, element, text, strict=True):
if not hasattr(element, 'text'):
return False
if strict:
return element.text == text
else:
return text in element.text
def _get_element(self, *locator):
return self.driver.find_element(*locator)
def _get_elements(self, *locator):
return self.driver.find_elements(*locator)
def _fill_field_element(self, data, field_element):
field_element.clear()
field_element.send_keys(data)
return field_element
def _select_dropdown(self, value, element):
select = Support.Select(element)
select.select_by_visible_text(value)
def _select_dropdown_by_value(self, value, element):
select = Support.Select(element)
select.select_by_value(value)
def _get_dropdown_options(self, element):
select = Support.Select(element)
return select.options
def _turn_off_implicit_wait(self):
self.driver.implicitly_wait(0)
def _turn_on_implicit_wait(self):
self.driver.implicitly_wait(self.conf.selenium.implicit_wait)
def _wait_until(self, predicate, timeout=None, poll_frequency=0.5):
"""Wait until the value returned by predicate is not False.
It also returns when the timeout is elapsed.
'predicate' takes the driver as argument.
"""
if not timeout:
timeout = self.explicit_wait
return wait.WebDriverWait(self.driver, timeout, poll_frequency).until(
predicate)
def _wait_till_text_present_in_element(self, element, texts, timeout=None):
"""Waiting for a text to appear in a certain element.
Most frequent usage is actually to wait for a _different_ element
with a different text to appear in place of an old element.
So a way to avoid capturing stale element reference should be provided
for this use case.
Better to wrap getting entity status cell in a lambda
to avoid problems with cell being replaced with totally different
element by Javascript
"""
if not isinstance(texts, (list, tuple)):
texts = (texts,)
def predicate(_):
elt = element() if hasattr(element, '__call__') else element
for text in texts:
if self._is_text_visible(elt, text):
return text
return False
return self._wait_until(predicate, timeout)
def _wait_till_element_visible(self, locator, timeout=None):
self._wait_until(lambda x: self._is_element_visible(*locator), timeout)
def _wait_till_element_disappears(self, element, timeout=None):
self._wait_until(lambda x: not self._is_element_displayed(element),
timeout)
@contextlib.contextmanager
def waits_disabled(self):
try:
self._turn_off_implicit_wait()
yield
finally:
self._turn_on_implicit_wait()
def wait_till_element_disappears(self, element_getter):
with self.waits_disabled():
try:
self._wait_till_element_disappears(element_getter())
except Exceptions.NoSuchElementException:
# NOTE(mpavlase): This is valid state. When request completes
# even before Selenium get a chance to get the spinner element,
# it will raise the NoSuchElementException exception.
pass
def wait_until_element_is_visible(self, locator):
with self.waits_disabled():
try:
self._wait_till_element_visible(locator)
except Exceptions.NoSuchElementException:
pass
def wait_till_spinner_disappears(self):
def getter():
return self.driver.find_element(*self._spinner_locator)
self.wait_till_element_disappears(getter)

View File

@@ -0,0 +1,176 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections.abc
import functools
import inspect
import unittest
from openstack_dashboard.test.integration_tests import config
def _is_test_method_name(method):
return method.startswith('test_')
def _is_test_fixture(method):
return method in ['setUp', 'tearDown']
def _is_test_cls(cls):
return cls.__name__.startswith('Test')
def _mark_method_skipped(meth, reason):
"""Decorate to mark method as skipped.
This marks method as skipped by replacing the actual method with wrapper
that raises the unittest.SkipTest exception.
"""
@functools.wraps(meth)
def wrapper(*args, **kwargs):
raise unittest.SkipTest(reason)
return wrapper
def _mark_class_skipped(cls, reason):
"""Mark every test method of the class as skipped."""
tests = [attr for attr in dir(cls) if _is_test_method_name(attr) or
_is_test_fixture(attr)]
for test in tests:
method = getattr(cls, test)
if callable(method):
setattr(cls, test, _mark_method_skipped(method, reason))
return cls
NOT_TEST_OBJECT_ERROR_MSG = "Decorator can be applied only on test" \
" classes and test methods."
def _get_skip_method(obj):
"""Make sure that we can decorate both methods and classes."""
if inspect.isclass(obj):
if not _is_test_cls(obj):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
return _mark_class_skipped
else:
if not _is_test_method_name(obj.__name__):
raise ValueError(NOT_TEST_OBJECT_ERROR_MSG)
return _mark_method_skipped
def services_required(*req_services):
"""Decorator for marking test's service requirements.
If requirements are not met in the configuration file
test is marked as skipped.
Usage:
from openstack_dashboard.test.integration_tests.tests import decorators
@decorators.services_required("sahara")
class TestLogin(helpers.BaseTestCase):
.
.
.
from openstack_dashboard.test.integration_tests.tests import decorators
class TestLogin(helpers.BaseTestCase):
@decorators.services_required("sahara")
def test_login(self):
login_pg = loginpage.LoginPage(self.driver, self.conf)
.
.
.
"""
def actual_decoration(obj):
skip_method = _get_skip_method(obj)
# get available services from configuration
avail_services = config.get_config().service_available
for req_service in req_services:
if not getattr(avail_services, req_service, False):
obj = skip_method(obj, "%s service is required for this test"
" to work properly." % req_service)
break
return obj
return actual_decoration
def _parse_compound_config_option_value(option_name):
"""Parses the value of a given config option.
The section name of the option is separated from option name by '.'.
"""
name_parts = option_name.split('.')
name_parts.reverse()
option = config.get_config()
while name_parts:
option = getattr(option, name_parts.pop())
return option
def config_option_required(option_key, required_value, message=None):
if message is None:
message = "%s option equal to '%s' is required for this test to work" \
" properly." % (option_key, required_value)
def actual_decoration(obj):
skip_method = _get_skip_method(obj)
option_value = _parse_compound_config_option_value(option_key)
if option_value != required_value:
obj = skip_method(obj, message)
return obj
return actual_decoration
def skip_because(**kwargs):
"""Decorator for skipping tests hitting known bugs
Usage:
from openstack_dashboard.test.integration_tests.tests import decorators
class TestDashboardHelp(helpers.TestCase):
@decorators.skip_because(bugs=["1234567"])
def test_dashboard_help_redirection(self):
.
.
.
"""
def actual_decoration(obj):
skip_method = _get_skip_method(obj)
bugs = kwargs.get("bugs")
if bugs and isinstance(bugs, collections.abc.Iterable):
for bug in bugs:
if not bug.isdigit():
raise ValueError("bug must be a valid bug number")
obj = skip_method(obj, "Skipped until Bugs: %s are resolved." %
", ".join([bug for bug in bugs]))
return obj
return actual_decoration
def attach_video(func):
"""Notify test runner to attach test video in any case"""
@functools.wraps(func)
def wrapper(self, *args, **kwgs):
self._need_attach_video = True
return func(self, *args, **kwgs)
return wrapper

View File

@@ -0,0 +1,355 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
import io
import logging
import os
import shutil
import socket
import subprocess
import tempfile
import time
import traceback
import unittest
from django.test import tag
from oslo_utils import uuidutils
from selenium.webdriver.common import action_chains
from selenium.webdriver.common import by
from selenium.webdriver.common import keys
import testtools
import xvfbwrapper
from horizon.test import helpers
from horizon.test import webdriver
from openstack_dashboard.test.integration_tests import config
from openstack_dashboard.test.integration_tests.pages import loginpage
from openstack_dashboard.test.integration_tests.regions import messages
from openstack_dashboard.test.integration_tests.video_recorder import \
VideoRecorder
# Set logging level to DEBUG for all logger here
# so that lower level messages are output even before starting tests.
ROOT_LOGGER = logging.getLogger()
ROOT_LOGGER.setLevel(logging.DEBUG)
LOG = logging.getLogger(__name__)
IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False)
ROOT_PATH = os.path.dirname(os.path.abspath(config.__file__))
SCREEN_SIZE = (None, None)
if not subprocess.call('which xdpyinfo > /dev/null 2>&1', shell=True):
try:
SCREEN_SIZE = subprocess.check_output(
'xdpyinfo | grep dimensions',
shell=True).decode().split()[1].split('x')
except subprocess.CalledProcessError:
LOG.info("Can't run 'xdpyinfo'")
else:
LOG.info("X11 isn't installed. Should use xvfb to run tests.")
def gen_random_resource_name(resource="", timestamp=True):
"""Generate random resource name using uuid and timestamp.
Input fields are usually limited to 255 or 80 characters hence their
provide enough space for quite long resource names, but it might be
the case that maximum field length is quite restricted, it is then
necessary to consider using shorter resource argument or avoid using
timestamp by setting timestamp argument to False.
"""
fields = ["horizon"]
if resource:
fields.append(resource)
if timestamp:
tstamp = time.strftime("%d-%m-%H-%M-%S")
fields.append(tstamp)
fields.append(uuidutils.generate_uuid(dashed=False))
return "_".join(fields)
@contextlib.contextmanager
def gen_temporary_file(name='', suffix='.qcow2', size=10485760):
"""Generate temporary file with provided parameters.
:param name: file name except the extension /suffix
:param suffix: file extension/suffix
:param size: size of the file to create, bytes are generated randomly
:return: path to the generated file
"""
with tempfile.NamedTemporaryFile(prefix=name, suffix=suffix) as tmp_file:
tmp_file.write(os.urandom(size))
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))
@helpers.pytest_mark('integration')
@tag('integration')
class BaseTestCase(testtools.TestCase):
CONFIG = config.get_config()
def setUp(self):
self._configure_log()
self.addOnException(
lambda exc_info: setattr(self, '_need_attach_test_log', True))
def cleanup():
if getattr(self, '_need_attach_test_log', None):
self._attach_test_log()
self.addCleanup(cleanup)
width, height = SCREEN_SIZE
display = '0.0'
# Start a virtual display server for running the tests headless.
if IS_SELENIUM_HEADLESS:
width, height = 1920, 1080
self.vdisplay = xvfbwrapper.Xvfb(width=width, height=height)
args = []
# workaround for memory leak in Xvfb taken from:
# http://blog.jeffterrace.com/2012/07/xvfb-memory-leak-workaround.html
args.append("-noreset")
# disables X access control
args.append("-ac")
if hasattr(self.vdisplay, 'extra_xvfb_args'):
# xvfbwrapper 0.2.8 or newer
self.vdisplay.extra_xvfb_args.extend(args)
else:
self.vdisplay.xvfb_cmd.extend(args)
self.vdisplay.start()
display = self.vdisplay.new_display
self.addCleanup(self.vdisplay.stop)
self.video_recorder = VideoRecorder(width, height, display=display)
self.video_recorder.start()
def attach_video(exc_info):
if getattr(self, '_attached_video', None):
return
self.video_recorder.stop()
self._attach_video()
setattr(self, '_attached_video', True)
self.addOnException(attach_video)
def cleanup():
if getattr(self, '_attached_video', None):
return
self.video_recorder.stop()
self.video_recorder.clear()
self.addCleanup(cleanup)
# Increase the default Python socket timeout from nothing
# to something that will cope with slow webdriver startup times.
# This *just* affects the communication between this test process
# and the webdriver.
socket.setdefaulttimeout(60)
# Start the Selenium webdriver and setup configuration.
desired_capabilities = dict(webdriver.desired_capabilities)
desired_capabilities['loggingPrefs'] = {'browser': 'ALL'}
self.driver = webdriver.WebDriverWrapper(
desired_capabilities=desired_capabilities
)
if self.CONFIG.selenium.maximize_browser:
self.driver.maximize_window()
if IS_SELENIUM_HEADLESS: # force full screen in xvfb
self.driver.set_window_size(width, height)
self.driver.implicitly_wait(self.CONFIG.selenium.implicit_wait)
self.driver.set_page_load_timeout(
self.CONFIG.selenium.page_timeout)
self.addOnException(self._attach_page_source)
self.addOnException(self._attach_screenshot)
self.addOnException(
lambda exc_info: setattr(self, '_need_attach_browser_log', True))
def cleanup():
if getattr(self, '_need_attach_browser_log', None):
self._attach_browser_log()
self.driver.quit()
self.addCleanup(cleanup)
super().setUp()
def addOnException(self, exception_handler):
def wrapped_handler(exc_info):
if issubclass(exc_info[0], unittest.SkipTest):
return
return exception_handler(exc_info)
super().addOnException(wrapped_handler)
def __hash__(self):
return hash((type(self), self._testMethodName))
def _configure_log(self):
"""Configure log to capture test logs include selenium logs.
This allows us to attach them if test will be broken.
"""
# clear other handlers to set target handler
ROOT_LOGGER.handlers[:] = []
self._log_buffer = io.StringIO()
stream_handler = logging.StreamHandler(stream=self._log_buffer)
stream_handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(formatter)
ROOT_LOGGER.addHandler(stream_handler)
@property
def _test_report_dir(self):
report_dir = os.path.join(
ROOT_PATH, self.CONFIG.selenium.screenshots_directory,
'{}.{}'.format(self.__class__.__name__, self._testMethodName))
if not os.path.isdir(report_dir):
os.makedirs(report_dir)
return report_dir
def _attach_page_source(self, exc_info):
source_path = os.path.join(self._test_report_dir, 'page.html')
with self.log_exception("Attach page source"):
with open(source_path, 'w') as f:
f.write(self._get_page_html_source())
def _attach_screenshot(self, exc_info):
screen_path = os.path.join(self._test_report_dir, 'screenshot.png')
with self.log_exception("Attach screenshot"):
self.driver.get_screenshot_as_file(screen_path)
def _attach_video(self, exc_info=None):
with self.log_exception("Attach video"):
if not os.path.isfile(self.video_recorder.file_path):
LOG.warning("Can't find video %s",
self.video_recorder.file_path)
return
shutil.move(self.video_recorder.file_path,
os.path.join(self._test_report_dir, 'video.mp4'))
def _attach_browser_log(self, exc_info=None):
browser_log_path = os.path.join(self._test_report_dir, 'browser.log')
try:
log = self._unwrap_browser_log(self.driver.get_log('browser'))
except Exception:
log = traceback.format_exc()
with open(browser_log_path, 'w') as f:
f.write(log)
def _attach_test_log(self, exc_info=None):
test_log_path = os.path.join(self._test_report_dir, 'test.log')
with self.log_exception("Attach test log"):
with open(test_log_path, 'w') as f:
f.write(self._log_buffer.getvalue())
@contextlib.contextmanager
def log_exception(self, label):
try:
yield
except Exception:
self.addDetail(
label, testtools.content.text_content(traceback.format_exc()))
@staticmethod
def _unwrap_browser_log(_log):
def rec(log):
if isinstance(log, dict):
return log['message'].encode('utf-8')
elif isinstance(log, list):
return '\n'.join([rec(item) for item in log])
else:
return log.encode('utf-8')
return rec(_log)
def zoom_out(self, times=3):
"""Zooming out a specified element.
It prevents different elements being driven out of xvfb viewport
(which in Selenium>=2.50.1 prevents interaction with them).
"""
html = self.driver.find_element(by.By.TAG_NAME, 'html')
html.send_keys(keys.Keys.NULL)
zoom_out_keys = (keys.Keys.SUBTRACT,) * times
action_chains.ActionChains(self.driver).key_down(
keys.Keys.CONTROL).send_keys(*zoom_out_keys).key_up(
keys.Keys.CONTROL).perform()
def _get_page_html_source(self):
"""Gets html page source.
self.driver.page_source is not used on purpose because it does not
display html code generated/changed by javascript.
"""
html_elem = self.driver.find_element_by_tag_name("html")
return html_elem.get_property("innerHTML")
@helpers.pytest_mark('integration')
@tag('integration')
class TestCase(BaseTestCase, AssertsMixin):
TEST_USER_NAME = BaseTestCase.CONFIG.identity.username
TEST_PASSWORD = BaseTestCase.CONFIG.identity.password
HOME_PROJECT = BaseTestCase.CONFIG.identity.home_project
def setUp(self):
super().setUp()
self.login_pg = loginpage.LoginPage(self.driver, self.CONFIG)
self.login_pg.go_to_login_page()
# TODO(schipiga): lets check that tests work without viewport changing,
# otherwise will uncomment.
# self.zoom_out()
self.home_pg = self.login_pg.login(self.TEST_USER_NAME,
self.TEST_PASSWORD)
self.home_pg.change_project(self.HOME_PROJECT)
self.assertEqual(
self.home_pg.find_messages_and_dismiss(), {messages.SUCCESS})
def cleanup():
if self.home_pg.is_logged_in:
self.home_pg.go_to_home_page()
self.home_pg.log_out()
self.addCleanup(cleanup)
class AdminTestCase(TestCase, AssertsMixin):
TEST_USER_NAME = TestCase.CONFIG.identity.admin_username
TEST_PASSWORD = TestCase.CONFIG.identity.admin_password
HOME_PROJECT = BaseTestCase.CONFIG.identity.admin_home_project
def setUp(self):
super().setUp()
self.home_pg.go_to_admin_overviewpage()

View File

@@ -0,0 +1,155 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 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
class FlavorsTable(tables.TableRegion):
name = "flavors"
CREATE_FLAVOR_FORM_FIELDS = (("name", "flavor_id", "vcpus", "memory_mb",
"disk_gb", "eph_gb",
"swap_mb"),
{"members": menus.MembershipMenuRegion})
UPDATE_FLAVOR_FORM_FIELDS = (("name", "vcpus", "memory_mb",
"disk_gb", "eph_gb", "swap_mb"),
{"members": menus.MembershipMenuRegion})
@tables.bind_table_action('create')
def create_flavor(self, create_button):
create_button.click()
return forms.TabbedFormRegion(
self.driver,
self.conf,
field_mappings=self.CREATE_FLAVOR_FORM_FIELDS
)
@tables.bind_row_action('update')
def update_flavor_info(self, edit_button, row):
edit_button.click()
return forms.TabbedFormRegion(
self.driver,
self.conf,
field_mappings=self.UPDATE_FLAVOR_FORM_FIELDS
)
@tables.bind_row_action('projects')
def update_flavor_access(self, update_button, row):
update_button.click()
return forms.TabbedFormRegion(
self.driver,
self.conf,
field_mappings=self.UPDATE_FLAVOR_FORM_FIELDS,
default_tab=1
)
@tables.bind_row_action('delete')
def delete_by_row(self, delete_button, row):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class FlavorsPage(basepage.BaseNavigationPage):
DEFAULT_ID = "auto"
FLAVORS_TABLE_NAME_COLUMN = 'Flavor Name'
FLAVORS_TABLE_VCPUS_COLUMN = 'VCPUs'
FLAVORS_TABLE_RAM_COLUMN = 'RAM'
FLAVORS_TABLE_DISK_COLUMN = 'Root Disk'
FLAVORS_TABLE_PUBLIC_COLUMN = 'Public'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Flavors"
@property
def flavors_table(self):
return FlavorsTable(self.driver, self.conf)
def _get_flavor_row(self, name):
return self.flavors_table.get_row(self.FLAVORS_TABLE_NAME_COLUMN, name)
def create_flavor(self, name, id_=DEFAULT_ID, vcpus=None, ram=None,
root_disk=None, ephemeral_disk=None, swap_disk=None):
create_flavor_form = self.flavors_table.create_flavor()
create_flavor_form.name.text = name
if id_ is not None:
create_flavor_form.flavor_id.text = id_
create_flavor_form.vcpus.value = vcpus
create_flavor_form.memory_mb.value = ram
create_flavor_form.disk_gb.value = root_disk
create_flavor_form.eph_gb.value = ephemeral_disk
create_flavor_form.swap_mb.value = swap_disk
create_flavor_form.submit()
def is_flavor_present(self, name):
return bool(self._get_flavor_row(name))
def update_flavor_info(self, name, add_up):
row = self._get_flavor_row(name)
update_flavor_form = self.flavors_table.update_flavor_info(row)
update_flavor_form.name.text = "edited-" + name
update_flavor_form.vcpus.value = \
int(update_flavor_form.vcpus.value) + add_up
update_flavor_form.memory_mb.value =\
int(update_flavor_form.memory_mb.value) + add_up
update_flavor_form.disk_gb.value =\
int(update_flavor_form.disk_gb.value) + add_up
update_flavor_form.submit()
def update_flavor_access(self, name, project_name, allocate=True):
row = self._get_flavor_row(name)
update_flavor_form = self.flavors_table.update_flavor_access(row)
if allocate:
update_flavor_form.members.allocate_member(project_name)
else:
update_flavor_form.members.deallocate_member(project_name)
update_flavor_form.submit()
def delete_flavor_by_row(self, name):
row = self._get_flavor_row(name)
delete_form = self.flavors_table.delete_by_row(row)
delete_form.submit()
def get_flavor_vcpus(self, name):
row = self._get_flavor_row(name)
return row.cells[self.FLAVORS_TABLE_VCPUS_COLUMN].text
def get_flavor_ram(self, name):
row = self._get_flavor_row(name)
return row.cells[self.FLAVORS_TABLE_RAM_COLUMN].text
def get_flavor_disk(self, name):
row = self._get_flavor_row(name)
return row.cells[self.FLAVORS_TABLE_DISK_COLUMN].text
def is_flavor_public(self, name):
row = self._get_flavor_row(name)
return row.cells[self.FLAVORS_TABLE_PUBLIC_COLUMN].text == "Yes"
class FlavorsPageNG(FlavorsPage):
_resource_page_header_locator = (by.By.CSS_SELECTOR,
'.page-header > h1')
@property
def header(self):
return self._get_element(*self._resource_page_header_locator)

View File

@@ -0,0 +1,79 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class HostAggregatesTable(tables.TableRegion):
name = "host_aggregates"
CREATE_HOST_AGGREGATE_FORM_FIELDS = (("name",
"availability_zone"),)
@tables.bind_table_action('create')
def create_host_aggregate(self, create_button):
create_button.click()
return forms.TabbedFormRegion(self.driver, self.conf,
field_mappings=self.
CREATE_HOST_AGGREGATE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_host_aggregate(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf, None)
# Examples of how to bind to secondary actions
@tables.bind_row_action('update')
def update_host_aggregate(self, edit_host_aggregate_button, row):
edit_host_aggregate_button.click()
pass
@tables.bind_row_action('manage')
def modify_access(self, manage_button, row):
manage_button.click()
pass
class HostaggregatesPage(basepage.BaseNavigationPage):
HOST_AGGREGATES_TABLE_NAME_COLUMN = 'Name'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Host Aggregates"
@property
def host_aggregates_table(self):
return HostAggregatesTable(self.driver, self.conf)
def _get_host_aggregate_row(self, name):
return self.host_aggregates_table.get_row(
self.HOST_AGGREGATES_TABLE_NAME_COLUMN, name)
def create_host_aggregate(self, name, availability_zone):
create_host_aggregate_form = \
self.host_aggregates_table.create_host_aggregate()
create_host_aggregate_form.name.text = name
create_host_aggregate_form.availability_zone.text = \
availability_zone
create_host_aggregate_form.submit()
def delete_host_aggregate(self, name):
row = self._get_host_aggregate_row(name)
row.mark()
modal_confirmation_form = self.host_aggregates_table.\
delete_host_aggregate()
modal_confirmation_form.submit()
def is_host_aggregate_present(self, name):
return bool(self._get_host_aggregate_row(name))

View File

@@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class HypervisorsPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "All Hypervisors"

View File

@@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.compute \
import imagespage
class ImagesPage(imagespage.ImagesPage):
pass

View File

@@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.compute \
import instancespage
class InstancesPage(instancespage.InstancesPage):
INSTANCES_TABLE_NAME_COLUMN = 'Name'

View File

@@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.network \
import floatingipspage
class FloatingipsPage(floatingipspage.FloatingipsPage):
pass

View File

@@ -0,0 +1,38 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.network \
import networkspage
class NetworksPage(networkspage.NetworksPage):
NETWORKS_TABLE_NAME_COLUMN = 'Network Name'
@property
def is_admin(self):
return True
@property
def networks_table(self):
return NetworksTable(self.driver, self.conf)
class NetworksTable(networkspage.NetworksTable):
CREATE_NETWORK_FORM_FIELDS = (("name", "admin_state",
"with_subnet", "az_hints", "tenant_id",
"network_type"),
("subnet_name", "cidr", "ip_version",
"gateway_ip", "no_gateway"),
("enable_dhcp", "allocation_pools",
"dns_nameservers", "host_routes"))

View File

@@ -0,0 +1,41 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.network \
import routerspage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class RoutersTable(routerspage.RoutersTable):
EDIT_ROUTER_FORM_FIELDS = ("name", "admin_state")
@tables.bind_row_action('update')
def edit_router(self, edit_button, row):
edit_button.click()
return forms.FormRegion(self.driver, self.conf, None,
self.EDIT_ROUTER_FORM_FIELDS)
class RoutersPage(routerspage.RoutersPage):
@property
def routers_table(self):
return RoutersTable(self.driver, self.conf)
def edit_router(self, name, new_name, admin_state=None):
row = self._get_row_with_router_name(name)
edit_router_form = self.routers_table.edit_router(row)
edit_router_form.name.text = new_name
if admin_state is not None:
edit_router_form.admin_state.text = admin_state
edit_router_form.submit()

View File

@@ -0,0 +1,19 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class OverviewPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Overview"

View File

@@ -0,0 +1,166 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 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 tables
class DefaultQuotasTable(tables.TableRegion):
name = "compute_quotas"
UPDATE_DEFAULTS_FORM_FIELDS = (("injected_file_content_bytes",
"metadata_items", "ram",
"key_pairs",
"injected_file_path_bytes",
"instances", "injected_files",
"cores", "server_groups",
"server_group_members"),
("volumes", "gigabytes",
"snapshots"))
@tables.bind_table_action('update_compute_defaults')
def update(self, update_button):
update_button.click()
return forms.TabbedFormRegion(
self.driver,
self.conf,
self.UPDATE_DEFAULTS_FORM_FIELDS)
class DefaultVolumeQuotasTable(DefaultQuotasTable):
name = "volume_quotas"
@tables.bind_table_action('update_volume_defaults')
def update(self, update_button):
update_button.click()
return forms.TabbedFormRegion(
self.driver,
self.conf,
self.UPDATE_DEFAULTS_FORM_FIELDS)
class DefaultsPage(basepage.BaseNavigationPage):
QUOTAS_TABLE_NAME_COLUMN = 'Quota Name'
QUOTAS_TABLE_LIMIT_COLUMN = 'Limit'
VOLUMES_TAB_INDEX = 1
DEFAULT_COMPUTE_QUOTA_NAMES = [
'Injected File Content (B)',
'Metadata Items',
'Server Group Members',
'Server Groups',
'RAM (MB)',
'Key Pairs',
'Length of Injected File Path',
'Instances',
'Injected Files',
'VCPUs'
]
DEFAULT_VOLUME_QUOTA_NAMES = [
'Volumes',
'Total Size of Volumes and Snapshots (GiB)',
'Volume Snapshots',
]
_volume_quotas_tab_locator = (by.By.CSS_SELECTOR,
'a[href*="defaults__volume_quotas"]')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Defaults"
def _get_compute_quota_row(self, name):
return self.default_compute_quotas_table.get_row(
self.QUOTAS_TABLE_NAME_COLUMN, name)
def _get_volume_quota_row(self, name):
return self.default_volume_quotas_table.get_row(
self.QUOTAS_TABLE_NAME_COLUMN, name)
@property
def default_compute_quotas_table(self):
return DefaultQuotasTable(self.driver, self.conf)
@property
def default_volume_quotas_table(self):
return DefaultVolumeQuotasTable(self.driver, self.conf)
@property
def compute_quota_values(self):
quota_dict = {}
for row in self.default_compute_quotas_table.rows:
if row.cells[self.QUOTAS_TABLE_NAME_COLUMN].text in \
self.DEFAULT_COMPUTE_QUOTA_NAMES:
quota_dict[row.cells[self.QUOTAS_TABLE_NAME_COLUMN].text] =\
int(row.cells[self.QUOTAS_TABLE_LIMIT_COLUMN].text)
return quota_dict
@property
def volume_quota_values(self):
quota_dict = {}
for row in self.default_volume_quotas_table.rows:
if row.cells[self.QUOTAS_TABLE_NAME_COLUMN].text in \
self.DEFAULT_VOLUME_QUOTA_NAMES:
quota_dict[row.cells[self.QUOTAS_TABLE_NAME_COLUMN].text] =\
int(row.cells[self.QUOTAS_TABLE_LIMIT_COLUMN].text)
return quota_dict
@property
def volume_quotas_tab(self):
return self._get_element(*self._volume_quotas_tab_locator)
def update_compute_defaults(self, add_up):
update_form = self.default_compute_quotas_table.update()
update_form.injected_file_content_bytes.value = \
int(update_form.injected_file_content_bytes.value) + add_up
update_form.metadata_items.value = \
int(update_form.metadata_items.value) + add_up
update_form.server_group_members.value = \
int(update_form.server_group_members.value) + add_up
update_form.server_groups.value = \
int(update_form.server_groups.value) + add_up
update_form.ram.value = int(update_form.ram.value) + add_up
update_form.key_pairs.value = int(update_form.key_pairs.value) + add_up
update_form.injected_file_path_bytes.value = \
int(update_form.injected_file_path_bytes.value) + add_up
update_form.instances.value = int(update_form.instances.value) + add_up
update_form.injected_files.value = int(
update_form.injected_files.value) + add_up
update_form.cores.value = int(update_form.cores.value) + add_up
update_form.submit()
def update_volume_defaults(self, add_up):
update_form = self.default_volume_quotas_table.update()
update_form.switch_to(self.VOLUMES_TAB_INDEX)
update_form.volumes.value = int(update_form.volumes.value) + add_up
update_form.gigabytes.value = int(update_form.gigabytes.value) + add_up
update_form.snapshots.value = int(update_form.snapshots.value) + add_up
update_form.submit()
def is_compute_quota_a_match(self, quota_name, limit):
row = self._get_compute_quota_row(quota_name)
return row.cells[self.QUOTAS_TABLE_LIMIT_COLUMN].text == str(limit)
def is_volume_quota_a_match(self, quota_name, limit):
row = self._get_volume_quota_row(quota_name)
return row.cells[self.QUOTAS_TABLE_LIMIT_COLUMN].text == str(limit)
def go_to_volume_quotas_tab(self):
self.volume_quotas_tab.click()

View File

@@ -0,0 +1,17 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.compute \
import imagespage
class ImagesPage(imagespage.ImagesPage):
pass

View File

@@ -0,0 +1,128 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
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
class MetadatadefinitionsTable(tables.TableRegion):
name = "namespaces"
CREATE_NAMESPACE_FORM_FIELDS = (
"source_type", "direct_input", "metadef_file", "public", "protected")
@tables.bind_table_action('import')
def import_namespace(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver,
self.conf,
field_mappings=self.CREATE_NAMESPACE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_namespace(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class MetadatadefinitionsPage(basepage.BaseNavigationPage):
NAMESPACE_TABLE_NAME_COLUMN = 'Name'
NAMESPACE_TABLE_DESCRIPTION_COLUMN = 'Description'
NAMESPACE_TABLE_RESOURCE_TYPES_COLUMN = 'Resource Types'
NAMESPACE_TABLE_PUBLIC_COLUMN = 'Public'
NAMESPACE_TABLE_PROTECTED_COLUMN = 'Protected'
boolean_mapping = {True: 'Yes', False: 'No'}
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Metadata Definitions"
def _get_row_with_namespace_name(self, name):
return self.namespaces_table.get_row(
self.NAMESPACE_TABLE_NAME_COLUMN,
name)
@property
def namespaces_table(self):
return MetadatadefinitionsTable(self.driver, self.conf)
def json_load_template(self, namespace_template_name):
"""Read template for namespace creation
:param namespace_template_name: Path to template
:return = json data container
"""
try:
with open(namespace_template_name, 'r') as template:
json_template = json.load(template)
except Exception:
raise EOFError("Can not read template file: [{0}]".format(
namespace_template_name))
return json_template
def import_namespace(
self, namespace_source_type, namespace_json_container,
is_public=True, is_protected=False):
create_namespace_form = self.namespaces_table.import_namespace()
create_namespace_form.source_type.value = namespace_source_type
if namespace_source_type == 'raw':
json_template_dump = json.dumps(namespace_json_container)
create_namespace_form.direct_input.text = json_template_dump
elif namespace_source_type == 'file':
metadeffile = namespace_json_container
create_namespace_form.metadef_file.choose(metadeffile)
if is_public:
create_namespace_form.public.mark()
if is_protected:
create_namespace_form.protected.mark()
create_namespace_form.submit()
def delete_namespace(self, name):
row = self._get_row_with_namespace_name(name)
row.mark()
confirm_delete_namespaces_form = \
self.namespaces_table.delete_namespace()
confirm_delete_namespaces_form.submit()
def is_namespace_present(self, name):
return bool(self._get_row_with_namespace_name(name))
def is_public_set_correct(self, name, exp_value, row=None):
if not isinstance(exp_value, bool):
raise ValueError('Expected value "exp_value" is not boolean')
if not row:
row = self._get_row_with_namespace_name(name)
cell = row.cells[self.NAMESPACE_TABLE_PUBLIC_COLUMN]
return self._is_text_visible(cell, self.boolean_mapping[exp_value])
def is_protected_set_correct(self, name, exp_value, row=None):
if not isinstance(exp_value, bool):
raise ValueError('Expected value "exp_value" is not boolean')
if not row:
row = self._get_row_with_namespace_name(name)
cell = row.cells[self.NAMESPACE_TABLE_PROTECTED_COLUMN]
return self._is_text_visible(cell, self.boolean_mapping[exp_value])
def is_resource_type_set_correct(self, name, expected_resources, row=None):
if not row:
row = self._get_row_with_namespace_name(name)
cell = row.cells[self.NAMESPACE_TABLE_RESOURCE_TYPES_COLUMN]
return all(
[self._is_text_visible(cell, res, strict=False)
for res in expected_resources])

View File

@@ -0,0 +1,70 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class GroupTypesTable(tables.TableRegion):
name = 'group_types'
CREATE_GROUP_TYPE_FORM_FIELDS = ("name", "group_type_description")
@tables.bind_table_action('create')
def create_group_type(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_GROUP_TYPE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_group_type(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class GrouptypesPage(basepage.BaseNavigationPage):
GROUP_TYPES_TABLE_NAME_COLUMN = 'Name'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Group Types"
@property
def group_types_table(self):
return GroupTypesTable(self.driver, self.conf)
def _get_row_with_group_type_name(self, name):
return self.group_types_table.get_row(
self.GROUP_TYPES_TABLE_NAME_COLUMN, name)
def create_group_type(self, group_type_name, description=None):
group_type_form = self.group_types_table.create_group_type()
group_type_form.name.text = group_type_name
if description is not None:
group_type_form.description.text = description
group_type_form.submit()
def delete_group_type(self, name):
row = self._get_row_with_group_type_name(name)
row.mark()
confirm_delete_group_types_form = \
self.group_types_table.delete_group_type()
confirm_delete_group_types_form.submit()
def is_group_type_present(self, name):
return bool(self._get_row_with_group_type_name(name))
def is_group_type_deleted(self, name):
return self.group_types_table.is_row_deleted(
lambda: self._get_row_with_group_type_name(name))

View File

@@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.\
volumes import snapshotspage
class SnapshotsPage(snapshotspage.SnapshotsPage):
pass

View File

@@ -0,0 +1,18 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.project.\
volumes import volumespage
class VolumesPage(volumespage.VolumesPage):
pass

View File

@@ -0,0 +1,135 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class QosSpecsTable(tables.TableRegion):
name = 'qos_specs'
CREATE_QOS_SPEC_FORM_FIELDS = ("name", "consumer")
EDIT_CONSUMER_FORM_FIELDS = ("consumer_choice", )
@tables.bind_table_action('create')
def create_qos_spec(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_QOS_SPEC_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_qos_specs(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('edit_consumer')
def edit_consumer(self, edit_consumer_button, row):
edit_consumer_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.EDIT_CONSUMER_FORM_FIELDS)
class VolumeTypesTable(tables.TableRegion):
name = 'volume_types'
CREATE_VOLUME_TYPE_FORM_FIELDS = (
"name", "vol_type_description")
@tables.bind_table_action('create')
def create_volume_type(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_VOLUME_TYPE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_volume_type(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class VolumetypesPage(basepage.BaseNavigationPage):
QOS_SPECS_TABLE_NAME_COLUMN = 'Name'
VOLUME_TYPES_TABLE_NAME_COLUMN = 'Name'
QOS_SPECS_TABLE_CONSUMER_COLUMN = 'Consumer'
CINDER_CONSUMER = 'back-end'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Volume Types"
@property
def qos_specs_table(self):
return QosSpecsTable(self.driver, self.conf)
@property
def volume_types_table(self):
return VolumeTypesTable(self.driver, self.conf)
def _get_row_with_qos_spec_name(self, name):
return self.qos_specs_table.get_row(
self.QOS_SPECS_TABLE_NAME_COLUMN, name)
def _get_row_with_volume_type_name(self, name):
return self.volume_types_table.get_row(
self.VOLUME_TYPES_TABLE_NAME_COLUMN, name)
def create_qos_spec(self, qos_spec_name, consumer=CINDER_CONSUMER):
create_qos_spec_form = self.qos_specs_table.create_qos_spec()
create_qos_spec_form.name.text = qos_spec_name
create_qos_spec_form.submit()
def create_volume_type(self, volume_type_name, description=None):
volume_type_form = self.volume_types_table.create_volume_type()
volume_type_form.name.text = volume_type_name
if description is not None:
volume_type_form.description.text = description
volume_type_form.submit()
def delete_qos_specs(self, name):
row = self._get_row_with_qos_spec_name(name)
row.mark()
confirm_delete_qos_spec_form = self.qos_specs_table.delete_qos_specs()
confirm_delete_qos_spec_form.submit()
def delete_volume_type(self, name):
row = self._get_row_with_volume_type_name(name)
row.mark()
confirm_delete_volume_types_form = \
self.volume_types_table.delete_volume_type()
confirm_delete_volume_types_form.submit()
def edit_consumer(self, name, consumer_choice):
row = self._get_row_with_qos_spec_name(name)
edit_consumer_form = self.qos_specs_table.edit_consumer(row)
edit_consumer_form.consumer_choice.value = consumer_choice
edit_consumer_form.submit()
def is_qos_spec_present(self, name):
return bool(self._get_row_with_qos_spec_name(name))
def is_volume_type_present(self, name):
return bool(self._get_row_with_volume_type_name(name))
def is_qos_spec_deleted(self, name):
return self.qos_specs_table.is_row_deleted(
lambda: self._get_row_with_qos_spec_name(name))
def is_volume_type_deleted(self, name):
return self.volume_types_table.is_row_deleted(
lambda: self._get_row_with_volume_type_name(name))
def get_consumer(self, name):
row = self._get_row_with_qos_spec_name(name)
return row.cells[self.QOS_SPECS_TABLE_CONSUMER_COLUMN].text

View File

@@ -0,0 +1,89 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import selenium.common.exceptions as Exceptions
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import navigation
from openstack_dashboard.test.integration_tests.pages import pageobject
from openstack_dashboard.test.integration_tests.regions import bars
from openstack_dashboard.test.integration_tests.regions import menus
from openstack_dashboard.test.integration_tests.regions import messages
class BasePage(pageobject.PageObject):
"""Base class for all dashboard page objects."""
_heading_locator = (by.By.CSS_SELECTOR, 'div.page-header > h2')
_help_page_brand = (by.By.CSS_SELECTOR, '.navbar-brand')
_default_message_locator = (by.By.CSS_SELECTOR, 'div.alert')
@property
def heading(self):
return self._get_element(*self._heading_locator)
@property
def topbar(self):
return bars.TopBarRegion(self.driver, self.conf)
@property
def is_logged_in(self):
return self.topbar.is_logged_in
@property
def navaccordion(self):
return menus.NavigationAccordionRegion(self.driver, self.conf)
def go_to_login_page(self):
self.driver.get(self.login_url)
def go_to_home_page(self):
self.topbar.brand.click()
def log_out(self):
self.topbar.user_dropdown_menu.click_on_logout()
def go_to_help_page(self):
self.topbar.user_dropdown_menu.click_on_help()
def is_help_page(self):
self._wait_till_element_visible(self._help_page_brand)
def choose_theme(self, theme_name):
self.topbar.user_dropdown_menu.choose_theme(theme_name)
def find_all_messages(self):
self.driver.implicitly_wait(self.conf.selenium.message_implicit_wait)
try:
msg_elements = self.driver.find_elements(
*self._default_message_locator)
except Exceptions.NoSuchElementException:
msg_elements = []
finally:
self._turn_on_implicit_wait()
return msg_elements
def find_messages_and_dismiss(self):
messages_level_present = set()
for message_element in self.find_all_messages():
message = messages.MessageRegion(
self.driver, self.conf, message_element)
messages_level_present.add(message.message_class)
message.close()
return messages_level_present
def change_project(self, name):
self.topbar.user_dropdown_project.click_on_project(name)
class BaseNavigationPage(BasePage, navigation.Navigation):
pass

View File

@@ -0,0 +1,84 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class GroupsTable(tables.TableRegion):
name = "groups"
@property
def form_fields(self):
return ("name", "description")
@tables.bind_table_action('create')
def create_group(self, create_button):
create_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.form_fields)
@tables.bind_table_action('delete')
def delete_group(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('edit')
def edit_group(self, edit_button, row):
edit_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.form_fields)
class GroupsPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Groups'
@property
def table_name_column(self):
return "Name"
@property
def groups_table(self):
return GroupsTable(self.driver, self.conf)
def _get_row_with_group_name(self, name):
return self.groups_table.get_row(self.table_name_column, name)
def create_group(self, name, description=None):
create_form = self.groups_table.create_group()
create_form.name.text = name
if description is not None:
create_form.description.text = description
create_form.submit()
def delete_group(self, name):
row = self._get_row_with_group_name(name)
row.mark()
confirm_delete_form = self.groups_table.delete_group()
confirm_delete_form.submit()
def edit_group(self, name, new_name=None, new_description=None):
row = self._get_row_with_group_name(name)
edit_form = self.groups_table.edit_group(row)
if new_name is not None:
edit_form.name.text = new_name
if new_description is not None:
edit_form.description.text = new_description
edit_form.submit()
def is_group_present(self, name):
return bool(self._get_row_with_group_name(name))

View File

@@ -0,0 +1,100 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 menus
from openstack_dashboard.test.integration_tests.regions import tables
class ProjectForm(forms.TabbedFormRegion):
FIELDS = (("name", "description", "enabled"),
{'members': menus.MembershipMenuRegion})
def __init__(self, driver, conf, tab=0):
super().__init__(driver, conf, field_mappings=self.FIELDS,
default_tab=tab)
class ProjectsTable(tables.TableRegion):
name = 'tenants'
@tables.bind_table_action('create')
def create_project(self, create_button):
create_button.click()
return ProjectForm(self.driver, self.conf)
@tables.bind_row_action('update')
def update_members(self, members_button, row):
members_button.click()
return ProjectForm(self.driver, self.conf, tab=1)
@tables.bind_table_action('delete')
def delete_project(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf, None)
class ProjectsPage(basepage.BaseNavigationPage):
DEFAULT_ENABLED = True
PROJECTS_TABLE_NAME_COLUMN = 'Name'
PROJECT_ID_TABLE_NAME_COLUMN = 'Project ID'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Projects"
@property
def projects_table(self):
return ProjectsTable(self.driver, self.conf)
def _get_row_with_project_name(self, name):
return self.projects_table.get_row(self.PROJECTS_TABLE_NAME_COLUMN,
name)
def create_project(self, project_name, description=None,
is_enabled=DEFAULT_ENABLED):
create_project_form = self.projects_table.create_project()
create_project_form.name.text = project_name
if description is not None:
create_project_form.description.text = description
if not is_enabled:
create_project_form.enabled.unmark()
create_project_form.submit()
def delete_project(self, project_name):
row = self._get_row_with_project_name(project_name)
row.mark()
modal_confirmation_form = self.projects_table.delete_project()
modal_confirmation_form.submit()
def is_project_present(self, project_name):
return bool(self._get_row_with_project_name(project_name))
def get_project_id_from_row(self, name):
row = self._get_row_with_project_name(name)
return row.cells[self.PROJECT_ID_TABLE_NAME_COLUMN].text
def allocate_user_to_project(self, user_name, roles, project_name):
row = self._get_row_with_project_name(project_name)
members_form = self.projects_table.update_members(row)
members_form.members.allocate_member(user_name)
members_form.members.allocate_member_roles(user_name, roles)
members_form.submit()
def get_user_roles_at_project(self, user_name, project_name):
row = self._get_row_with_project_name(project_name)
members_form = self.projects_table.update_members(row)
roles = members_form.members.get_member_allocated_roles(user_name)
members_form.cancel()
return set(roles)

View File

@@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class RolesPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Roles'

View File

@@ -0,0 +1,69 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class UsersTable(tables.TableRegion):
name = 'users'
CREATE_USER_FORM_FIELDS = ("name", "email", "password",
"confirm_password", "project", "role_id")
@tables.bind_table_action('create')
def create_user(self, create_button):
create_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.CREATE_USER_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_user(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class UsersPage(basepage.BaseNavigationPage):
USERS_TABLE_NAME_COLUMN = 'User Name'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Users"
def _get_row_with_user_name(self, name):
return self.users_table.get_row(self.USERS_TABLE_NAME_COLUMN, name)
@property
def users_table(self):
return UsersTable(self.driver, self.conf)
def create_user(self, name, password,
project, role, email=None):
create_user_form = self.users_table.create_user()
create_user_form.name.text = name
if email is not None:
create_user_form.email.text = email
create_user_form.password.text = password
create_user_form.confirm_password.text = password
create_user_form.project.text = project
create_user_form.role_id.text = role
create_user_form.submit()
def delete_user(self, name):
row = self._get_row_with_user_name(name)
row.mark()
confirm_delete_users_form = self.users_table.delete_user()
confirm_delete_users_form.submit()
def is_user_present(self, name):
return bool(self._get_row_with_user_name(name))

View File

@@ -0,0 +1,99 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from selenium.webdriver.common import keys
from openstack_dashboard.test.integration_tests.pages.admin import \
overviewpage as system_overviewpage
from openstack_dashboard.test.integration_tests.pages import pageobject
from openstack_dashboard.test.integration_tests.pages.project.compute import \
overviewpage as compute_overviewpage
class LoginPage(pageobject.PageObject):
_login_domain_field_locator = (by.By.ID, 'id_domain')
_login_username_field_locator = (by.By.ID, 'id_username')
_login_password_field_locator = (by.By.ID, 'id_password')
_login_submit_button_locator = (by.By.CSS_SELECTOR,
'div.panel-footer button.btn')
_login_logout_reason_locator = (by.By.ID, 'logout_reason')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Login"
def is_login_page(self):
return (self.is_the_current_page() and
self._is_element_visible(*self._login_submit_button_locator))
@property
def domain(self):
return self._get_elements(*self._login_domain_field_locator)
@property
def username(self):
return self._get_element(*self._login_username_field_locator)
@property
def password(self):
return self._get_element(*self._login_password_field_locator)
@property
def login_button(self):
return self._get_element(*self._login_submit_button_locator)
def _click_on_login_button(self):
self.login_button.click()
def _press_enter_on_login_button(self):
self.login_button.send_keys(keys.Keys.RETURN)
def is_logout_reason_displayed(self):
return self._get_element(*self._login_logout_reason_locator)
def login(self, user=None, password=None):
return self.login_with_mouse_click(user, password)
def login_with_mouse_click(self, user, password):
return self._do_login(user, password, self._click_on_login_button)
def login_with_enter_key(self, user, password):
return self._do_login(user, password,
self._press_enter_on_login_button)
def _do_login(self, user, password, login_method):
if self.conf.identity.domain:
self.domain[0].clear()
self.domain[0].send_keys(self.conf.identity.domain)
if user == self.conf.identity.admin_username:
if password is None:
password = self.conf.identity.admin_password
return self.login_as_admin(password, login_method)
else:
if password is None:
password = self.conf.identity.password
if user is None:
user = self.conf.identity.username
return self.login_as_user(user, password, login_method)
def login_as_admin(self, password, login_method):
self.username.send_keys(self.conf.identity.admin_username)
self.password.send_keys(password)
login_method()
return system_overviewpage.OverviewPage(self.driver, self.conf)
def login_as_user(self, user, password, login_method):
self.username.send_keys(user)
self.password.send_keys(password)
login_method()
return compute_overviewpage.OverviewPage(self.driver, self.conf)

View File

@@ -0,0 +1,341 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
import importlib
import json
import types
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests import config
class Navigation(object):
"""Provide basic navigation among pages.
* allows navigation among pages without need to import pageobjects
* shortest path possible should be always used for navigation to
specific page as user would navigate in the same manner
* go_to_*some_page* method respects following pattern:
* go_to_{sub_menu}_{pagename}page
* go_to_*some_page* methods are generated at runtime at module import
* go_to_*some_page* method returns pageobject
* pages must be located in the correct directories for the navigation
to work correctly
* pages modules and class names must respect following pattern
to work correctly with navigation module:
* module name consist of menu item in lower case without spaces and '&'
* page class begins with capital letter and ends with 'Page'
Examples:
In order to go to Project/Compute/Overview page, one would have to call
method go_to_compute_overviewpage()
In order to go to Admin/System/Overview page, one would have to call
method go_to_system_overviewpage()
"""
# constants
MAX_SUB_LEVEL = 4
MIN_SUB_LEVEL = 2
SIDE_MENU_MAX_LEVEL = 3
CONFIG = config.get_config()
PAGES_IMPORT_PATH = [
"openstack_dashboard.test.integration_tests.pages.%s"
]
if CONFIG.plugin.is_plugin and CONFIG.plugin.plugin_page_path:
for path in CONFIG.plugin.plugin_page_path:
PAGES_IMPORT_PATH.append(path + ".%s")
ITEMS = "_"
CORE_PAGE_STRUCTURE = \
{
"Project":
{
"Compute":
{
ITEMS:
(
"Overview",
"Instances",
"Images",
"Key Pairs",
"Server Groups",
)
},
"Volumes":
{
ITEMS:
(
"Volumes",
"Backups",
"Snapshots",
)
},
"Network":
{
ITEMS:
(
"Network Topology",
"Networks",
"Routers",
"Security Groups",
"Floating IPs",
)
},
ITEMS:
(
"API Access",
)
},
"Admin":
{
"Compute":
{
ITEMS:
(
"Hypervisors",
"Host Aggregates",
"Instances",
"Flavors",
"Images",
)
},
"Volume":
{
ITEMS:
(
"Volumes",
"Snapshots",
"Volume Types",
"Group Types",
)
},
"Network":
{
ITEMS:
(
"Networks",
"Routers",
"Floating IPs",
)
},
"System":
{
ITEMS:
(
"Defaults",
"Metadata Definitions",
"System Information"
)
},
ITEMS:
(
"Overview",
)
},
"Identity":
{
ITEMS:
(
"Projects",
"Users",
"Groups",
"Roles",
"Application Credentials",
)
},
"Settings":
{
ITEMS:
(
"User Settings",
"Change Password",
)
},
}
_main_content_locator = (by.By.ID, 'content_body')
# protected methods
def _go_to_page(self, path, page_class=None):
"""Go to page specified via path parameter.
* page_class parameter overrides basic process for receiving
pageobject
"""
path_len = len(path)
if path_len < self.MIN_SUB_LEVEL or path_len > self.MAX_SUB_LEVEL:
raise ValueError("Navigation path length should be in the interval"
" between %s and %s, but its length is %s" %
(self.MIN_SUB_LEVEL, self.MAX_SUB_LEVEL,
path_len))
if path_len == self.MIN_SUB_LEVEL:
# menu items that do not contain second layer of menu
if path[0] == "Settings":
self._go_to_settings_page(path[1])
else:
self._go_to_side_menu_page([path[0], None, path[1]])
else:
# side menu contains only three sub-levels
self._go_to_side_menu_page(path[:self.SIDE_MENU_MAX_LEVEL])
if path_len == self.MAX_SUB_LEVEL:
# apparently there is tabbed menu,
# because another extra sub level is present
self._go_to_tab_menu_page(path[self.MAX_SUB_LEVEL - 1])
# if there is some nonstandard pattern in page object naming
return self._get_page_class(path, page_class)(self.driver, self.conf)
def _go_to_tab_menu_page(self, item_text):
content_body = self.driver.find_element(*self._main_content_locator)
content_body.find_element_by_link_text(item_text).click()
def _go_to_settings_page(self, item_text):
"""Go to page that is located under the settings tab."""
self.topbar.user_dropdown_menu.click_on_settings()
self.navaccordion.click_on_menu_items(third_level=item_text)
def _go_to_side_menu_page(self, menu_items):
"""Go to page that is located in the side menu (navaccordion)."""
self.navaccordion.click_on_menu_items(*menu_items)
def _get_page_cls_name(self, filename):
"""Gather page class name from path.
* take last item from path (should be python filename
without extension)
* make the first letter capital
* append 'Page'
"""
cls_name = "".join((filename.capitalize(), "Page"))
return cls_name
def _get_page_class(self, path, page_cls_name):
# last module name does not contain '_'
final_module = self.unify_page_path(path[-1],
preserve_spaces=False)
page_cls_path = ".".join(path[:-1] + (final_module,))
page_cls_path = self.unify_page_path(page_cls_path)
# append 'page' as every page module ends with this keyword
page_cls_path += "page"
page_cls_name = page_cls_name or self._get_page_cls_name(final_module)
module = None
# return imported class
for path in self.PAGES_IMPORT_PATH:
try:
module = importlib.import_module(path %
page_cls_path)
break
except ImportError:
pass
if module is None:
raise ImportError("Failed to import module: " +
(path % page_cls_path))
return getattr(module, page_cls_name)
class GoToMethodFactory(object):
"""Represent the go_to_some_page method."""
METHOD_NAME_PREFIX = "go_to_"
METHOD_NAME_SUFFIX = "page"
METHOD_NAME_DELIMITER = "_"
# private methods
def __init__(self, path, page_class=None):
self.path = path
self.page_class = page_class
self._name = self._create_name()
def __call__(self, *args, **kwargs):
return Navigation._go_to_page(args[0], self.path, self.page_class)
# protected methods
def _create_name(self):
"""Create method name.
* consist of 'go_to_subsubmenu_menuitem_page'
"""
if len(self.path) < 3:
path_2_name = list(self.path[-2:])
else:
path_2_name = list(self.path[-3:])
name = self.METHOD_NAME_DELIMITER.join(path_2_name)
name = self.METHOD_NAME_PREFIX + name + self.METHOD_NAME_SUFFIX
name = Navigation.unify_page_path(name, preserve_spaces=False)
return name
# properties
@property
def name(self):
return self._name
# classmethods
@classmethod
def _initialize_go_to_methods(cls):
"""Create all navigation methods based on the PAGE_STRUCTURE."""
def rec(items, sub_menus):
if isinstance(items, dict):
for sub_menu, sub_item in items.items():
rec(sub_item, sub_menus + (sub_menu,))
elif isinstance(items, (list, tuple)):
# exclude ITEMS element from sub_menus
paths = (sub_menus[:-1] + (menu_item,) for menu_item in items)
for path in paths:
cls._create_go_to_method(path)
rec(cls.CORE_PAGE_STRUCTURE, ())
plugin_page_structure_strings = cls.CONFIG.plugin.plugin_page_structure
for plugin_ps_string in plugin_page_structure_strings:
plugin_page_structure = json.loads(plugin_ps_string)
rec(plugin_page_structure, ())
@classmethod
def _create_go_to_method(cls, path, class_name=None):
go_to_method = Navigation.GoToMethodFactory(path, class_name)
inst_method = types.MethodType(go_to_method, Navigation)
def _go_to_page(self, path):
return Navigation._go_to_page(self, path)
wrapped_go_to = functools.partialmethod(_go_to_page, path)
setattr(Navigation, inst_method.name, wrapped_go_to)
@classmethod
def unify_page_path(cls, path, preserve_spaces=True):
"""Unify path to page.
Replace '&' in path with 'and', remove spaces (if not specified
otherwise) and convert path to lower case.
"""
path = path.replace("&", "and")
path = path.lower()
if preserve_spaces:
path = path.replace(" ", "_")
else:
path = path.replace(" ", "")
return path
Navigation._initialize_go_to_methods()

View File

@@ -0,0 +1,94 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 urllib import parse
from openstack_dashboard.test.integration_tests import basewebobject
class PageObject(basewebobject.BaseWebObject):
"""Base class for page objects."""
PARTIAL_LOGIN_URL = 'auth/login'
def __init__(self, driver, conf):
"""Constructor."""
super().__init__(driver, conf)
self._page_title = None
@property
def page_title(self):
return self.driver.title
def is_the_current_page(self, do_assert=False):
found_expected_title = self.page_title.startswith(self._page_title)
if do_assert:
self.assertTrue(
found_expected_title,
"Expected to find %s in page title, instead found: %s"
% (self._page_title, self.page_title))
return found_expected_title
@property
def login_url(self):
base_url = self.conf.dashboard.dashboard_url
if not base_url.endswith('/'):
base_url += '/'
return parse.urljoin(base_url, self.PARTIAL_LOGIN_URL)
def get_url_current_page(self):
return self.driver.current_url
def close_window(self):
return self.driver.close()
def is_nth_window_opened(self, n):
return len(self.driver.window_handles) == n
def switch_window(self, window_name=None, window_index=None):
"""Switches focus between the webdriver windows.
Args:
- window_name: The name of the window to switch to.
- window_index: The index of the window handle to switch to.
If the method is called without arguments it switches to the
last window in the driver window_handles list.
In case only one window exists nothing effectively happens.
Usage:
page.switch_window('_new')
page.switch_window(2)
page.switch_window()
"""
if window_name is not None and window_index is not None:
raise ValueError("switch_window receives the window's name or "
"the window's index, not both.")
if window_name is not None:
self.driver.switch_to.window(window_name)
elif window_index is not None:
self.driver.switch_to.window(
self.driver.window_handles[window_index])
else:
self.driver.switch_to.window(self.driver.window_handles[-1])
def go_to_previous_page(self):
self.driver.back()
def go_to_next_page(self):
self.driver.forward()
def refresh_page(self):
self.driver.refresh()
def go_to_login_page(self):
self.driver.get(self.login_url)
self.is_the_current_page(do_assert=True)

View File

@@ -0,0 +1,81 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 os import listdir
from os.path import isfile
from os.path import join
from re import search
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import tables
class ApiAccessTable(tables.TableRegion):
name = "endpoints"
dropdown = 'Download OpenStack'
def download_rcfile_dropdown(self):
self.driver.find_element_by_partial_link_text(
self.dropdown).click()
@tables.bind_table_action('download_openrc_v2')
def download_openstack_rc_v2(self, download_button):
self.download_rcfile_dropdown()
download_button.click()
@tables.bind_table_action('download_openrc')
def download_openstack_rc_v3(self, download_button):
self.download_rcfile_dropdown()
download_button.click()
class ApiaccessPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "API Access"
@property
def apiaccess_table(self):
return ApiAccessTable(self.driver, self.conf)
def download_openstack_rc_file(self, version, directory, template):
if version == 2:
self.apiaccess_table.download_openstack_rc_v2()
elif version == 3:
self.apiaccess_table.download_openstack_rc_v3()
def list_of_files(self, directory, template):
return [f for f in listdir(directory)
if (isfile(join(directory, f)) and
f.endswith(template))]
def get_credentials_from_file(self, version, directory, template):
self._wait_until(
lambda _: len(self.list_of_files(directory, template)) > 0)
file_name = self.list_of_files(directory, template)[0]
with open(join(directory, file_name)) as cred_file:
content = cred_file.read()
username_re = r'export OS_USERNAME="([^"]+)"'
if version == 2:
tenant_name_re = r'export OS_TENANT_NAME="([^"]+)"'
tenant_id_re = r'export OS_TENANT_ID=(.+)'
elif version == 3:
tenant_name_re = r'export OS_PROJECT_NAME="([^"]+)"'
tenant_id_re = r'export OS_PROJECT_ID=(.+)'
username = search(username_re, content).group(1)
tenant_name = search(tenant_name_re, content).group(1)
tenant_id = search(tenant_id_re, content).group(1)
cred_dict = {'OS_USERNAME': username,
'OS_TENANT_NAME': tenant_name,
'OS_TENANT_ID': tenant_id}
return cred_dict

View File

@@ -0,0 +1,314 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 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
# 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 = 'string:raw'
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.TableRegionNG):
name = "images"
CREATE_IMAGE_FORM_FIELDS = (
"name", "description", "image_file", "kernel", "ramdisk", "format",
"architecture", "min_disk", "min_ram", "visibility", "protected"
)
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
"name", "description",
"volume_size",
"availability_zone")
LAUNCH_INSTANCE_FORM_FIELDS = (
("name", "count", "availability_zone"),
("boot_source_type", "volume_size"),
{
'flavor': menus.InstanceFlavorMenuRegion
},
{
'network': menus.InstanceAvailableResourceMenuRegion
},
)
EDIT_IMAGE_FORM_FIELDS = (
"name", "description", "format", "min_disk",
"min_ram", "visibility", "protected"
)
@tables.bind_table_action_ng('Create Image')
def create_image(self, create_button):
create_button.click()
return forms.FormRegionNG(self.driver, self.conf,
field_mappings=self.CREATE_IMAGE_FORM_FIELDS)
@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_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_ng('Launch')
def launch_instance(self, launch_instance, row):
launch_instance.click()
return forms.WizardFormRegion(
self.driver, self.conf, self.LAUNCH_INSTANCE_FORM_FIELDS)
class ImagesPage(basepage.BaseNavigationPage):
_resource_page_header_locator = (by.By.CSS_SELECTOR,
'hz-resource-panel hz-page-header h1')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
_search_field_locator = (by.By.CSS_SELECTOR,
'magic-search.form-control input.search-input')
_search_button_locator = (by.By.CSS_SELECTOR,
'hz-magic-search-bar span.fa-search')
_search_option_locator = (by.By.CSS_SELECTOR,
'magic-search.form-control span.search-entry')
_search_name_locator_filled = (
by.By.XPATH,
"//*[@id='imageForm-name'][contains(@class,'ng-not-empty')]"
)
_search_checkbox_loaded = (
by.By.CSS_SELECTOR,
"td .themable-checkbox [type='checkbox'] + label[for*='Zactive']"
)
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Images"
@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):
self.wait_until_element_is_visible(self._search_checkbox_loaded)
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):
create_image_form = self.images_table.create_image()
create_image_form.name.text = name
if description is not None:
create_image_form.description.text = description
# TODO(bpokorny): Add this back once the show_multiple_locations
# option is removed from Glance
# create_image_form.source_type.value = image_source_type
if image_source_type == 'url':
if location is None:
create_image_form.image_url.text = \
self.conf.image.http_image
else:
create_image_form.image_url.text = location
else:
create_image_form.image_file.choose(image_file)
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)
self.wait_until_element_is_visible(self._search_name_locator_filled)
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()
self.wait_till_element_disappears(self.wizard_getter)
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)
update_metadata_form = self.images_table.update_metadata(row)
for field_name, value in metadata.items():
update_metadata_form.add_custom_field(field_name, value)
update_metadata_form.submit()
def check_image_details(self, name, dict_with_details):
row = self._get_row_with_image_name(name)
matches = []
description_page = self.images_table.go_to_image_description_page(row)
content = description_page.get_content()
for name, value in content.items():
if name in dict_with_details:
if dict_with_details[name] in value:
matches.append(True)
return matches
def is_image_present(self, name):
return bool(self._get_row_with_image_name(name))
def is_image_active(self, name):
def cell_getter():
row = self._get_row_with_image_name(name)
return row and row.cells[IMAGES_TABLE_STATUS_COLUMN]
return bool(self.images_table.wait_cell_status(cell_getter, 'Active'))
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):
row = self._get_row_with_image_name(name)
create_volume_form = self.images_table.create_volume(row)
if volume_name is not None:
create_volume_form.name.text = volume_name
if description is not None:
create_volume_form.description.text = description
create_volume_form.image_source = name
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()
self.wait_till_element_disappears(self.wizard_getter)
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)
instance_form = self.images_table.launch_instance(row)
instance_form.availability_zone.value = \
self.conf.launch_instances.available_zone
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
instance_form.flavor.transfer_available_resource(flavor)
instance_form.switch_to(instance_page.NETWORKS_STEP_INDEX)
instance_form.network.transfer_available_resource(
instance_page.DEFAULT_NETWORK_TYPE)
instance_form.submit()
instance_form.wait_till_wizard_disappears()

View File

@@ -0,0 +1,203 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
import netaddr
from selenium.common import exceptions
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
class InstancesTable(tables.TableRegion):
name = "instances"
LAUNCH_INSTANCE_FORM_FIELDS = (
("name", "count", "availability_zone"),
("boot_source_type", "volume_size"),
{
'flavor': menus.InstanceFlavorMenuRegion
},
{
'network': menus.InstanceAvailableResourceMenuRegion
},
)
@tables.bind_table_action('launch-ng')
def launch_instance(self, launch_button):
launch_button.click()
return forms.WizardFormRegion(self.driver, self.conf,
self.LAUNCH_INSTANCE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_instance(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class InstancesPage(basepage.BaseNavigationPage):
DEFAULT_FLAVOR = 'm1.tiny'
DEFAULT_COUNT = 1
DEFAULT_BOOT_SOURCE = 'Image'
DEFAULT_VOLUME_NAME = None
DEFAULT_SNAPSHOT_NAME = None
DEFAULT_VOLUME_SNAPSHOT_NAME = None
DEFAULT_VOL_DELETE_ON_INSTANCE_DELETE = True
DEFAULT_SECURITY_GROUP = True
DEFAULT_NETWORK_TYPE = 'shared'
INSTANCES_TABLE_NAME_COLUMN = 'Instance Name'
INSTANCES_TABLE_STATUS_COLUMN = 'Status'
INSTANCES_TABLE_IP_COLUMN = 'IP Address'
INSTANCES_TABLE_IMAGE_NAME_COLUMN = 'Image Name'
SOURCE_STEP_INDEX = 1
FLAVOR_STEP_INDEX = 2
NETWORKS_STEP_INDEX = 3
_search_state_active = (
by.By.XPATH,
"//*[contains(@class,'normal_column')][contains(text(),'Active')]"
)
def __init__(self, driver=None, conf=None):
super().__init__(driver, conf)
self._page_title = "Instances"
def _get_row_with_instance_name(self, name):
for attempt in range(4):
row = self.instances_table.get_row(
self.INSTANCES_TABLE_NAME_COLUMN, name)
if row is not None:
return row
else:
time.sleep(0.5)
return None
def _get_rows_with_instances_names(self, names):
return [
self.instances_table.get_row(
self.INSTANCES_TABLE_IMAGE_NAME_COLUMN, n) for n in names
]
@property
def instances_table(self):
return InstancesTable(self.driver, self.conf)
def is_instance_present(self, name):
return bool(self._get_row_with_instance_name(name))
def create_instance(
self, instance_name,
available_zone=None,
instance_count=DEFAULT_COUNT,
flavor=DEFAULT_FLAVOR,
boot_source=DEFAULT_BOOT_SOURCE,
network_type=DEFAULT_NETWORK_TYPE,
source_name=None,
device_size=None,
vol_delete_on_instance_delete=DEFAULT_VOL_DELETE_ON_INSTANCE_DELETE
):
if not available_zone:
available_zone = self.conf.launch_instances.available_zone
instance_form = self.instances_table.launch_instance()
instance_form.availability_zone.value = available_zone
instance_form.name.text = instance_name
instance_form.count.value = instance_count
instance_form.switch_to(self.SOURCE_STEP_INDEX)
instance_form.boot_source_type.text = boot_source
boot_source = self._get_source_name(instance_form, boot_source,
self.conf.launch_instances)
if not source_name:
source_name = boot_source
menus.InstanceAvailableResourceMenuRegion(
self.driver, self.conf).transfer_available_resource(source_name)
if device_size:
instance_form.volume_size.value = device_size
if vol_delete_on_instance_delete:
self.vol_delete_on_instance_delete_click()
instance_form.switch_to(self.FLAVOR_STEP_INDEX)
instance_form.flavor.transfer_available_resource(flavor)
instance_form.switch_to(self.NETWORKS_STEP_INDEX)
instance_form.network.transfer_available_resource(
self.DEFAULT_NETWORK_TYPE)
instance_form.submit()
instance_form.wait_till_wizard_disappears()
def vol_delete_on_instance_delete_click(self):
locator = (
by.By.XPATH,
'//label[contains(@ng-model, "vol_delete_on_instance_delete")]')
elements = self._get_elements(*locator)
for ele in elements:
if ele.text == 'Yes':
ele.click()
def delete_instance(self, name):
row = self._get_row_with_instance_name(name)
row.mark()
confirm_delete_instances_form = self.instances_table.delete_instance()
confirm_delete_instances_form.submit()
def delete_instances(self, instances_names):
for instance_name in instances_names:
self._get_row_with_instance_name(instance_name).mark()
confirm_delete_instances_form = self.instances_table.delete_instance()
confirm_delete_instances_form.submit()
def is_instance_deleted(self, name):
return self.instances_table.is_row_deleted(
lambda: self._get_row_with_instance_name(name))
def are_instances_deleted(self, instances_names):
return self.instances_table.are_rows_deleted(
lambda: self._get_rows_with_instances_names(instances_names))
def is_instance_active(self, name):
def cell_getter():
row = self._get_row_with_instance_name(name)
return row and row.cells[self.INSTANCES_TABLE_STATUS_COLUMN]
try:
self.wait_until_element_is_visible(self._search_state_active)
except exceptions.TimeoutException:
return False
status = self.instances_table.wait_cell_status(cell_getter,
('Active', 'Error'))
return status == 'Active'
def _get_source_name(self, instance, boot_source, conf):
if 'Image' in boot_source:
return conf.image_name
elif boot_source == 'Volume':
return instance.volume_id, self.DEFAULT_VOLUME_NAME
elif boot_source == 'Instance Snapshot':
return instance.instance_snapshot_id, self.DEFAULT_SNAPSHOT_NAME
elif 'Volume Snapshot' in boot_source:
return (instance.volume_snapshot_id,
self.DEFAULT_VOLUME_SNAPSHOT_NAME)
def get_image_name(self, instance_name):
row = self._get_row_with_instance_name(instance_name)
return row.cells[self.INSTANCES_TABLE_IMAGE_NAME_COLUMN].text
def get_fixed_ipv4(self, name):
row = self._get_row_with_instance_name(name)
ips = row.cells[self.INSTANCES_TABLE_IP_COLUMN].text
for ip in ips.split(','):
if netaddr.valid_ipv4(ip):
return ip

View File

@@ -0,0 +1,82 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class KeypairsTable(tables.TableRegion):
name = "keypairs"
CREATE_KEY_PAIR_FORM_FIELDS = ('name',)
@tables.bind_table_action('create')
def create_keypair(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_KEY_PAIR_FORM_FIELDS)
@tables.bind_row_action('delete')
def delete_keypair(self, delete_button, row):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_table_action('delete')
def delete_keypairs(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class KeypairsPage(basepage.BaseNavigationPage):
KEY_PAIRS_TABLE_ACTIONS = ("create", "import", "delete")
KEY_PAIRS_TABLE_ROW_ACTION = "delete"
KEY_PAIRS_TABLE_NAME_COLUMN = 'Name'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Key Pairs"
def _get_row_with_keypair_name(self, name):
return self.keypairs_table.get_row(self.KEY_PAIRS_TABLE_NAME_COLUMN,
name)
@property
def keypairs_table(self):
return KeypairsTable(self.driver, self.conf)
@property
def delete_keypair_form(self):
return forms.BaseFormRegion(self.driver, self.conf, None)
def is_keypair_present(self, name):
return bool(self._get_row_with_keypair_name(name))
def create_keypair(self, keypair_name):
create_keypair_form = self.keypairs_table.create_keypair()
create_keypair_form.name.text = keypair_name
create_keypair_form.submit()
def delete_keypair(self, name):
row = self._get_row_with_keypair_name(name)
delete_keypair_form = self.keypairs_table.delete_keypair(row)
delete_keypair_form.submit()
def delete_keypairs(self, name):
row = self._get_row_with_keypair_name(name)
row.mark()
delete_keypair_form = self.keypairs_table.delete_keypairs()
delete_keypair_form.submit()

View File

@@ -0,0 +1,37 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 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 tables
class UsageTable(tables.TableRegion):
name = 'project_usage'
class OverviewPage(basepage.BaseNavigationPage):
_date_form_locator = (by.By.ID, 'date_form')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Instance Overview'
@property
def usage_table(self):
return UsageTable(self.driver, self.conf)
@property
def date_form(self):
src_elem = self._get_element(*self._date_form_locator)
return forms.DateFormRegion(self.driver, self.conf, src_elem)

View File

@@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class ServergroupsPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Server Groups'

View File

@@ -0,0 +1,107 @@
# Copyrigh:t 2015 Hewlett-Packard Development Company, L.P
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
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 tables
class FloatingIPTable(tables.TableRegion):
name = 'floating_ips'
FLOATING_IP_ASSOCIATIONS = (
("ip_id", "port_id"))
@tables.bind_table_action('allocate')
def allocate_ip(self, allocate_button):
allocate_button.click()
self.wait_till_spinner_disappears()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_table_action('release')
def release_ip(self, release_button):
release_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('associate')
def associate_ip(self, associate_button, row):
associate_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.FLOATING_IP_ASSOCIATIONS)
@tables.bind_row_action('disassociate')
def disassociate_ip(self, disassociate_button, row):
disassociate_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class FloatingipsPage(basepage.BaseNavigationPage):
FLOATING_IPS_TABLE_IP_COLUMN = 'IP Address'
FLOATING_IPS_TABLE_FIXED_IP_COLUMN = 'Mapped Fixed IP Address'
_floatingips_fadein_popup_locator = (
by.By.CSS_SELECTOR, '.alert.alert-success.alert-dismissable.fade.in>p')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Floating IPs"
def _get_row_with_floatingip(self, floatingip):
return self.floatingips_table.get_row(
self.FLOATING_IPS_TABLE_IP_COLUMN, floatingip)
@property
def floatingips_table(self):
return FloatingIPTable(self.driver, self.conf)
def allocate_floatingip(self):
floatingip_form = self.floatingips_table.allocate_ip()
floatingip_form.submit()
ip = re.compile(r'(([2][5][0-5]\.)|([2][0-4][0-9]\.)'
r'|([0-1]?[0-9]?[0-9]\.)){3}(([2][5][0-5])|'
r'([2][0-4][0-9])|([0-1]?[0-9]?[0-9]))')
match = ip.search((self._get_element(
*self._floatingips_fadein_popup_locator)).text)
floatingip = str(match.group())
return floatingip
def release_floatingip(self, floatingip):
row = self._get_row_with_floatingip(floatingip)
row.mark()
modal_confirmation_form = self.floatingips_table.release_ip()
modal_confirmation_form.submit()
def is_floatingip_present(self, floatingip):
return bool(self._get_row_with_floatingip(floatingip))
def associate_floatingip(self, floatingip, instance_name=None,
instance_ip=None):
row = self._get_row_with_floatingip(floatingip)
floatingip_form = self.floatingips_table.associate_ip(row)
floatingip_form.port_id.text = "{}: {}".format(instance_name,
instance_ip)
floatingip_form.submit()
def disassociate_floatingip(self, floatingip):
row = self._get_row_with_floatingip(floatingip)
floatingip_form = self.floatingips_table.disassociate_ip(row)
floatingip_form.submit()
def get_fixed_ip(self, floatingip):
row = self._get_row_with_floatingip(floatingip)
return row.cells[self.FLOATING_IPS_TABLE_FIXED_IP_COLUMN].text

View File

@@ -0,0 +1,37 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
class NetworkOverviewPage(basepage.BaseNavigationPage):
_network_dd_name_locator = (by.By.CSS_SELECTOR, 'dt[title*="Name"]+dd')
_network_dd_status_locator = (by.By.CSS_SELECTOR, 'dt[title*="Status"]+dd')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Network Details'
self._external_network = conf.network.external_network
def is_network_name_present(self, network_name=None):
if network_name is None:
network_name = self._external_network
dd_text = self._get_element(*self._network_dd_name_locator).text
return dd_text == network_name
def is_network_status(self, status):
dd_text = self._get_element(*self._network_dd_status_locator).text
return dd_text == status

View File

@@ -0,0 +1,124 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class NetworksTable(tables.TableRegion):
name = "networks"
CREATE_NETWORK_FORM_FIELDS = (("net_name", "admin_state",
"with_subnet", "az_hints"),
("subnet_name", "cidr", "ip_version",
"gateway_ip", "no_gateway"),
("enable_dhcp", "allocation_pools",
"dns_nameservers", "host_routes"))
@tables.bind_table_action('create')
def create_network(self, create_button):
create_button.click()
return forms.TabbedFormRegion(self.driver, self.conf,
self.CREATE_NETWORK_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_network(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class NetworksPage(basepage.BaseNavigationPage):
DEFAULT_ADMIN_STATE = 'True'
DEFAULT_CREATE_SUBNET = True
DEFAULT_IP_VERSION = '4'
DEFAULT_DISABLE_GATEWAY = False
DEFAULT_ENABLE_DHCP = True
NETWORKS_TABLE_NAME_COLUMN = 'Name'
NETWORKS_TABLE_STATUS_COLUMN = 'Status'
SUBNET_TAB_INDEX = 1
DETAILS_TAB_INDEX = 2
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Networks"
def _get_row_with_network_name(self, name):
return self.networks_table.get_row(
self.NETWORKS_TABLE_NAME_COLUMN, name)
@property
def is_admin(self):
return False
@property
def networks_table(self):
return NetworksTable(self.driver, self.conf)
def create_network(self, network_name, subnet_name,
admin_state=DEFAULT_ADMIN_STATE,
create_subnet=DEFAULT_CREATE_SUBNET,
network_address=None, ip_version=DEFAULT_IP_VERSION,
gateway_ip=None,
disable_gateway=DEFAULT_DISABLE_GATEWAY,
enable_dhcp=DEFAULT_ENABLE_DHCP, allocation_pools=None,
dns_name_servers=None, host_routes=None,
project='admin', net_type='Local'):
create_network_form = self.networks_table.create_network()
if self.is_admin:
create_network_form.network_type.text = net_type
create_network_form.tenant_id.text = project
create_network_form.name.text = network_name
else:
create_network_form.net_name.text = network_name
create_network_form.admin_state.value = admin_state
if not create_subnet:
create_network_form.with_subnet.unmark()
else:
create_network_form.switch_to(self.SUBNET_TAB_INDEX)
create_network_form.subnet_name.text = subnet_name
if network_address is None:
network_address = self.conf.network.network_cidr
create_network_form.cidr.text = network_address
create_network_form.ip_version.value = ip_version
if gateway_ip is not None:
create_network_form.gateway_ip.text = gateway_ip
if disable_gateway:
create_network_form.disable_gateway.mark()
create_network_form.switch_to(self.DETAILS_TAB_INDEX)
if not enable_dhcp:
create_network_form.enable_dhcp.unmark()
if allocation_pools is not None:
create_network_form.allocation_pools.text = allocation_pools
if dns_name_servers is not None:
create_network_form.dns_nameservers.text = dns_name_servers
if host_routes is not None:
create_network_form.host_routes.text = host_routes
create_network_form.submit()
def delete_network(self, name):
row = self._get_row_with_network_name(name)
row.mark()
confirm_delete_networks_form = self.networks_table.delete_network()
confirm_delete_networks_form.submit()
def is_network_present(self, name):
return bool(self._get_row_with_network_name(name))
def is_network_active(self, name):
def cell_getter():
row = self._get_row_with_network_name(name)
return row and row.cells[self.NETWORKS_TABLE_STATUS_COLUMN]
return bool(self.networks_table.wait_cell_status(cell_getter,
'Active'))

View File

@@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class NetworktopologyPage(basepage.BaseNavigationPage):
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = 'Network Topology'

View File

@@ -0,0 +1,111 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 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 tables
class InterfacesTable(tables.TableRegion):
name = "interfaces"
CREATE_INTERFACE_FORM_FIELDS = ("subnet_id", "ip_address")
@tables.bind_table_action('create')
def create_interface(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver,
self.conf,
field_mappings=self.CREATE_INTERFACE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_interface(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('delete')
def delete_interface_by_row_action(self, delete_button, row):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
class RouterInterfacesPage(basepage.BaseNavigationPage):
INTERFACES_TABLE_STATUS_COLUMN = 'Status'
INTERFACES_TABLE_NAME_COLUMN = 'Name'
INTERFACES_TABLE_FIXED_IPS_COLUMN = 'Fixed IPs'
DEFAULT_IPv4_ADDRESS = '10.100.0.1'
_interface_subnet_selector = (by.By.CSS_SELECTOR, 'div > .themable-select')
_breadcrumb_routers_locator = (
by.By.CSS_SELECTOR,
'ol.breadcrumb>li>' + 'a[href*="/project/routers"]')
def __init__(self, driver, conf, router_name):
super().__init__(driver, conf)
self._page_title = router_name
def _get_row_with_interface_name(self, name):
return self.interfaces_table.get_row(self.INTERFACES_TABLE_NAME_COLUMN,
name)
def _get_row_with_ip_address(self):
return self.interfaces_table.get_row(
self.INTERFACES_TABLE_FIXED_IPS_COLUMN, self.DEFAULT_IPv4_ADDRESS)
@property
def subnet_selector(self):
src_elem = self._get_element(*self._interface_subnet_selector)
return forms.ThemableSelectFormFieldRegion(
self.driver,
self.conf,
src_elem=src_elem,
strict_options_match=False)
@property
def interfaces_table(self):
return InterfacesTable(self.driver, self.conf)
@property
def interface_name(self):
row = self._get_row_with_ip_address()
return row.cells[self.INTERFACES_TABLE_NAME_COLUMN].text
def switch_to_routers_page(self):
self._get_element(*self._breadcrumb_routers_locator).click()
def create_interface(self, subnet):
interface_form = self.interfaces_table.create_interface()
self.subnet_selector.text = subnet
interface_form.ip_address.text = self.DEFAULT_IPv4_ADDRESS
interface_form.submit()
def delete_interface(self, interface_name):
row = self._get_row_with_interface_name(interface_name)
row.mark()
confirm_delete_interface_form = self.interfaces_table.\
delete_interface()
confirm_delete_interface_form.submit()
def delete_interface_by_row_action(self, interface_name):
row = self._get_row_with_interface_name(interface_name)
confirm_delete_interface = self.interfaces_table.\
delete_interface_by_row_action(row)
confirm_delete_interface.submit()
def is_interface_present(self, interface_name):
return bool(self._get_row_with_interface_name(interface_name))
def is_interface_status(self, interface_name, status):
row = self._get_row_with_interface_name(interface_name)
return row.cells[self.INTERFACES_TABLE_STATUS_COLUMN].text == status

View File

@@ -0,0 +1,42 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages.project.network\
.networkoverviewpage import NetworkOverviewPage
class RouterOverviewPage(basepage.BaseNavigationPage):
_network_link_locator = (by.By.CSS_SELECTOR,
'hr+dl.dl-horizontal>dt:nth-child(3)+dd>a')
def __init__(self, driver, conf, router_name):
super().__init__(driver, conf)
self._page_title = router_name
def is_router_name_present(self, router_name):
dd_text = self._get_element(by.By.XPATH,
"//dd[.='{0}']".format(router_name)).text
return dd_text == router_name
def is_router_status(self, status):
dd_text = self._get_element(by.By.XPATH,
"//dd[.='{0}']".format(status)).text
return dd_text == status
def go_to_router_network(self):
self._get_element(*self._network_link_locator).click()
return NetworkOverviewPage(self.driver, self.conf)

View File

@@ -0,0 +1,149 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.common import exceptions
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages.project.network.\
routerinterfacespage import RouterInterfacesPage
from openstack_dashboard.test.integration_tests.pages.project.network\
.routeroverviewpage import RouterOverviewPage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class RoutersTable(tables.TableRegion):
name = "routers"
CREATE_ROUTER_FORM_FIELDS = ("name", "admin_state_up", "external_network")
SET_GATEWAY_FORM_FIELDS = ("network_id",)
@tables.bind_table_action('create')
def create_router(self, create_button):
create_button.click()
return forms.FormRegion(self.driver,
self.conf,
field_mappings=self.CREATE_ROUTER_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_router(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('clear')
def clear_gateway(self, clear_gateway_button, row):
clear_gateway_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('setgateway')
def set_gateway(self, set_gateway_button, row):
set_gateway_button.click()
return forms.FormRegion(self.driver,
self.conf,
field_mappings=self.SET_GATEWAY_FORM_FIELDS)
class RoutersPage(basepage.BaseNavigationPage):
DEFAULT_ADMIN_STATE_UP = 'True'
ROUTERS_TABLE_NAME_COLUMN = 'Name'
ROUTERS_TABLE_STATUS_COLUMN = 'Status'
ROUTERS_TABLE_NETWORK_COLUMN = 'External Network'
_interfaces_tab_locator = (by.By.CSS_SELECTOR,
'a[href*="tab=router_details__interfaces"]')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Routers"
self._external_network = conf.network.external_network
def _get_row_with_router_name(self, name):
return self.routers_table.get_row(self.ROUTERS_TABLE_NAME_COLUMN, name)
@property
def routers_table(self):
return RoutersTable(self.driver, self.conf)
def create_router(self, name, admin_state_up=DEFAULT_ADMIN_STATE_UP):
create_router_form = self.routers_table.create_router()
create_router_form.name.text = name
create_router_form.admin_state_up.value = admin_state_up
create_router_form.external_network.text = self._external_network
create_router_form.submit()
def set_gateway(self, router_id):
row = self._get_row_with_router_name(router_id)
set_gateway_form = self.routers_table.set_gateway(row)
set_gateway_form.network_id.text = self._external_network
set_gateway_form.submit()
def clear_gateway(self, name):
row = self._get_row_with_router_name(name)
confirm_clear_gateway_form = self.routers_table.clear_gateway(row)
confirm_clear_gateway_form.submit()
def delete_router(self, name):
row = self._get_row_with_router_name(name)
row.mark()
confirm_delete_routers_form = self.routers_table.delete_router()
confirm_delete_routers_form.submit()
def is_router_present(self, name):
return bool(self._get_row_with_router_name(name))
def is_router_active(self, name):
row = self._get_row_with_router_name(name)
def cell_getter():
return row.cells[self.ROUTERS_TABLE_STATUS_COLUMN]
try:
self._wait_till_text_present_in_element(cell_getter, 'Active')
except exceptions.TimeoutException:
return False
return True
def is_gateway_cleared(self, name):
row = self._get_row_with_router_name(name)
def cell_getter():
return row.cells[self.ROUTERS_TABLE_NETWORK_COLUMN]
try:
self._wait_till_text_present_in_element(cell_getter, '-')
except exceptions.TimeoutException:
return False
return True
def is_gateway_set(self, name):
row = self._get_row_with_router_name(name)
def cell_getter():
return row.cells[self.ROUTERS_TABLE_NETWORK_COLUMN]
try:
self._wait_till_text_present_in_element(cell_getter,
self._external_network)
except exceptions.TimeoutException:
return False
return True
def go_to_interfaces_page(self, name):
self._get_element(by.By.LINK_TEXT, name).click()
self._get_element(*self._interfaces_tab_locator).click()
return RouterInterfacesPage(self.driver, self.conf, name)
def go_to_overview_page(self, name):
self._get_element(by.By.LINK_TEXT, name).click()
return RouterOverviewPage(self.driver, self.conf, name)

View File

@@ -0,0 +1,74 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
class RulesTable(tables.TableRegion):
name = 'rules'
ADD_RULE_FORM_FIELDS = ("rule_menu", "description", "direction",
"port_or_range", "port", "remote", "cidr")
@tables.bind_table_action('add_rule')
def create_rule(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.ADD_RULE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_rules_by_table(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf, None)
@tables.bind_row_action('delete')
def delete_rule_by_row(self, delete_button, row):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf, None)
class ManageRulesPage(basepage.BaseNavigationPage):
RULES_TABLE_PORT_RANGE_COLUMN = 'Port Range'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Manage Security Group Rules"
def _get_row_with_port_range(self, port):
return self.rules_table.get_row(
self.RULES_TABLE_PORT_RANGE_COLUMN, port)
@property
def rules_table(self):
return RulesTable(self.driver, self.conf)
def create_rule(self, port):
create_rule_form = self.rules_table.create_rule()
create_rule_form.port.text = port
create_rule_form.submit()
def delete_rule(self, port):
row = self._get_row_with_port_range(port)
modal_confirmation_form = self.rules_table.delete_rule_by_row(row)
modal_confirmation_form.submit()
def delete_rules(self, port):
row = self._get_row_with_port_range(port)
row.mark()
modal_confirmation_form = self.rules_table.delete_rules_by_table()
modal_confirmation_form.submit()
def is_port_present(self, port):
return bool(self._get_row_with_port_range(port))

View File

@@ -0,0 +1,79 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
from openstack_dashboard.test.integration_tests.pages.project.network.\
security_groups.managerulespage import ManageRulesPage
class SecurityGroupsTable(tables.TableRegion):
name = "security_groups"
CREATE_SECURITYGROUP_FORM_FIELDS = ("name", "description")
@tables.bind_table_action('create')
def create_group(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver,
self.conf,
field_mappings=self.CREATE_SECURITYGROUP_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_group(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf, None)
@tables.bind_row_action('manage_rules')
def manage_rules(self, manage_rules_button, row):
manage_rules_button.click()
return ManageRulesPage(self.driver, self.conf)
class SecuritygroupsPage(basepage.BaseNavigationPage):
SECURITYGROUPS_TABLE_NAME_COLUMN = 'Name'
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Security Groups"
def _get_row_with_securitygroup_name(self, name):
return self.securitygroups_table.get_row(
self.SECURITYGROUPS_TABLE_NAME_COLUMN, name)
@property
def securitygroups_table(self):
return SecurityGroupsTable(self.driver, self.conf)
def create_securitygroup(self, name, description=None):
create_securitygroups_form = self.securitygroups_table.create_group()
create_securitygroups_form.name.text = name
if description is not None:
create_securitygroups_form.description.text = description
create_securitygroups_form.submit()
if 'Manage Security Group Rules' in self.driver.title:
return ManageRulesPage(self.driver, self.conf)
def delete_securitygroup(self, name):
row = self._get_row_with_securitygroup_name(name)
row.mark()
modal_confirmation_form = self.securitygroups_table.delete_group()
modal_confirmation_form.submit()
def is_securitygroup_present(self, name):
return bool(self._get_row_with_securitygroup_name(name))
def go_to_manage_rules(self, name):
row = self._get_row_with_securitygroup_name(name)
return self.securitygroups_table.manage_rules(row)

View File

@@ -0,0 +1,133 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages.project.volumes.\
volumespage import VolumesPage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class VolumesnapshotsTable(tables.TableRegion):
name = 'volume_snapshots'
marker_name = 'snapshot_marker'
prev_marker_name = 'prev_snapshot_marker'
EDIT_SNAPSHOT_FORM_FIELDS = ("name", "description")
CREATE_VOLUME_FORM_FIELDS = (
"name", "description", "snapshot_source", "size")
@tables.bind_table_action('delete')
def delete_volume_snapshots(self, delete_button):
"""Batch Delete table action."""
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('delete')
def delete_volume_snapshot(self, delete_button, row):
"""Per-entity delete row action."""
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('edit')
def edit_snapshot(self, edit_button, row):
edit_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.EDIT_SNAPSHOT_FORM_FIELDS)
@tables.bind_row_action('create_from_snapshot')
def create_volume(self, create_volume_button, row):
create_volume_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.CREATE_VOLUME_FORM_FIELDS)
class SnapshotsPage(basepage.BaseNavigationPage):
SNAPSHOT_TABLE_NAME_COLUMN = 'Name'
SNAPSHOT_TABLE_STATUS_COLUMN = 'Status'
SNAPSHOT_TABLE_VOLUME_NAME_COLUMN = 'Volume Name'
_volumes_tab_locator = (
by.By.CSS_SELECTOR,
'a[href*="tab=volumes_and_snapshots__volumes_tab"]')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "Volume Snapshots"
@property
def volumesnapshots_table(self):
return VolumesnapshotsTable(self.driver, self.conf)
def switch_to_volumes_tab(self):
self._get_element(*self._volumes_tab_locator).click()
return VolumesPage(self.driver, self.conf)
def _get_row_with_volume_snapshot_name(self, name):
return self.volumesnapshots_table.get_row(
self.SNAPSHOT_TABLE_NAME_COLUMN,
name)
def is_snapshot_present(self, name):
return bool(self._get_row_with_volume_snapshot_name(name))
def delete_volume_snapshot(self, name):
row = self._get_row_with_volume_snapshot_name(name)
confirm_form = self.volumesnapshots_table.delete_volume_snapshot(row)
confirm_form.submit()
def delete_volume_snapshots(self, names):
for name in names:
row = self._get_row_with_volume_snapshot_name(name)
row.mark()
confirm_form = self.volumesnapshots_table.delete_volume_snapshots()
confirm_form.submit()
def is_volume_snapshot_deleted(self, name):
return self.volumesnapshots_table.is_row_deleted(
lambda: self._get_row_with_volume_snapshot_name(name))
def is_volume_snapshot_available(self, name):
def cell_getter():
row = self._get_row_with_volume_snapshot_name(name)
return row and row.cells[self.SNAPSHOT_TABLE_STATUS_COLUMN]
return bool(self.volumesnapshots_table.wait_cell_status(cell_getter,
'Available'))
def get_volume_name(self, snapshot_name):
row = self._get_row_with_volume_snapshot_name(snapshot_name)
return row.cells[self.SNAPSHOT_TABLE_VOLUME_NAME_COLUMN].text
def edit_snapshot(self, name, new_name=None, description=None):
row = self._get_row_with_volume_snapshot_name(name)
snapshot_edit_form = self.volumesnapshots_table.edit_snapshot(row)
if new_name:
snapshot_edit_form.name.text = new_name
if description:
snapshot_edit_form.description.text = description
snapshot_edit_form.submit()
def create_volume_from_snapshot(self, snapshot_name, volume_name=None,
description=None, volume_size=None):
row = self._get_row_with_volume_snapshot_name(snapshot_name)
volume_form = self.volumesnapshots_table.create_volume(row)
if volume_name:
volume_form.name.text = volume_name
if description:
volume_form.description.text = description
if volume_size is None:
volume_size = self.conf.volume.volume_size
volume_form.size.value = volume_size
volume_form.submit()

View File

@@ -0,0 +1,275 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.common import exceptions
from selenium.webdriver.common.by import By
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages.project.compute \
import instancespage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
VOLUME_SOURCE_TYPE = 'Volume'
IMAGE_SOURCE_TYPE = 'Image'
class VolumesTable(tables.TableRegion):
name = 'volumes'
# This form is applicable for volume creation from image only.
# Volume creation from volume requires additional 'volume_source' field
# which is available only in case at least one volume is already present.
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
"name", "description", "volume_source_type", "image_source",
"type", "size", "availability_zone")
EDIT_VOLUME_FORM_FIELDS = ("name", "description")
CREATE_VOLUME_SNAPSHOT_FORM_FIELDS = ("name", "description")
EXTEND_VOLUME_FORM_FIELDS = ("new_size",)
UPLOAD_VOLUME_FORM_FIELDS = ("image_name", "disk_format")
@tables.bind_table_action('create')
def create_volume(self, create_button):
create_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS)
@tables.bind_table_action('delete')
def delete_volume(self, delete_button):
delete_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
@tables.bind_row_action('edit')
def edit_volume(self, edit_button, row):
edit_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.EDIT_VOLUME_FORM_FIELDS)
@tables.bind_row_action('snapshots')
def create_snapshot(self, create_snapshot_button, row):
create_snapshot_button.click()
return forms.FormRegion(
self.driver, self.conf,
field_mappings=self.CREATE_VOLUME_SNAPSHOT_FORM_FIELDS)
@tables.bind_row_action('extend')
def extend_volume(self, extend_button, row):
extend_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.EXTEND_VOLUME_FORM_FIELDS)
@tables.bind_row_action('launch_volume_ng')
def launch_as_instance(self, launch_button, row):
launch_button.click()
return instancespage.LaunchInstanceForm(self.driver, self.conf)
@tables.bind_row_action('upload_to_image')
def upload_volume_to_image(self, upload_button, row):
upload_button.click()
return forms.FormRegion(self.driver, self.conf,
field_mappings=self.UPLOAD_VOLUME_FORM_FIELDS)
@tables.bind_row_action('attachments')
def manage_attachments(self, manage_attachments, row):
manage_attachments.click()
return VolumeAttachForm(self.driver, self.conf)
class VolumesPage(basepage.BaseNavigationPage):
VOLUMES_TABLE_NAME_COLUMN = 'Name'
VOLUMES_TABLE_STATUS_COLUMN = 'Status'
VOLUMES_TABLE_TYPE_COLUMN = 'Type'
VOLUMES_TABLE_SIZE_COLUMN = 'Size'
VOLUMES_TABLE_ATTACHED_COLUMN = 'Attached To'
def __init__(self, driver=None, conf=None):
super().__init__(driver, conf)
self._page_title = "Volumes"
def _get_row_with_volume_name(self, name):
return self.volumes_table.get_row(
self.VOLUMES_TABLE_NAME_COLUMN, name)
def _get_rows_with_volumes_names(self, names):
return [self.volumes_table.get_row(self.VOLUMES_TABLE_NAME_COLUMN, n)
for n in names]
@property
def volumes_table(self):
return VolumesTable(self.driver, self.conf)
def create_volume(self, volume_name, description=None,
volume_source_type=IMAGE_SOURCE_TYPE,
volume_size=None,
volume_source=None):
volume_form = self.volumes_table.create_volume()
volume_form.name.text = volume_name
if description is not None:
volume_form.description.text = description
volume_form.volume_source_type.text = volume_source_type
volume_source_type = self._get_source_name(volume_form,
volume_source_type,
self.conf.launch_instances,
volume_source)
volume_source_type[0].text = volume_source_type[1]
if volume_size is None:
volume_size = self.conf.volume.volume_size
volume_form.size.value = volume_size
if volume_source_type != "Volume":
volume_form.type.value = self.conf.volume.volume_type
volume_form.availability_zone.value = \
self.conf.launch_instances.available_zone
volume_form.submit()
def delete_volume(self, name):
row = self._get_row_with_volume_name(name)
row.mark()
confirm_delete_volumes_form = self.volumes_table.delete_volume()
confirm_delete_volumes_form.submit()
self.wait_till_spinner_disappears()
def delete_volumes(self, volumes_names):
for volume_name in volumes_names:
self._get_row_with_volume_name(volume_name).mark()
confirm_delete_volumes_form = self.volumes_table.delete_volume()
confirm_delete_volumes_form.submit()
self.wait_till_spinner_disappears()
def edit_volume(self, name, new_name=None, description=None):
row = self._get_row_with_volume_name(name)
volume_edit_form = self.volumes_table.edit_volume(row)
if new_name:
volume_edit_form.name.text = new_name
if description:
volume_edit_form.description.text = description
volume_edit_form.submit()
def is_volume_present(self, name):
return bool(self._get_row_with_volume_name(name))
def is_volume_status(self, name, status):
def cell_getter():
row = self._get_row_with_volume_name(name)
return row.cells[self.VOLUMES_TABLE_STATUS_COLUMN]
try:
self._wait_till_text_present_in_element(cell_getter, status)
except exceptions.TimeoutException:
return False
return True
def is_volume_deleted(self, name):
return self.volumes_table.is_row_deleted(
lambda: self._get_row_with_volume_name(name))
def are_volumes_deleted(self, volumes_names):
return self.volumes_table.are_rows_deleted(
lambda: self._get_rows_with_volumes_names(volumes_names))
def _get_source_name(self, volume_form, volume_source_type, conf,
volume_source):
if volume_source_type == IMAGE_SOURCE_TYPE:
return volume_form.image_source, conf.image_name
if volume_source_type == VOLUME_SOURCE_TYPE:
return volume_form.volume_id, volume_source
def create_volume_snapshot(self, volume, snapshot, description='test'):
from openstack_dashboard.test.integration_tests.pages.project.\
volumes.snapshotspage import SnapshotsPage
row = self._get_row_with_volume_name(volume)
snapshot_form = self.volumes_table.create_snapshot(row)
snapshot_form.name.text = snapshot
if description is not None:
snapshot_form.description.text = description
snapshot_form.submit()
return SnapshotsPage(self.driver, self.conf)
def extend_volume(self, name, new_size):
row = self._get_row_with_volume_name(name)
extend_volume_form = self.volumes_table.extend_volume(row)
extend_volume_form.new_size.value = new_size
extend_volume_form.submit()
def upload_volume_to_image(self, name, image_name, disk_format):
row = self._get_row_with_volume_name(name)
upload_volume_form = self.volumes_table.upload_volume_to_image(row)
upload_volume_form.image_name.text = image_name
upload_volume_form.disk_format.value = disk_format
upload_volume_form.submit()
def get_size(self, name):
row = self._get_row_with_volume_name(name)
size = str(row.cells[self.VOLUMES_TABLE_SIZE_COLUMN].text)
return int(''.join(filter(str.isdigit, size)))
def launch_instance(self, name, instance_name, available_zone=None):
row = self._get_row_with_volume_name(name)
instance_form = self.volumes_table.launch_as_instance(row)
if available_zone is None:
available_zone = self.conf.launch_instances.available_zone
instance_form.availability_zone.value = available_zone
instance_form.name.text = instance_name
instance_form.submit()
def get_attach_instance(self, name):
row = self._get_row_with_volume_name(name)
attach_instance = row.cells[self.VOLUMES_TABLE_ATTACHED_COLUMN].text
return attach_instance
def attach_volume_to_instance(self, volume, instance):
row = self._get_row_with_volume_name(volume)
attach_form = self.volumes_table.manage_attachments(row)
attach_form.attach_instance(instance)
def is_volume_attached_to_instance(self, volume, instance):
row = self._get_row_with_volume_name(volume)
return row.cells[
self.VOLUMES_TABLE_ATTACHED_COLUMN].text.endswith(instance)
def detach_volume_from_instance(self, volume, instance):
row = self._get_row_with_volume_name(volume)
attachment_form = self.volumes_table.manage_attachments(row)
detach_form = attachment_form.detach(volume, instance)
detach_form.submit()
class VolumeAttachForm(forms.BaseFormRegion):
_attach_to_instance_selector = (By.CSS_SELECTOR, 'div > .themable-select')
_attachments_table_selector = (By.CSS_SELECTOR, 'table[id="attachments"]')
_detach_template = 'tr[data-display="Volume {0} on instance {1}"] button'
@property
def attachments_table(self):
return self._get_element(*self._attachments_table_selector)
@property
def instance_selector(self):
src_elem = self._get_element(*self._attach_to_instance_selector)
return forms.ThemableSelectFormFieldRegion(
self.driver, self.conf, src_elem=src_elem,
strict_options_match=False)
def detach(self, volume, instance):
detach_button = self.attachments_table.find_element(
By.CSS_SELECTOR, self._detach_template.format(volume, instance))
detach_button.click()
return forms.BaseFormRegion(self.driver, self.conf)
def attach_instance(self, instance_name):
self.instance_selector.text = instance_name
self.submit()

View File

@@ -0,0 +1,52 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import forms
class ChangepasswordPage(basepage.BaseNavigationPage):
_password_form_locator = (by.By.ID, 'change_password_modal')
CHANGE_PASSWORD_FORM_FIELDS = ("current_password", "new_password",
"confirm_password")
@property
def password_form(self):
src_elem = self._get_element(*self._password_form_locator)
return forms.FormRegion(
self.driver, self.conf, src_elem=src_elem,
field_mappings=self.CHANGE_PASSWORD_FORM_FIELDS)
def change_password(self, current, new):
self.password_form.current_password.text = current
self.password_form.new_password.text = new
self.password_form.confirm_password.text = new
self.password_form.submit()
# NOTE(tsufiev): try to apply the same fix as Tempest did for the
# issue of Keystone Fernet tokens lacking sub-second precision
# (in which case it's possible to log in the same second that
# token was revoked due to password change), see bug 1473567
time.sleep(1)
def reset_to_default_password(self, current):
if self.topbar.user.text == self.conf.identity.admin_username:
return self.change_password(current,
self.conf.identity.admin_password)
else:
return self.change_password(current,
self.conf.identity.password)

View File

@@ -0,0 +1,83 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages.settings import \
changepasswordpage
from openstack_dashboard.test.integration_tests.regions import forms
class UsersettingsPage(basepage.BaseNavigationPage):
DEFAULT_LANGUAGE = "en"
DEFAULT_TIMEZONE = "UTC"
DEFAULT_PAGESIZE = "20"
DEFAULT_LOGLINES = "35"
DEFAULT_SETTINGS = {
"language": DEFAULT_LANGUAGE,
"timezone": DEFAULT_TIMEZONE,
"pagesize": DEFAULT_PAGESIZE,
"loglines": DEFAULT_LOGLINES
}
SETTINGS_FORM_FIELDS = (
"language", "timezone", "pagesize", "instance_log_length")
_settings_form_locator = (by.By.ID, 'user_settings_modal')
_change_password_tab_locator = (by.By.CSS_SELECTOR,
'a[href*="/settings/password/"]')
def __init__(self, driver, conf):
super().__init__(driver, conf)
self._page_title = "User Settings"
@property
def settings_form(self):
src_elem = self._get_element(*self._settings_form_locator)
return forms.FormRegion(
self.driver, self.conf, src_elem=src_elem,
field_mappings=self.SETTINGS_FORM_FIELDS)
@property
def changepassword(self):
return changepasswordpage.ChangePasswordPage(self.driver, self.conf)
@property
def change_password_tab(self):
return self._get_element(*self._change_password_tab_locator)
def change_language(self, lang=DEFAULT_LANGUAGE):
self.settings_form.language.value = lang
self.settings_form.submit()
def change_timezone(self, timezone=DEFAULT_TIMEZONE):
self.settings_form.timezone.value = timezone
self.settings_form.submit()
def change_pagesize(self, size=DEFAULT_PAGESIZE):
self.settings_form.pagesize.value = size
self.settings_form.submit()
def change_loglines(self, lines=DEFAULT_LOGLINES):
self.settings_form.instance_log_length.value = lines
self.settings_form.submit()
def return_to_default_settings(self):
self.change_language()
self.change_timezone()
self.change_pagesize()
self.change_loglines()
def go_to_change_password_page(self):
self.change_password_tab.click()
return changepasswordpage.ChangePasswordPage(self.driver, self.conf)

View File

@@ -0,0 +1,61 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
from openstack_dashboard.test.integration_tests.regions import menus
class TopBarRegion(baseregion.BaseRegion):
_user_dropdown_menu_locator = (by.By.CSS_SELECTOR,
'.nav.navbar-nav.navbar-right')
_openstack_brand_locator = (by.By.CSS_SELECTOR, 'a[href*="/home/"]')
_user_dropdown_project_locator = (
by.By.CSS_SELECTOR, '.navbar-collapse > ul.navbar-nav:first-child')
_header_locator = (by.By.CSS_SELECTOR, 'nav.navbar-fixed-top')
MATERIAL_THEME_CLASS = 'material-header'
@property
def user(self):
return self._get_element(*self._user_dropdown_menu_locator)
@property
def brand(self):
return self._get_element(*self._openstack_brand_locator)
@property
def header(self):
return self._get_element(*self._header_locator)
@property
def is_material_theme_enabled(self):
classes = self.header.get_attribute('class').strip().split()
return self.MATERIAL_THEME_CLASS in classes
@property
def user_dropdown_menu(self):
src_elem = self._get_element(*self._user_dropdown_menu_locator)
return menus.UserDropDownMenuRegion(self.driver,
self.conf, src_elem)
@property
def is_logged_in(self):
return self._is_element_visible(*self._user_dropdown_menu_locator)
@property
def user_dropdown_project(self):
src_elem = self._get_element(*self._user_dropdown_project_locator)
return menus.ProjectDropDownRegion(self.driver,
self.conf, src_elem)

View File

@@ -0,0 +1,64 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import basewebobject
class BaseRegion(basewebobject.BaseWebObject):
"""Base class for region module
* there is necessity to override some basic methods for obtaining elements
as in content of regions it is required to do relative searches
* self.driver cannot be easily replaced with self.src_elem because that
would result in functionality loss, self.driver is WebDriver and
src_elem is WebElement its usage is different.
* this does not mean that self.src_elem cannot be self.driver
"""
_default_src_locator = None
# private methods
def __init__(self, driver, conf, src_elem=None):
super().__init__(driver, conf)
if self._default_src_locator:
root = src_elem or driver
src_elem = root.find_element(*self._default_src_locator)
self.src_elem = src_elem or driver
# variable for storing names of dynamic properties and
# associated 'getters' - meaning method that are supplying
# regions or web elements
self._dynamic_properties = {}
def __getattr__(self, name):
# It is not possible to create property bounded just to object
# and not class at runtime, therefore it is necessary to
# override __getattr__ and make fake 'properties' by storing them in
# the protected attribute _dynamic_attributes and returning result
# of the method associated with the specified attribute.
# This way the feeling of having regions accessed as 'properties'
# is created, which is one of the requirement of page object pattern.
try:
return self._dynamic_properties[name]
except KeyError:
msg = "'{0}' object has no attribute '{1}'"
raise AttributeError(msg.format(type(self).__name__, name))
def _get_element(self, *locator):
return self.src_elem.find_element(*locator)
def _get_elements(self, *locator):
return self.src_elem.find_elements(*locator)

View File

@@ -0,0 +1,22 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class BaseRegionException(Exception):
"""Base exception class for region module."""
pass
class UnknownFormFieldTypeException(BaseRegionException):
def __str__(self):
return "No FormField class matched the scope of web content."

View File

@@ -0,0 +1,652 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import os
from django.utils import html
from selenium.common import exceptions
from selenium.webdriver.common import by
import selenium.webdriver.support.ui as Support
from openstack_dashboard.test.integration_tests.regions import baseregion
from openstack_dashboard.test.integration_tests.regions import menus
class FieldFactory(baseregion.BaseRegion):
"""Factory for creating form field objects."""
FORM_FIELDS_TYPES = set()
_element_locator_str_prefix = 'div.form-group'
def __init__(self, driver, conf, src_elem=None):
super().__init__(driver, conf, src_elem)
def fields(self):
for field_cls in self.FORM_FIELDS_TYPES:
locator = (by.By.CSS_SELECTOR,
'%s %s' % (self._element_locator_str_prefix,
field_cls._element_locator_str_suffix))
elements = super()._get_elements(*locator)
for element in elements:
yield field_cls(self.driver, self.conf, src_elem=element)
@classmethod
def register_field_cls(cls, field_class, base_classes=None):
"""Register new field class.
Add new field class and remove all base classes from the set of
registered classes as they should not be in.
"""
cls.FORM_FIELDS_TYPES.add(field_class)
cls.FORM_FIELDS_TYPES -= set(base_classes)
class MetaBaseFormFieldRegion(type):
"""Register form field class in FieldFactory."""
def __init__(cls, name, bases, dct):
FieldFactory.register_field_cls(cls, bases)
super().__init__(name, bases, dct)
class BaseFormFieldRegion(baseregion.BaseRegion,
metaclass=MetaBaseFormFieldRegion):
"""Base class for form fields classes."""
_label_locator = None
_element_locator = None
@property
def label(self):
return self._get_element(*self._label_locator)
@property
def element(self):
return self.src_elem
@property
def name(self):
return self.element.get_attribute('name')
@property
def id(self):
return self.element.get_attribute('id')
def is_required(self):
classes = self.driver.get_attribute('class')
return 'required' in classes
def is_displayed(self):
return self.element.is_displayed()
class CheckBoxMixin(object):
@property
def label(self):
id_attribute = self.element.get_attribute('id')
return self.element.find_element(
by.By.XPATH, '../..//label[@for="{}"]'.format(id_attribute))
def is_marked(self):
return self.element.is_selected()
def mark(self):
if not self.is_marked():
self.label.click()
def unmark(self):
if self.is_marked():
self.label.click()
class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
"""Checkbox field."""
_element_locator_str_suffix = 'input[type=checkbox]'
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
"""Choose file field."""
_element_locator_str_suffix = 'input[type=file]'
def choose(self, path):
self.element.send_keys(os.path.join(os.getcwd(), path))
class BaseTextFormFieldRegion(BaseFormFieldRegion):
_element_locator = None
@property
def text(self):
return self.element.text
@text.setter
def text(self, text):
self._fill_field_element(text, self.element)
class TextInputFormFieldRegion(BaseTextFormFieldRegion):
"""Text input box."""
_element_locator_str_suffix = \
'input[type=text], input[type=None]'
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
"""Password text input box."""
_element_locator_str_suffix = 'input[type=password]'
class EmailInputFormFieldRegion(BaseTextFormFieldRegion):
"""Email text input box."""
_element_locator_str_suffix = 'input[type=email]'
class TextAreaFormFieldRegion(BaseTextFormFieldRegion):
"""Multi-line text input box."""
_element_locator_str_suffix = 'textarea'
class IntegerFormFieldRegion(BaseFormFieldRegion):
"""Integer input box."""
_element_locator_str_suffix = 'input[type=number]'
@property
def value(self):
return self.element.get_attribute("value")
@value.setter
def value(self, value):
self._fill_field_element(value, self.element)
class SelectFormFieldRegion(BaseFormFieldRegion):
"""Select box field."""
_element_locator_str_suffix = 'select.form-control'
def is_displayed(self):
return self.element._el.is_displayed()
@property
def element(self):
return Support.Select(self.src_elem)
@property
def values(self):
results = []
for option in self.element.all_selected_options:
results.append(option.get_attribute('value'))
return results
@property
def options(self):
results = collections.OrderedDict()
for option in self.element.options:
results[option.get_attribute('value')] = option.text
return results
@property
def name(self):
return self.element._el.get_attribute('name')
@property
def id(self):
return self.element._el.get_attribute('id')
@property
def text(self):
return self.element.first_selected_option.text
@text.setter
def text(self, text):
js_cmd = ("$('select option').filter(function() {return $(this).text()"
" == \"%s\";}).prop('selected', true); $('[name=\"%s\"]')."
"change();" % (html.escape(text), self.name))
self.driver.execute_script(js_cmd)
@property
def value(self):
return self.element.first_selected_option.get_attribute('value')
@value.setter
def value(self, value):
js_cmd = "$('[name=\"%s\"]').val(\"%s\").change();" % (
self.name, html.escape(value))
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."""
_element_locator_str_suffix = '.themable-select'
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
def __init__(self, driver, conf, strict_options_match=True, **kwargs):
super().__init__(driver, conf, **kwargs)
self.strict_options_match = strict_options_match
@property
def hidden_element(self):
elem = self._get_element(*self._raw_select_locator)
return SelectFormFieldRegion(self.driver, self.conf, src_elem=elem)
@property
def name(self):
return self.hidden_element.name
@property
def text(self):
return self._get_element(*self._selected_label_locator).text.strip()
@property
def value(self):
return self.hidden_element.value
@property
def options(self):
return self._get_elements(*self._dropdown_menu_locator)
@text.setter
def text(self, text):
if text != self.text:
js_cmd = ("$('select[name=\"%s\"]').closest(\"div\")."
"find(\".btn\").click();" % (self.name))
self.driver.execute_script(js_cmd)
for option in self.options:
if self.strict_options_match:
match = text == str(option.text.strip())
else:
match = text in str(option.text.strip())
if match:
option.click()
return
raise ValueError(
'Widget "%s" does not have an option with text "%s"' %
(self.name, text))
@value.setter
def value(self, value):
if value != self.value:
self.src_elem.click()
for option in self.options:
if value == option.get_attribute('data-select-value'):
option.click()
return
raise ValueError(
'Widget "%s" does not have an option with value "%s"' %
(self.name, value))
class BaseFormRegion(baseregion.BaseRegion):
"""Base class for forms."""
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary,*.btn.btn-danger')
_cancel_locator = (by.By.CSS_SELECTOR, '*.btn.cancel')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
def __init__(self, driver, conf, src_elem=None):
# In most cases forms can be located through _default_form_locator,
# so specifying source element can be skipped.
if src_elem is None:
# fake self.src_elem must be set up in order self._get_element work
self.src_elem = driver
# bind the topmost modal form in a modal stack
src_elem = self._get_elements(*self._default_form_locator)[-1]
super().__init__(driver, conf, src_elem)
@property
def _submit_element(self):
submit_element = self._get_element(*self._submit_locator)
return submit_element
def submit(self):
self._submit_element.click()
self.wait_till_spinner_disappears()
@property
def _cancel_element(self):
return self._get_element(*self._cancel_locator)
def cancel(self):
self._cancel_element.click()
class FormRegion(BaseFormRegion):
"""Standard form."""
_header_locator = (by.By.CSS_SELECTOR, 'div.modal-header > h3')
_side_info_locator = (by.By.CSS_SELECTOR, 'div.right')
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset')
_step_locator = (by.By.CSS_SELECTOR, 'div.step')
# private methods
def __init__(self, driver, conf, src_elem=None, field_mappings=None):
super().__init__(driver, conf, src_elem)
self.field_mappings = self._prepare_mappings(field_mappings)
self.wait_till_spinner_disappears()
self._init_form_fields()
# protected methods
# NOTE: There is a case where a subclass accepts different field_mappings.
# In such case, this method should be overridden.
def _prepare_mappings(self, field_mappings):
return self._format_mappings(field_mappings)
@staticmethod
def _format_mappings(field_mappings):
if isinstance(field_mappings, tuple):
return {item: item for item in field_mappings}
else:
return field_mappings
def _init_form_fields(self):
self.fields_src_elem = self._get_element(*self._fields_locator)
fields = self._get_form_fields()
for accessor_name, accessor_expr in self.field_mappings.items():
if isinstance(accessor_expr, str):
self._dynamic_properties[accessor_name] = fields[accessor_expr]
else: # it is a class
self._dynamic_properties[accessor_name] = accessor_expr(
self.driver, self.conf)
def _get_form_fields(self):
factory = FieldFactory(self.driver, self.conf, self.fields_src_elem)
fields = {}
try:
self._turn_off_implicit_wait()
for field in factory.fields():
if hasattr(field, 'name') and field.name is not None:
fields.update({field.name.replace('-', '_'): field})
elif hasattr(field, 'id') and field.id is not None:
fields.update({field.id.replace('-', '_'): field})
return fields
finally:
self._turn_on_implicit_wait()
def set_field_values(self, data):
"""Set fields values
data - {field_name: field_value, field_name: field_value ...}
"""
for field_name in data:
field = getattr(self, field_name, None)
# Field form does not exist
if field is None:
raise AttributeError("Unknown form field name.")
value = data[field_name]
# if None - default value is left in field
if value is not None:
# all text fields
if hasattr(field, "text"):
field.text = value
# file upload field
elif hasattr(field, "path"):
field.path = value
# integers fields
elif hasattr(field, "value"):
field.value = value
# properties
@property
def header(self):
"""Form header."""
return self._get_element(*self._header_locator)
@property
def sideinfo(self):
"""Right part of form, usually contains description."""
return self._get_element(*self._side_info_locator)
@property
def fields(self):
"""List of all fields that form contains."""
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.
As example is taken form under the
Project/Network/Networks/Create Network, on initialization form needs
to have form field names divided into tuples, that represents the tabs
and the fields located under them.
Usage:
form_field_names = (("network_name", "admin_state"),
("create_subnet", "subnet_name", "network_address",
"ip_version", "gateway_ip", "disable_gateway"),
("enable_dhcp", "allocation_pools", "dns_name_servers",
"host_routes"))
form = TabbedFormRegion(self.conf, self.driver, None, form_field_names)
form.network_name.text = "test_network_name"
"""
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary[type=submit]')
_side_info_locator = (by.By.CSS_SELECTOR, "td.help_text")
def __init__(self, driver, conf, field_mappings=None, default_tab=0):
self.current_tab = default_tab
super().__init__(driver, conf, field_mappings=field_mappings)
def _prepare_mappings(self, field_mappings):
return [
self._format_mappings(tab_mappings)
for tab_mappings in field_mappings
]
def _init_form_fields(self):
self.switch_to(self.current_tab)
def _init_tab_fields(self, tab_index):
fieldsets = self._get_elements(*self._fields_locator)
self.fields_src_elem = fieldsets[tab_index]
fields = self._get_form_fields()
current_tab_mappings = self.field_mappings[tab_index]
for accessor_name, accessor_expr in current_tab_mappings.items():
if isinstance(accessor_expr, str):
self._dynamic_properties[accessor_name] = fields[accessor_expr]
else: # it is a class
self._dynamic_properties[accessor_name] = accessor_expr(
self.driver, self.conf)
def switch_to(self, tab_index=0):
self.tabs.switch_to(index=tab_index)
self._init_tab_fields(tab_index)
@property
def tabs(self):
return menus.TabbedMenuRegion(self.driver,
self.conf,
src_elem=self.src_elem)
class WizardFormRegion(FormRegion):
"""Form consists of sequence of steps."""
_submit_locator = (by.By.CSS_SELECTOR,
'*.btn.btn-primary.finish[type=button]')
def __init__(self, driver, conf, field_mappings=None, default_step=0):
self.current_step = default_step
super().__init__(driver, conf, field_mappings=field_mappings)
def _form_getter(self):
return self.driver.find_element(*self._default_form_locator)
def _prepare_mappings(self, field_mappings):
return [
self._format_mappings(step_mappings)
for step_mappings in field_mappings
]
def _init_form_fields(self):
self.switch_to(self.current_step)
def _init_step_fields(self, step_index):
steps = self._get_elements(*self._step_locator)
self.fields_src_elem = steps[step_index]
fields = self._get_form_fields()
current_step_mappings = self.field_mappings[step_index]
for accessor_name, accessor_expr in current_step_mappings.items():
if isinstance(accessor_expr, str):
self._dynamic_properties[accessor_name] = fields[accessor_expr]
else: # it is a class
self._dynamic_properties[accessor_name] = accessor_expr(
self.driver, self.conf)
def switch_to(self, step_index=0):
self.steps.switch_to(index=step_index)
self._init_step_fields(step_index)
def wait_till_wizard_disappears(self):
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
@property
def steps(self):
return menus.WizardMenuRegion(self.driver,
self.conf,
src_elem=self.src_elem)
class DateFormRegion(BaseFormRegion):
"""Form that queries data to table that is regularly below the form.
A typical example is located on Project/Compute/Overview page.
"""
_from_field_locator = (by.By.CSS_SELECTOR, 'input#id_start')
_to_field_locator = (by.By.CSS_SELECTOR, 'input#id_end')
@property
def from_date(self):
return self._get_element(*self._from_field_locator)
@property
def to_date(self):
return self._get_element(*self._to_field_locator)
def query(self, start, end):
self._set_from_field(start)
self._set_to_field(end)
self.submit()
def _set_from_field(self, value):
self._fill_field_element(value, self.from_date)
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().__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))

View File

@@ -0,0 +1,468 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
from selenium.common import exceptions
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
class NavigationAccordionRegion(baseregion.BaseRegion):
"""Navigation menu located in the left."""
_project_security_groups_locator = (
by.By.CSS_SELECTOR, 'a[href*="/project/security_groups/"]')
_project_key_pairs_locator = (
by.By.CSS_SELECTOR, 'a[href*="/project/key_pairs/"]')
_settings_change_password_locator = (
by.By.CSS_SELECTOR, 'a[href*="/settings/password//"]')
_project_bar_locator = (by.By.XPATH,
".//*[@id='main_content']//div[contains(text(),"
"'Project')]")
@property
def project_bar(self):
return self._get_element(*self._project_bar_locator)
_first_level_item_selected_locator = (
by.By.CSS_SELECTOR, '.panel.openstack-dashboard > a:not(.collapsed)')
_second_level_item_selected_locator = (
by.By.CSS_SELECTOR, '.panel.openstack-panel-group > a:not(.collapsed)')
_first_level_item_xpath_template = (
"//li[contains(concat('', @class, ''), 'panel openstack-dashboard') "
"and contains(., '%s')]/a")
_second_level_item_xpath_template = (
"//ul[contains(@class, 'in')]//li[contains(@class, "
"'panel openstack-panel-group') and contains(., '%s')]/a")
_third_level_item_xpath_template = (
"//ul[contains(@class, 'in')]//a[contains(concat('', @class, ''),"
"'list-group-item openstack-panel') and contains(., '%s')]")
_parent_item_locator = (by.By.XPATH, '..')
_menu_list_locator = (by.By.CSS_SELECTOR, 'a')
_expanded_menu_class = ""
_transitioning_menu_class = 'collapsing'
_form_body_locator = (by.By.CSS_SELECTOR, 'body')
def _get_first_level_item_locator(self, text):
return (by.By.XPATH,
self._first_level_item_xpath_template % text)
def _get_second_level_item_locator(self, text):
return (by.By.XPATH,
self._second_level_item_xpath_template % text)
def _get_third_level_item_locator(self, text):
return (by.By.XPATH,
self._third_level_item_xpath_template % text)
def get_first_level_selected_item(self):
if self._is_element_present(*self._first_level_item_selected_locator):
return self._get_element(*self._first_level_item_selected_locator)
else:
return None
def get_second_level_selected_item(self):
if self._is_element_present(*self._second_level_item_selected_locator):
return self._get_element(*self._second_level_item_selected_locator)
else:
return None
@property
def get_form_body(self):
return self._get_element(*self._form_body_locator)
@property
def security_groups(self):
return self._get_element(*self._project_security_groups_locator)
@property
def key_pairs(self):
return self._get_element(*self._project_key_pairs_locator)
@property
def change_password(self):
return self._get_element(*self._settings_change_password_locator)
def _wait_until_transition_ends(self, item, to_be_expanded=False):
def predicate(d):
classes = item.get_attribute('class')
if to_be_expanded:
status = self._expanded_menu_class == classes
else:
status = self._expanded_menu_class is not classes
return status and self._transitioning_menu_class not in classes
self._wait_until(predicate)
def _wait_until_modal_dialog_close(self):
def predicate(d):
classes = self.get_form_body.get_attribute('class')
return classes and "modal-open" not in classes
self._wait_until(predicate)
def _click_menu_item(self, text, loc_craft_func, get_selected_func=None,
src_elem=None):
"""Click on menu item if not selected.
Menu animation that visualize transition from one selection to
another take some time - if clicked on item during this animation
nothing happens, therefore it is necessary to wait for the transition
to complete first.
Third-level menus are handled differently from others. First, they do
not need to collapse other 3rd-level menu item prior to clicking them
(because 3rd-level menus are atomic). Second, clicking them doesn't
initiate an animated transition, hence no need to wait until it
finishes. Also an ambiguity is possible when matching third-level menu
items, because different dashboards have panels with the same name (for
example Volumes/Images/Instances both at Project and Admin dashboards).
To avoid this ambiguity, an argument `src_elem` is used. Whenever
dashboard or a panel group is clicked, its wrapper is returned to be
used as `src_elem` in a subsequent call for clicking third-level item.
This way the set of panel labels being matched is restricted to the
descendants of that particular dashboard or panel group.
"""
is_already_within_required_item = False
selected_item = None
self._wait_until_modal_dialog_close()
if get_selected_func is not None:
selected_item = get_selected_func()
if selected_item:
if text != selected_item.text and selected_item.text:
# In case different item was chosen previously, collapse
# it. Otherwise selenium will complain with
# MoveTargetOutOfBoundsException
selected_item.click()
time.sleep(1)
self._wait_until_transition_ends(
self._get_menu_list_next_to_menu_title(selected_item))
else:
is_already_within_required_item = True
# In case menu item is collapsed, then below code detects the same
# and disable is_already_within_required_item flag.
# For instance, in case of tacker-horizon, selenium report an
# exception ElementNotInteractableException while opening pages
# under 'NFV Orchestration' panel group. This error occurs when
# element is not clickable or it is not visible yet.
# The panel group 'NFV Orchestration' is never clicked/open hence
# requested pages are not visible/clickable.
item = self._get_item(text, loc_craft_func, src_elem)
if "collapsed" == item.get_attribute(
'class') and is_already_within_required_item is True:
is_already_within_required_item = False
if not is_already_within_required_item:
item.click()
if get_selected_func is not None:
self._wait_until_transition_ends(
self._get_menu_list_next_to_menu_title(item),
to_be_expanded=True)
return item
return selected_item
def _get_item(self, text, loc_craft_func, src_elem=None):
item_locator = loc_craft_func(text)
src_elem = src_elem or self.src_elem
return src_elem.find_element(*item_locator)
def _get_menu_list_next_to_menu_title(self, title_item):
parent_item = title_item.find_element(*self._parent_item_locator)
return parent_item.find_element(*self._menu_list_locator)
def click_on_menu_items(self, first_level=None,
second_level=None,
third_level=None):
src_elem = None
if first_level:
src_elem = self._click_menu_item(
first_level,
self._get_first_level_item_locator,
self.get_first_level_selected_item)
if second_level:
src_elem = self._click_menu_item(
second_level,
self._get_second_level_item_locator,
self.get_second_level_selected_item)
if third_level:
# NOTE(tsufiev): possible dashboard/panel group label passed as
# `src_elem` is a sibling of <ul> with all the panels it contains.
# So to get the panel within specified dashboard/panel group, we
# need to traverse upwards first. When `src_elem` is not specified
# (true for Settings pseudo-dashboard), we cannot and should not
# go upward.
if src_elem:
src_elem = src_elem.find_element(*self._parent_item_locator)
self._click_menu_item(third_level,
self._get_third_level_item_locator,
src_elem=src_elem)
class DropDownMenuRegion(baseregion.BaseRegion):
"""Drop down menu region."""
_menu_container_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu')
_menu_items_locator = (by.By.CSS_SELECTOR,
'ul.dropdown-menu > li > *')
_dropdown_locator = (by.By.CSS_SELECTOR, '.dropdown > a')
_active_cls = 'dropdown-toggle'
@property
def menu_items(self):
self.open()
menu_items = self._get_elements(*self._menu_items_locator)
return menu_items
def is_open(self):
"""Returns True if drop down menu is open, otherwise False."""
return "open" in self.src_elem.get_attribute('class')
def open(self):
"""Opens menu by clicking on the first child of the source element."""
if self.is_open() is False:
dropdown = self._get_element(*self._dropdown_locator)
# NOTE(tsufiev): there is an issue with clicking dropdowns too fast
# after page has been loaded - the Bootstrap constructors haven't
# completed yet, so the dropdown never opens in that case. Avoid
# this by waiting for a specific class to appear, which is set in
# horizon.selenium.js for dropdowns after a timeout passes
def predicate(d):
classes = dropdown.get_attribute('class').split()
return self._active_cls in classes
self._wait_until(predicate)
dropdown.click()
self._wait_till_element_visible(self._menu_container_locator)
class UserDropDownMenuRegion(DropDownMenuRegion):
"""Drop down menu located in the right side of the topbar.
This menu contains links to settings and help.
"""
_settings_link_locator = (by.By.CSS_SELECTOR,
'a[href*="/settings/"]')
_help_link_locator = (by.By.CSS_SELECTOR,
'ul#editor_list li:nth-of-type(2) > a')
_logout_link_locator = (by.By.CSS_SELECTOR,
'a[href*="/auth/logout/"]')
def _theme_picker_locator(self, theme_name):
return (by.By.CSS_SELECTOR,
'.theme-picker-item[data-theme="%s"]' % theme_name)
@property
def settings_link(self):
return self._get_element(*self._settings_link_locator)
@property
def help_link(self):
return self._get_element(*self._help_link_locator)
@property
def logout_link(self):
return self._get_element(*self._logout_link_locator)
def theme_picker_link(self, theme_name):
return self._get_element(*self._theme_picker_locator(theme_name))
def click_on_settings(self):
self.open()
self.settings_link.click()
def click_on_help(self):
self.open()
self.help_link.click()
def choose_theme(self, theme_name):
self.open()
self.theme_picker_link(theme_name).click()
def click_on_logout(self):
self.open()
self.logout_link.click()
class TabbedMenuRegion(baseregion.BaseRegion):
_tab_locator = (by.By.CSS_SELECTOR, 'li > a')
_default_src_locator = (by.By.CSS_SELECTOR, 'div > .nav.nav-pills')
def switch_to(self, index=0):
self._get_elements(*self._tab_locator)[index].click()
class WizardMenuRegion(baseregion.BaseRegion):
_step_locator = (by.By.CSS_SELECTOR, 'li > a')
_default_src_locator = (by.By.CSS_SELECTOR, 'div > .nav.nav-pills')
def switch_to(self, index=0):
self._get_elements(*self._step_locator)[index].click()
class ProjectDropDownRegion(DropDownMenuRegion):
_menu_items_locator = (
by.By.CSS_SELECTOR, 'ul.context-selection li > a')
def click_on_project(self, name):
for item in self.menu_items:
if item.text == name:
item.click()
break
else:
raise exceptions.NoSuchElementException(
"Not found element with text: %s" % name)
class MembershipMenuRegion(baseregion.BaseRegion):
_available_members_locator = (
by.By.CSS_SELECTOR, 'ul.available_members > ul.btn-group')
_allocated_members_locator = (
by.By.CSS_SELECTOR, 'ul.members > ul.btn-group')
_add_remove_member_sublocator = (
by.By.CSS_SELECTOR, 'li > a[href="#add_remove"]')
_member_name_sublocator = (
by.By.CSS_SELECTOR, 'li.member > span.display_name')
_member_roles_widget_sublocator = (by.By.CSS_SELECTOR, 'li.role_options')
_member_roles_widget_open_subsublocator = (by.By.CSS_SELECTOR, 'a.btn')
_member_roles_widget_roles_subsublocator = (
by.By.CSS_SELECTOR, 'ul.role_dropdown > li')
def _get_member_name(self, element):
return element.find_element(*self._member_name_sublocator).text
@property
def available_members(self):
return {self._get_member_name(el): el for el in
self._get_elements(*self._available_members_locator)}
@property
def allocated_members(self):
return {self._get_member_name(el): el for el in
self._get_elements(*self._allocated_members_locator)}
def allocate_member(self, name, available_members=None):
# NOTE(tsufiev): available_members here (and allocated_members below)
# are meant to be used for performance optimization to reduce the
# amount of calls to selenium by reusing still valid element reference
if available_members is None:
available_members = self.available_members
available_members[name].find_element(
*self._add_remove_member_sublocator).click()
def deallocate_member(self, name, allocated_members=None):
if allocated_members is None:
allocated_members = self.allocated_members
allocated_members[name].find_element(
*self._add_remove_member_sublocator).click()
def _get_member_roles_widget(self, name, allocated_members=None):
if allocated_members is None:
allocated_members = self.allocated_members
return allocated_members[name].find_element(
*self._member_roles_widget_sublocator)
def _get_member_all_roles(self, name, allocated_members=None):
roles_widget = self._get_member_roles_widget(name, allocated_members)
return roles_widget.find_elements(
*self._member_roles_widget_roles_subsublocator)
@staticmethod
def _is_role_selected(role):
return 'selected' == role.get_attribute('class')
def get_member_available_roles(self, name, allocated_members=None,
strip=True):
roles = self._get_member_all_roles(name, allocated_members)
return [(role.text.strip() if strip else role)
for role in roles if not self._is_role_selected(role)]
def get_member_allocated_roles(self, name, allocated_members=None,
strip=True):
self.open_member_roles_dropdown(name, allocated_members)
roles = self._get_member_all_roles(name, allocated_members)
return [(role.text.strip() if strip else role)
for role in roles if self._is_role_selected(role)]
def open_member_roles_dropdown(self, name, allocated_members=None):
widget = self._get_member_roles_widget(name, allocated_members)
button = widget.find_element(
*self._member_roles_widget_open_subsublocator)
button.click()
def _switch_member_roles(self, name, roles2toggle, method,
allocated_members=None):
self.open_member_roles_dropdown(name, allocated_members)
roles = method(name, allocated_members, False)
roles2toggle = set(roles2toggle)
for role in roles:
role_name = role.text.strip()
if role_name in roles2toggle:
role.click()
roles2toggle.remove(role_name)
if not roles2toggle:
break
def allocate_member_roles(self, name, roles2add, allocated_members=None):
self._switch_member_roles(
name, roles2add, self.get_member_available_roles,
allocated_members=allocated_members)
def deallocate_member_roles(self, name, roles2remove,
allocated_members=None):
self._switch_member_roles(
name, roles2remove, self.get_member_allocated_roles,
allocated_members=allocated_members)
class InstanceAvailableResourceMenuRegion(baseregion.BaseRegion):
_available_table_locator = (
by.By.CSS_SELECTOR,
'div.step:not(.ng-hide) div.transfer-available table')
_available_table_row_locator = (by.By.CSS_SELECTOR,
"tbody > tr.ng-scope:not(.detail-row)")
_available_table_column_locator = (by.By.TAG_NAME, "td")
_action_column_btn_locator = (by.By.CSS_SELECTOR,
"td.actions_column button")
def transfer_available_resource(self, resource_name):
available_table = self._get_element(*self._available_table_locator)
rows = available_table.find_elements(
*self._available_table_row_locator)
for row in rows:
cols = row.find_elements(*self._available_table_column_locator)
if len(cols) > 1 and self._get_column_text(cols) in resource_name:
row_selector_btn = row.find_element(
*self._action_column_btn_locator)
row_selector_btn.click()
break
def _get_column_text(self, cols):
return cols[2].text.strip()
class InstanceFlavorMenuRegion(InstanceAvailableResourceMenuRegion):
def _get_column_text(self, cols):
return cols[1].text.strip()

View File

@@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
ERROR = 'alert-danger'
INFO = 'alert-info'
SUCCESS = 'alert-success'
WARNING = 'alert-warning'
class MessageRegion(baseregion.BaseRegion):
_close_locator = (by.By.CSS_SELECTOR, 'a.close')
def __init__(self, driver, conf, src_elem):
self.src_elem = src_elem
self.message_class = self.get_message_class()
def exists(self):
return self._is_element_displayed(self.src_elem)
def close(self):
self._get_element(*self._close_locator).click()
def get_message_class(self):
message_class = self.src_elem.get_attribute("class")
if SUCCESS in message_class:
return SUCCESS
elif ERROR in message_class:
return ERROR
elif INFO in message_class:
return INFO
elif WARNING in message_class:
return WARNING
else:
return "Unknown"

View File

@@ -0,0 +1,520 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
from django.utils import html
from selenium.common import exceptions
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.regions import baseregion
NORMAL_COLUMN_CLASS = 'normal_column'
class RowRegion(baseregion.BaseRegion):
"""Classic table row."""
_cell_locator = (by.By.CSS_SELECTOR, 'td.%s' % NORMAL_COLUMN_CLASS)
_row_checkbox_locator = (
by.By.CSS_SELECTOR,
'td .themable-checkbox [type="checkbox"] + label'
)
def __init__(self, driver, conf, src_elem, column_names):
self.column_names = column_names
super().__init__(driver, conf, src_elem)
@property
def cells(self):
elements = self._get_elements(*self._cell_locator)
return {column_name: elements[i]
for i, column_name in enumerate(self.column_names)}
def mark(self):
chck_box = self._get_element(*self._row_checkbox_locator)
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."""
_heading_locator = (by.By.CSS_SELECTOR, 'h3.table_title')
_columns_names_locator = (by.By.CSS_SELECTOR, 'thead > tr > th')
_footer_locator = (by.By.CSS_SELECTOR, 'tfoot > tr > td > span')
_rows_locator = (by.By.CSS_SELECTOR, 'tbody > tr')
_empty_table_locator = (by.By.CSS_SELECTOR, 'tbody > tr.empty')
_search_field_locator = (by.By.CSS_SELECTOR,
'div.table_search input.form-control')
_search_button_locator = (by.By.CSS_SELECTOR,
'div.table_search > button')
_search_option_locator = (by.By.CSS_SELECTOR,
'div.table_search > .themable-select')
_cell_progress_bar_locator = (by.By.CSS_SELECTOR, 'div.progress-bar')
_warning_cell_locator = (by.By.CSS_SELECTOR, 'td.warning')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
marker_name = 'marker'
prev_marker_name = 'prev_marker'
def _table_locator(self, table_name):
return by.By.CSS_SELECTOR, 'table#%s' % table_name
@property
def _next_locator(self):
return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.marker_name
@property
def _prev_locator(self):
return by.By.CSS_SELECTOR, 'a[href^="?%s"]' % self.prev_marker_name
def _search_menu_value_locator(self, value):
return (by.By.CSS_SELECTOR,
'ul.dropdown-menu a[data-select-value="%s"]' % value)
def _cell_progress_bar_getter(self):
return self.driver.find_element(*self._cell_progress_bar_locator)
def _warning_cell_getter(self):
return self.driver.find_element(*self._warning_cell_locator)
def _form_getter(self):
return self.driver.find_element(*self._default_form_locator)
def __init__(self, driver, conf):
self._default_src_locator = self._table_locator(self.__class__.name)
super().__init__(driver, conf)
@property
def heading(self):
return self._get_element(*self._heading_locator)
@property
def rows(self):
if self._is_element_present(*self._empty_table_locator):
return []
else:
return self._get_rows()
@property
def column_names(self):
names = []
for element in self._get_elements(*self._columns_names_locator):
classes = element.get_attribute('class').split()
if NORMAL_COLUMN_CLASS in classes:
names.append(element.text)
return names
@property
def footer(self):
return self._get_element(*self._footer_locator)
def filter(self, value):
self._set_search_field(value)
self._click_search_btn()
self.driver.implicitly_wait(5)
def set_filter_value(self, value):
self.wait_till_element_disappears(self._form_getter)
js_cmd = ("$('ul.dropdown-menu').find(\"a[data-select-value='%s']\")."
"click();" % (html.escape(value)))
self.driver.execute_script(js_cmd)
def get_row(self, column_name, text, exact_match=True):
"""Get row that contains specified text in specified column.
In case exact_match is set to True, text contained in row must equal
searched text, otherwise occurrence of searched text in the column
text will result in row match.
"""
def get_text(element):
text = element.get_attribute('data-selenium')
return text or element.text
# wait until cells actions are completed eg: downloading image,
# uploading image, creating, deleting etc.
self.wait_till_element_disappears(self._cell_progress_bar_getter)
self.wait_till_element_disappears(self._warning_cell_getter)
for row in self.rows:
try:
cell = row.cells[column_name]
if exact_match and text == get_text(cell):
return row
if not exact_match and text in get_text(cell):
return row
# NOTE(tsufiev): if a row was deleted during iteration
except exceptions.StaleElementReferenceException:
pass
return None
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 _get_rows(self, *args):
return [RowRegion(self.driver, self.conf, elem, self.column_names)
for elem in self._get_elements(*self._rows_locator)]
def _is_row_deleted(self, evaluator):
def predicate(driver):
if self._is_element_present(*self._empty_table_locator):
return True
with self.waits_disabled():
return evaluator()
try:
self._wait_until(predicate)
except exceptions.TimeoutException:
return False
except IndexError:
return True
return True
def is_row_deleted(self, row_getter):
return self._is_row_deleted(
lambda: not self._is_element_displayed(row_getter()))
def are_rows_deleted(self, rows_getter):
# wait until rows are deleted.
self.wait_till_element_disappears(self._warning_cell_getter)
return self._is_row_deleted(
lambda: all([not self._is_element_displayed(row) for row
in rows_getter()]))
def wait_cell_status(self, cell_getter, statuses):
if not isinstance(statuses, (list, tuple)):
statuses = (statuses,)
try:
return self._wait_till_text_present_in_element(cell_getter,
statuses)
except exceptions.TimeoutException:
return False
def is_next_link_available(self):
try:
self._turn_off_implicit_wait()
return self._is_element_visible(*self._next_locator)
finally:
self._turn_on_implicit_wait()
def is_prev_link_available(self):
try:
self._turn_off_implicit_wait()
return self._is_element_visible(*self._prev_locator)
finally:
self._turn_on_implicit_wait()
def turn_next_page(self):
if self.is_next_link_available():
lnk = self._get_element(*self._next_locator)
lnk.click()
def turn_prev_page(self):
if self.is_prev_link_available():
lnk = self._get_element(*self._prev_locator)
lnk.click()
def get_column_data(self, name_column='Name'):
return [row.cells[name_column].text for row in self.rows]
def assert_definition(self,
expected_table_definition,
sorting=False,
name_column='Name'):
"""Checks that actual table is expected one.
Items to compare: 'next' and 'prev' links, count of rows and names of
elements in list
:param expected_table_definition: expected values (dictionary)
:param sorting: boolean arg specifying whether to sort actual names
:return:
"""
names = self.get_column_data(name_column)
if sorting:
names.sort()
actual_table = {'Next': self.is_next_link_available(),
'Prev': self.is_prev_link_available(),
'Count': len(self.rows),
'Names': names}
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.
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, 'div.table_actions > button, a')
def decorator(method):
@functools.wraps(method)
def wrapper(table):
actions = table._get_elements(*_actions_locator)
action_element = None
for action in actions:
target_action_id = '%s__action_%s' % (table.name, action_name)
if action.get_attribute('id') == target_action_id:
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_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.
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.
"""
# NOTE(tsufiev): button tag could be either <a> or <button> - target
# both with *. Also primary action could be single as well, do not use
# .btn-group because of that
primary_action_locator = (
by.By.CSS_SELECTOR, 'td.actions_column *.btn:nth-child(1)')
secondary_actions_opener_locator = (
by.By.CSS_SELECTOR,
'td.actions_column > .btn-group > *.btn:nth-child(2)')
secondary_actions_locator = (
by.By.CSS_SELECTOR,
'td.actions_column > .btn-group > ul.row_actions > li > a, button')
def decorator(method):
@functools.wraps(method)
def wrapper(table, row):
def find_action(element):
pattern = "__action_%s" % action_name
return element.get_attribute('id').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_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 -> Instances, Admin
-> System -> Flavors.
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
def bind_row_anchor_column_ng(column_name):
"""A decorator to bind table region method to a anchor in a column.
This decorator works with angular-based tables.
Typical examples of such tables are Project -> Compute -> Images,
Admin -> Compute -> Images.
The method can be used to follow the link in the anchor by the click.
"""
def decorator(method):
@functools.wraps(method)
def wrapper(table, row):
cell = row.cells[column_name]
action_element = cell.find_element(
by.By.CSS_SELECTOR, 'td > hz-cell > a')
return method(table, action_element, row)
return wrapper
return decorator

View File

@@ -0,0 +1,21 @@
{
"namespace": "1A_TestNamespace",
"display_name": "1A_TestNamespace",
"description": "Description for TestNamespace",
"resource_type_associations": [
{
"name": "OS::Nova::Flavor"
},
{
"name": "OS::Glance::Image"
}
],
"properties": {
"prop1": {
"default": "20",
"type": "integer",
"description": "More info here",
"title": "My property1"
}
}
}

View File

@@ -0,0 +1,61 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 os import listdir
from os.path import join
from os import remove
from horizon.test import firefox_binary
from openstack_dashboard.test.integration_tests import helpers
class TestDownloadRCFile(helpers.AdminTestCase):
_directory = firefox_binary.WebDriver.TEMPDIR
_openrc_template = "-openrc.sh"
def setUp(self):
super().setUp()
username = self.TEST_USER_NAME
tenant_name = self.HOME_PROJECT
projects_page = self.home_pg.go_to_identity_projectspage()
tenant_id = projects_page.get_project_id_from_row(tenant_name)
self.actual_dict = {'OS_USERNAME': username,
'OS_TENANT_NAME': tenant_name,
'OS_TENANT_ID': tenant_id}
def cleanup():
temporary_files = listdir(self._directory)
if len(temporary_files):
remove(join(self._directory, temporary_files[0]))
self.addCleanup(cleanup)
def test_download_rc_v3_file(self):
"""This is a basic scenario test:
Steps:
1) Login to Horizon Dashboard as admin user
2) Navigate to Project > API Access tab
3) Click on "Download OpenStack RC File" dropdown button
4) Click on "OpenStack RC File (Identity API v3" button
5) File named by template "<tenant_name>-openrc.sh" must be downloaded
6) Check that username, project name and project id correspond to
current username, tenant name and tenant id
"""
api_access_page = self.home_pg. \
go_to_project_apiaccesspage()
api_access_page.download_openstack_rc_file(
3, self._directory, self._openrc_template)
cred_dict = api_access_page.get_credentials_from_file(
3, self._directory, self._openrc_template)
self.assertEqual(cred_dict, self.actual_dict)

View File

@@ -0,0 +1,71 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import random
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestDefaults(helpers.AdminTestCase):
def setUp(self):
super().setUp()
self.defaults_page = self.home_pg.go_to_admin_system_defaultspage()
self.add_up = random.randint(1, 10)
def test_update_compute_defaults(self):
"""Tests the Update Default Compute Quotas functionality:
1) Login as Admin and go to Admin > System > Defaults
2) Updates default compute Quotas by adding a random
number between 1 and 10
3) Verifies that the updated values are present in the
Compute Quota Defaults table
"""
default_quota_values = self.defaults_page.compute_quota_values
self.defaults_page.update_compute_defaults(self.add_up)
self.assertEqual(
self.defaults_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertGreater(len(default_quota_values), 0)
for quota_name in default_quota_values:
self.assertTrue(
self.defaults_page.is_compute_quota_a_match(
quota_name,
default_quota_values[quota_name] + self.add_up
))
def test_update_volume_defaults(self):
"""Tests the Update Default Volume Quotas functionality:
1) Login as Admin and go to Admin > System > Defaults
2) Clicks on Volume Quotas tab
3) Updates default volume Quotas by adding a random
number between 1 and 10
4) Verifies that the updated values are present in the
Volume Quota Defaults table
"""
self.defaults_page.go_to_volume_quotas_tab()
default_quota_values = self.defaults_page.volume_quota_values
self.defaults_page.update_volume_defaults(self.add_up)
self.assertEqual(
self.defaults_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertGreater(len(default_quota_values), 0)
for quota_name in default_quota_values:
self.assertTrue(
self.defaults_page.is_volume_quota_a_match(
quota_name,
default_quota_values[quota_name] + self.add_up
))

View File

@@ -0,0 +1,75 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestFlavorAngular(helpers.AdminTestCase):
@property
def flavors_page(self):
from openstack_dashboard.test.integration_tests.pages.admin.\
compute.flavorspage import FlavorsPageNG
self.home_pg.go_to_admin_compute_flavorspage()
return FlavorsPageNG(self.driver, self.CONFIG)
def test_basic_flavors_browse(self):
flavors_page = self.flavors_page
self.assertEqual(flavors_page.header.text, 'Flavors')
class TestFlavors(helpers.AdminTestCase):
FLAVOR_NAME = helpers.gen_random_resource_name("flavor")
def setUp(self):
super().setUp()
self.flavors_page = self.home_pg.go_to_admin_compute_flavorspage()
def _create_flavor(self, flavor_name):
self.flavors_page.create_flavor(
name=flavor_name,
vcpus=1,
ram=1024,
root_disk=20,
ephemeral_disk=0,
swap_disk=0
)
self.assertEqual(
self.flavors_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(
self.flavors_page.is_flavor_present(self.FLAVOR_NAME))
def _delete_flavor(self, flavor_name):
self.flavors_page.delete_flavor_by_row(flavor_name)
self.assertEqual(
self.flavors_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(
self.flavors_page.is_flavor_present(self.FLAVOR_NAME))
def test_flavor_module_exists(self):
js_cmd = "$('html').append('<div id=\"testonly\">'"\
" + angular.module('horizon.app.core.flavors').name"\
" + '</div>');"
self.driver.execute_script(js_cmd)
value = self.driver.find_element_by_id('testonly').text
self.assertEqual(value, 'horizon.app.core.flavors')
def test_flavor_create(self):
"""tests the flavor creation and deletion functionalities:
* creates a new flavor
* verifies the flavor appears in the flavors table
* deletes the newly created flavor
* verifies the flavor does not appear in the table after deletion
"""
self._create_flavor(self.FLAVOR_NAME)
self._delete_flavor(self.FLAVOR_NAME)

View File

@@ -0,0 +1,79 @@
# Copyright 2015 Hewlett-Packard Development Company, L.P
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestFloatingip(helpers.TestCase):
"""Checks that the user is able to allocate/release floatingip."""
def test_floatingip(self):
floatingip_page = \
self.home_pg.go_to_project_network_floatingipspage()
floating_ip = floatingip_page.allocate_floatingip()
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(floatingip_page.is_floatingip_present(floating_ip))
floatingip_page.release_floatingip(floating_ip)
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(floatingip_page.is_floatingip_present(floating_ip))
class TestFloatingipAssociateDisassociate(helpers.TestCase):
"""Checks that the user is able to Associate/Disassociate floatingip."""
def test_floatingip_associate_disassociate(self):
instance_name = helpers.gen_random_resource_name('instance',
timestamp=False)
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.create_instance(instance_name, network_type='internal')
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_active(instance_name))
instance_ipv4 = instances_page.get_fixed_ipv4(instance_name)
instance_info = "{} {}".format(instance_name, instance_ipv4)
floatingip_page = \
self.home_pg.go_to_project_network_floatingipspage()
floating_ip = floatingip_page.allocate_floatingip()
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(floatingip_page.is_floatingip_present(floating_ip))
self.assertEqual('-', floatingip_page.get_fixed_ip(floating_ip))
floatingip_page.associate_floatingip(floating_ip, instance_name,
instance_ipv4)
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertEqual(instance_info,
floatingip_page.get_fixed_ip(floating_ip))
floatingip_page.disassociate_floatingip(floating_ip)
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertEqual('-', floatingip_page.get_fixed_ip(floating_ip))
floatingip_page.release_floatingip(floating_ip)
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(floatingip_page.is_floatingip_present(floating_ip))
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.delete_instance(instance_name)
self.assertEqual(
floatingip_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_deleted(instance_name))

View File

@@ -0,0 +1,60 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestGroup(helpers.AdminTestCase):
"""Checks if the user is able to create/delete/edit groups"""
def setUp(self):
super().setUp()
self.groups_page = self.home_pg.go_to_identity_groupspage()
@property
def group_name(self):
return helpers.gen_random_resource_name("group")
@property
def group_description(self):
return helpers.gen_random_resource_name('description')
def _test_create_group(self, group_name, group_desc=None):
self.groups_page.create_group(name=group_name, description=group_desc)
self.assertEqual(
self.groups_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(self.groups_page.is_group_present(group_name))
def _test_delete_group(self, group_name):
self.groups_page.delete_group(name=group_name)
self.assertEqual(
self.groups_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(self.groups_page.is_group_present(group_name))
def test_create_delete_group(self):
"""Tests ability to create and delete a group"""
group_name = self.group_name
self._test_create_group(group_name)
self._test_delete_group(group_name)
def test_edit_group(self):
"""Tests ability to edit group name and description"""
group_name = self.group_name
self._test_create_group(group_name)
new_group_name = self.group_name
new_group_desc = self.group_description
self.groups_page.edit_group(group_name, new_group_name, new_group_desc)
self.assertEqual(
self.groups_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(self.groups_page.is_group_present(new_group_name))
self._test_delete_group(new_group_name)

View File

@@ -0,0 +1,43 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestAdminGroupTypes(helpers.AdminTestCase):
GROUP_TYPE_NAME = helpers.gen_random_resource_name("group_type")
def test_group_type_create_delete(self):
"""This test case checks create, delete group type:
Steps:
1. Login to Horizon Dashboard as admin user
2. Navigate to Admin -> Volume -> Group Types page
3. Create new group type
4. Check that the group type is in the list
5. Check that no Error messages present
6. Delete the group type
7. Check that the group type is absent in the list
8. Check that no Error messages present
"""
group_types_page = self.home_pg.go_to_admin_volume_grouptypespage()
group_types_page.create_group_type(self.GROUP_TYPE_NAME)
self.assertEqual(
group_types_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(group_types_page.is_group_type_present(
self.GROUP_TYPE_NAME))
group_types_page.delete_group_type(self.GROUP_TYPE_NAME)
self.assertEqual(
group_types_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(group_types_page.is_group_type_deleted(
self.GROUP_TYPE_NAME))

View File

@@ -0,0 +1,46 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestHostAggregates(helpers.AdminTestCase):
HOST_AGGREGATE_NAME = helpers.gen_random_resource_name("host_aggregate")
HOST_AGGREGATE_AVAILABILITY_ZONE = "nova"
def test_host_aggregate_create(self):
"""tests the host aggregate creation and deletion functionalities:
* creates a new host aggregate
* verifies the host aggregate appears in the host aggregates table
* deletes the newly created host aggregate
* verifies the host aggregate does not appear in the table
* after deletion
"""
hostaggregates_page = \
self.home_pg.go_to_admin_compute_hostaggregatespage()
hostaggregates_page.create_host_aggregate(
name=self.HOST_AGGREGATE_NAME,
availability_zone=self.HOST_AGGREGATE_AVAILABILITY_ZONE)
self.assertEqual(
hostaggregates_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(hostaggregates_page.is_host_aggregate_present(
self.HOST_AGGREGATE_NAME))
hostaggregates_page.delete_host_aggregate(self.HOST_AGGREGATE_NAME)
self.assertEqual(
hostaggregates_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(hostaggregates_page.is_host_aggregate_present(
self.HOST_AGGREGATE_NAME))

View File

@@ -0,0 +1,384 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pytest
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
class TestImagesBasicAngular(helpers.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.IMAGE_NAME = helpers.gen_random_resource_name("image")
@property
def images_page(self):
return self.home_pg.go_to_project_compute_imagespage()
def test_basic_image_browse(self):
images_page = self.images_page
self.assertEqual(images_page.header.text, 'Images')
def image_create(self, local_file=None, **kwargs):
images_page = self.images_page
if local_file:
images_page.create_image(self.IMAGE_NAME,
image_file=local_file,
**kwargs)
else:
images_page.create_image(self.IMAGE_NAME,
image_source_type='url',
**kwargs)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(images_page.is_image_present(self.IMAGE_NAME))
self.assertTrue(images_page.is_image_active(self.IMAGE_NAME))
return images_page
def image_delete(self, image_name):
images_page = self.images_page
images_page.delete_image(image_name)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
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
* verifies the image appears in the images table as active
* deletes the newly created image
* verifies the image does not appear in the table after deletion
"""
self.image_create()
self.image_delete(self.IMAGE_NAME)
def test_images_pagination(self):
"""This test checks images pagination
Steps:
1) Login to Horizon Dashboard as horizon user
2) Navigate to user settings page
3) Change 'Items Per Page' value to 1
4) Go to Project -> Compute -> Images page
5) Check that only 'Next' link is available, only one image is
available (and it has correct name)
6) Click 'Next' and check that both 'Prev' and 'Next' links are
available, only one image is available (and it has correct name)
7) Click 'Next' and check that only 'Prev' link is available,
only one image is visible (and it has correct name)
8) Click 'Prev' and check results (should be the same as for step6)
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.assertEqual(
images_page.find_messages_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.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
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': [images_names[0]]}
third_page_definition = {'Next': False, 'Prev': True,
'Count': items_per_page,
'Names': [images_names[1]]}
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(items_per_page)
settings_page.find_messages_and_dismiss()
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()
images_page.images_table.assert_definition(second_page_definition)
images_page.images_table.turn_next_page()
images_page.images_table.assert_definition(third_page_definition)
images_page.images_table.turn_prev_page()
images_page.images_table.assert_definition(second_page_definition)
images_page.images_table.turn_prev_page()
images_page.images_table.assert_definition(first_page_definition)
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize()
settings_page.find_messages_and_dismiss()
images_page = self.images_page
images_page.wait_until_image_present(default_image_list[0])
images_page.delete_images(images_names)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
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
* 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,
description='test description')
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.IMAGE_NAME)
self.assertSequenceTrue(results)
def test_remove_protected_image(self):
"""tests that protected image is not deletable
* logs in as admin user
* creates image from locally downloaded file
* verifies the image appears in the images table as active
* marks 'Protected' checkbox
* verifies that edit action was successful
* verifies that delete action is not available in the list
* tries to delete the image
* verifies that exception is generated for the protected image
* unmarks 'Protected' checkbox
* deletes the image
* verifies the image does not appear in the table after deletion
"""
with helpers.gen_temporary_file() as file_name:
images_page = self.image_create(local_file=file_name)
images_page.edit_image(self.IMAGE_NAME, protected=True)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
# 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.
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)
# 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.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.image_delete(self.IMAGE_NAME)
def test_edit_image_description_and_name(self):
"""tests that image description is editable
* creates image from locally downloaded file
* verifies the image appears in the images table as active
* toggle edit action and adds some description
* verifies that edit action was successful
* verifies that new description is seen on image details page
* toggle edit action and changes image name
* verifies that edit action was successful
* verifies that image with new name is seen on the page
* deletes the image
* verifies the image does not appear in the table after deletion
"""
new_description_text = helpers.gen_random_resource_name("description")
new_image_name = helpers.gen_random_resource_name("image")
with helpers.gen_temporary_file() as file_name:
images_page = self.image_create(local_file=file_name)
images_page.edit_image(self.IMAGE_NAME,
description=new_description_text)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
results = images_page.check_image_details(self.IMAGE_NAME,
{'Description':
new_description_text})
self.assertSequenceTrue(results)
# Just go back to the images page and toggle edit again
images_page = self.images_page
images_page.edit_image(self.IMAGE_NAME,
new_name=new_image_name)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
results = images_page.check_image_details(new_image_name,
{'Name':
new_image_name})
self.assertSequenceTrue(results)
self.image_delete(new_image_name)
def test_filter_images(self):
"""This test checks filtering of images
Steps:
1) Login to Horizon dashboard as admin user
2) Go to Admin -> Compute -> Images
3) Use filter by Image Name
4) Check that filtered table has one image only (which name is
equal to filter value)
5) Check that no other images in the table
6) Clear filter and set nonexistent image name. Check that 0 rows
are displayed
"""
default_image_list = self.CONFIG.image.images_list
images_page = self.images_page
images_page.filter(default_image_list[0])
self.assertTrue(images_page.is_image_present(default_image_list[0]))
for image in default_image_list[1:]:
self.assertFalse(images_page.is_image_present(image))
images_page = self.images_page
nonexistent_image_name = "{0}_test".format(self.IMAGE_NAME)
images_page.filter(nonexistent_image_name)
self.assertEqual(images_page.images_table.rows, [])
images_page.filter('')
class TestImagesAdvancedAngular(helpers.TestCase):
@property
def images_page(self):
return self.home_pg.go_to_project_compute_imagespage()
def volumes_page(self):
self.home_pg.go_to_project_volumes_volumespage()
return VolumesPage(self.driver, self.CONFIG)
def instances_page(self):
self.home_pg.go_to_project_compute_instancespage()
return InstancesPage(self.driver, self.CONFIG)
"""Login as demo user"""
def test_create_volume_from_image(self):
"""This test case checks create volume from image functionality:
Steps:
1. Login to Horizon Dashboard as regular user
2. Navigate to Project -> Compute -> Images
3. Create new volume from image
4. Check that volume is created with expected name
5. Check that volume status is Available
"""
images_page = self.images_page
source_image = self.CONFIG.image.images_list[0]
target_volume = "created_from_{0}".format(source_image)
images_page.create_volume_from_image(
source_image, volume_name=target_volume)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.INFO})
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_messages_and_dismiss()
volumes_page = self.volumes_page()
self.assertTrue(volumes_page.is_volume_deleted(target_volume))
def test_launch_instance_from_image(self):
"""This test case checks launch instance from image functionality:
Steps:
1. Login to Horizon Dashboard as regular user
2. Navigate to Project -> Compute -> Images
3. Launch new instance from image
4. Check that instance is create
5. Check that status of newly created instance is Active
6. Check that image_name in correct in instances table
"""
images_page = self.images_page
source_image = self.CONFIG.image.images_list[0]
target_instance = "created_from_{0}".format(source_image)
images_page.launch_instance_from_image(source_image, target_instance)
self.assertEqual(
images_page.find_messages_and_dismiss(), {messages.INFO})
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.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_deleted(target_instance))

View File

@@ -0,0 +1,401 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestInstances(helpers.TestCase):
INSTANCE_NAME = helpers.gen_random_resource_name('instance',
timestamp=False)
@property
def instances_page(self):
return self.home_pg.go_to_project_compute_instancespage()
def test_create_delete_instance(self):
"""tests the instance creation and deletion functionality:
* creates a new instance in Project > Compute > Instances page
* verifies the instance appears in the instances table as active
* deletes the newly created instance via proper page (depends on user)
* verifies the instance does not appear in the table after deletion
"""
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.create_instance(self.INSTANCE_NAME)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_active(self.INSTANCE_NAME))
instances_page = self.instances_page
instances_page.delete_instance(self.INSTANCE_NAME)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_deleted(self.INSTANCE_NAME))
class TestInstancesPagination(helpers.TestCase):
INSTANCE_NAME = helpers.gen_random_resource_name('instance',
timestamp=False)
ITEMS_PER_PAGE = 1
INSTANCE_COUNT = 2
@property
def instances_page(self):
return self.home_pg.go_to_project_compute_instancespage()
def setUp(self):
super().setUp()
self.instance_list = ["{0}-{1}".format(self.INSTANCE_NAME, item)
for item in range(1, self.INSTANCE_COUNT + 1)]
instances_page = self.instances_page
# delete any old instances
garbage = instances_page.instances_table.get_column_data(
name_column='Instance Name')
if garbage:
instances_page.delete_instances(garbage)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(garbage))
instances_page.create_instance(self.INSTANCE_NAME,
instance_count=self.INSTANCE_COUNT)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.is_instance_active(self.instance_list[1]))
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(self.ITEMS_PER_PAGE)
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
def cleanup():
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize()
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
instances_page = self.instances_page
instances_page.delete_instances(self.instance_list)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(self.instance_list))
self.addCleanup(cleanup)
def test_instances_pagination(self):
"""This test checks instance pagination
Steps:
1) Login to Horizon Dashboard as regular user
2) Navigate to user settings page
3) Change 'Items Per Page' value to 1
4) Go to Project > Compute > Instances page
5) Create 2 instances
6) Go to appropriate page (depends on user)
7) Check that only 'Next' link is available, only one instance is
available (and it has correct name) on the first page
8) Click 'Next' and check that on the second page only one instance is
available (and it has correct name), there is no 'Next' link on page
9) Go to user settings page and restore 'Items Per Page'
10) Delete created instances via proper page (depends on user)
"""
first_page_definition = {'Next': True, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
second_page_definition = {'Next': False, 'Prev': True,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[0]]}
instances_page = self.instances_page
instances_page = self.instances_page
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True, name_column="Instance Name")
instances_page.instances_table.turn_next_page()
instances_page.instances_table.assert_definition(
second_page_definition, sorting=True, name_column="Instance Name")
instances_page = self.instances_page
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True, name_column="Instance Name")
def test_instances_pagination_and_filtration(self):
"""This test checks instance pagination and filtration
Steps:
1) Login to Horizon Dashboard as regular user
2) Go to to user settings page
3) Change 'Items Per Page' value to 1
4) Go to Project > Compute > Instances page
5) Create 2 instances
6) Go to appropriate page (depends on user)
7) Check filter by Name of the first and the second instance in order
to have one instance in the list (and it should have correct name)
and no 'Next' link is available
8) Check filter by common part of Name of in order to have one instance
in the list (and it should have correct name) and 'Next' link is
available on the first page and is not available on the second page
9) Go to user settings page and restore 'Items Per Page'
10) Delete created instances via proper page (depends on user)
"""
first_page_definition = {'Next': True, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
second_page_definition = {'Next': False, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[0]]}
filter_first_page_definition = {'Next': False, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
instances_page = self.instances_page
instances_page.instances_table.set_filter_value('name')
instances_page.instances_table.filter(self.instance_list[1])
instances_page.instances_table.assert_definition(
filter_first_page_definition, sorting=True,
name_column="Instance Name")
instances_page.instances_table.filter(self.instance_list[0])
instances_page.instances_table.assert_definition(
second_page_definition, sorting=True,
name_column="Instance Name")
instances_page.instances_table.filter(self.INSTANCE_NAME)
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True,
name_column="Instance Name")
instances_page.instances_table.filter('')
class TestInstancesFilter(helpers.TestCase):
INSTANCE_NAME = helpers.gen_random_resource_name('instance',
timestamp=False)
INSTANCE_COUNT = 2
@property
def instances_page(self):
return self.home_pg.go_to_project_compute_instancespage()
def setUp(self):
super().setUp()
self.instance_list = ["{0}-{1}".format(self.INSTANCE_NAME, item)
for item in range(1, self.INSTANCE_COUNT + 1)]
instances_page = self.instances_page
# delete any old instances
garbage = instances_page.instances_table.get_column_data(
name_column='Instance Name')
if garbage:
instances_page.delete_instances(garbage)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(garbage))
instances_page.create_instance(self.INSTANCE_NAME,
instance_count=self.INSTANCE_COUNT)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.is_instance_active(self.instance_list[1]))
def cleanup():
instances_page = self.instances_page
instances_page.delete_instances(self.instance_list)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(self.instance_list))
self.addCleanup(cleanup)
def test_filter_instances(self):
"""This test checks filtering of instances by Instance Name
Steps:
1) Login to Horizon dashboard as regular user
2) Go to Project > Compute > Instances
3) Create 2 instances
4) Go to appropriate page (depends on user)
5) Use filter by Instance Name
6) Check that filtered table has one instance only (which name is equal
to filter value) and no other instances in the table
7) Check that filtered table has both instances (search by common part
of instance names)
8) Set nonexistent instance name. Check that 0 rows are displayed
9) Clear filter and delete instances via proper page (depends on user)
"""
instances_page = self.instances_page
instances_page.instances_table.set_filter_value('name')
instances_page.instances_table.filter(self.instance_list[0])
self.assertTrue(
instances_page.is_instance_present(self.instance_list[0]))
for instance in self.instance_list[1:]:
self.assertFalse(instances_page.is_instance_present(instance))
instances_page.instances_table.filter(self.INSTANCE_NAME)
for instance in self.instance_list:
self.assertTrue(instances_page.is_instance_present(instance))
nonexistent_instance_name = "{0}_test".format(self.INSTANCE_NAME)
instances_page.instances_table.filter(nonexistent_instance_name)
self.assertEqual(instances_page.instances_table.rows, [])
instances_page.instances_table.filter('')
class TestAdminInstancesPagination(helpers.AdminTestCase, TestInstances):
INSTANCE_NAME = helpers.gen_random_resource_name('instance',
timestamp=False)
ITEMS_PER_PAGE = 1
INSTANCE_COUNT = 2
@property
def instances_page(self):
self.home_pg.go_to_admin_overviewpage()
return self.home_pg.go_to_admin_compute_instancespage()
def setUp(self):
super().setUp()
self.instance_list = ["{0}-{1}".format(self.INSTANCE_NAME, item)
for item in range(1, self.INSTANCE_COUNT + 1)]
# we have to use the project page, since admin has no create button
instances_page = self.home_pg.go_to_project_compute_instancespage()
# delete any old instances
garbage = instances_page.instances_table.get_column_data(
name_column='Instance Name')
if garbage:
instances_page.delete_instances(garbage)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(garbage))
instances_page.create_instance(self.INSTANCE_NAME,
instance_count=self.INSTANCE_COUNT)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.is_instance_active(self.instance_list[1]))
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(self.ITEMS_PER_PAGE)
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
def cleanup():
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize()
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
instances_page = self.instances_page
instances_page.delete_instances(self.instance_list)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
instances_page.are_instances_deleted(self.instance_list))
self.addCleanup(cleanup)
def test_instances_pagination(self):
"""This test checks instance pagination
Steps:
1) Login to Horizon Dashboard as admin
2) Navigate to user settings page
3) Change 'Items Per Page' value to 1
4) Go to Project > Compute > Instances page
5) Create 2 instances
6) Go to Admin > Compute > Instances page
7) Check that only 'Next' link is available, only one instance is
available (and it has correct name) on the first page
8) Click 'Next' and check that on the second page only one instance is
available (and it has correct name), there is no 'Next' link on page
9) Go to user settings page and restore 'Items Per Page'
10) Delete created instances via proper page
"""
first_page_definition = {'Next': True, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
second_page_definition = {'Next': False, 'Prev': True,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[0]]}
instances_page = self.instances_page
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True)
instances_page.instances_table.turn_next_page()
instances_page.instances_table.assert_definition(
second_page_definition, sorting=True)
instances_page = self.instances_page
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True)
def test_instances_pagination_and_filtration(self):
"""This test checks instance pagination and filtration
Steps:
1) Login to Horizon Dashboard as admin
2) Go to to user settings page
3) Change 'Items Per Page' value to 1
4) Go to Project > Compute > Instances page
5) Create 2 instances
6) Go to Admin > Compute > Instances page
7) Check filter by Name of the first and the second instance in order
to have one instance in the list (and it should have correct name)
and no 'Next' link is available
8) Check filter by common part of Name of in order to have one instance
in the list (and it should have correct name) and 'Next' link is
available on the first page and is not available on the second page
9) Go to user settings page and restore 'Items Per Page'
10) Delete created instances via proper page
"""
first_page_definition = {'Next': True, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
second_page_definition = {'Next': False, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[0]]}
filter_first_page_definition = {'Next': False, 'Prev': False,
'Count': self.ITEMS_PER_PAGE,
'Names': [self.instance_list[1]]}
instances_page = self.instances_page
instances_page.instances_table.set_filter_value('name')
instances_page.instances_table.filter(self.instance_list[1])
instances_page.instances_table.assert_definition(
filter_first_page_definition, sorting=True)
instances_page.instances_table.filter(self.instance_list[0])
instances_page.instances_table.assert_definition(
second_page_definition, sorting=True)
instances_page.instances_table.filter(self.INSTANCE_NAME)
instances_page.instances_table.assert_definition(
first_page_definition, sorting=True)
instances_page.instances_table.filter('')

View File

@@ -0,0 +1,40 @@
# Copyright 2014 Hewlett-Packard Development Company, L.P
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pytest
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestKeypair(helpers.TestCase):
"""Checks that the user is able to create/delete keypair."""
KEYPAIR_NAME = helpers.gen_random_resource_name("keypair")
@pytest.mark.skip(reason="Legacy Panel not tested")
def test_keypair(self):
keypair_page = self.home_pg.\
go_to_project_compute_keypairspage()
keypair_page.create_keypair(self.KEYPAIR_NAME)
self.assertEqual(
keypair_page.find_messages_and_dismiss(), {messages.SUCCESS})
keypair_page = self.home_pg.\
go_to_project_compute_keypairspage()
self.assertTrue(keypair_page.is_keypair_present(self.KEYPAIR_NAME))
keypair_page.delete_keypair(self.KEYPAIR_NAME)
self.assertEqual(
keypair_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(keypair_page.is_keypair_present(self.KEYPAIR_NAME))

View File

@@ -0,0 +1,31 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.pages import loginpage
class TestLogin(helpers.BaseTestCase):
"""This is a basic scenario test:
* checks that the login page is available
* logs in as a regular user
* checks that the user home page loads without error
"""
def test_login(self):
login_pg = loginpage.LoginPage(self.driver, self.CONFIG)
login_pg.go_to_login_page()
home_pg = login_pg.login()
if not home_pg.is_logged_in:
self.fail("Could not determine if logged in")
home_pg.log_out()

View File

@@ -0,0 +1,162 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestMetadataDefinitions(helpers.AdminTestCase):
NAMESPACE_TEMPLATE_PATH = os.path.join(
os.path.dirname(__file__),
'test-data/empty_namespace.json')
PUBLIC = 'public'
PROTECTED = 'protected'
def namespace_create_with_checks(
self, namespace_name, page, template_json_container,
expected_namespace_res=None, template_source_type='raw',
is_public=True, is_protected=False, template_path=None,
checks=(PUBLIC, PROTECTED)):
"""Create NameSpace and run checks
:param namespace_name: Display name of namespace in template
:param page: Connection point
:param template_json_container: JSON container with NameSpace content
:param expected_namespace_res: Resources from template
:param template_source_type: 'raw' or 'file'
:param is_public: True or False
:param is_protected: If True- you can't delete it from GUI
:param checks: Put name of columns if you need to check actual
representation of 'public' and/or 'protected' value.
To disable leave it empty: '' OR put None
:param template_path: Full path to NameSpace template file
:return: Nothing
"""
if template_source_type == 'file':
template_json_container = template_path
page.import_namespace(
namespace_json_container=template_json_container,
is_public=is_public,
is_protected=is_protected,
namespace_source_type=template_source_type)
# Checks
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(page.is_namespace_present(namespace_name))
row = page._get_row_with_namespace_name(namespace_name)
if checks:
if self.PUBLIC in checks:
self.assertTrue(
page.is_public_set_correct(namespace_name, is_public, row))
elif self.PROTECTED in checks:
self.assertTrue(
page.is_protected_set_correct(namespace_name, is_protected,
row))
if expected_namespace_res:
self.assertTrue(page.is_resource_type_set_correct(
namespace_name, expected_namespace_res, row))
def namespace_delete_with_checks(self, namespace_name, page):
"""Delete NameSpace and run checks
:param namespace_name: Display name of namespace in template
:param page: Connection point
:return: Nothing
"""
page.delete_namespace(name=namespace_name)
# Checks
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(page.is_namespace_present(namespace_name))
def test_namespace_create_delete(self):
"""Tests the NameSpace creation and deletion functionality:
* Actions:
* 1) Login to Horizon Dashboard as admin user.
* 2) Navigate to Admin -> System -> Metadata Definitions.
* 3) Click "Import Namespace" button. Wait for Create Network dialog.
* 4) Enter settings for new Namespace:
* - Namespace Definition Source: 'raw' or 'file'
* - Namespace JSON location;
* - Public: Yes or No;
* - Protected: No (check box not enabled).
* 5) Press "Import Namespace" button.
* 6) Check that new Namespace was successfully created.
* 7) Check that new Namespace is present in the table.
* 8) Check that values in table on page "Metadata Definitions" are
* the same as in Namespace JSON template and as in step (4):
* - Name
* - Public
* - Protected
* - Resource Types
* 9) Select Namespace in table and press "Delete Namespace" button.
* 10) In "Confirm Delete Namespace" window press "Delete Namespace".
* 11) Check that new Namespace was successfully deleted.
* 12) Check that new Namespace is not present in the table.
"""
namespaces_page = \
self.home_pg.go_to_admin_system_metadatadefinitionspage()
template_json_container = namespaces_page.json_load_template(
namespace_template_name=self.NAMESPACE_TEMPLATE_PATH)
# Get name from template file
namespace_name = template_json_container['display_name']
# Get resources from template to check representation in GUI
namespace_res_type = \
template_json_container['resource_type_associations']
namespace_res_type = \
[x['name'] for x in namespace_res_type]
# Create / Delete NameSpaces with checks
kwargs = {'namespace_name': namespace_name,
'page': namespaces_page,
'is_protected': False,
'template_json_container': template_json_container,
'template_path': self.NAMESPACE_TEMPLATE_PATH}
self.namespace_create_with_checks(
template_source_type='raw',
is_public=True,
expected_namespace_res=namespace_res_type,
checks=(self.PUBLIC, self.PROTECTED),
**kwargs)
self.namespace_delete_with_checks(namespace_name, namespaces_page)
self.namespace_create_with_checks(
template_source_type='raw',
is_public=False,
expected_namespace_res=None,
checks=(self.PUBLIC,),
**kwargs)
self.namespace_delete_with_checks(namespace_name, namespaces_page)
self.namespace_create_with_checks(
template_source_type='file',
is_public=True,
expected_namespace_res=namespace_res_type,
checks=(self.PUBLIC, self.PROTECTED),
**kwargs)
self.namespace_delete_with_checks(namespace_name, namespaces_page)
self.namespace_create_with_checks(
template_source_type='file',
is_public=False,
expected_namespace_res=None,
checks=(self.PUBLIC,),
**kwargs)
self.namespace_delete_with_checks(namespace_name, namespaces_page)

View File

@@ -0,0 +1,162 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import decorators
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
@decorators.services_required("neutron")
class TestNetworks(helpers.TestCase):
NETWORK_NAME = helpers.gen_random_resource_name("network")
SUBNET_NAME = helpers.gen_random_resource_name("subnet")
@property
def networks_page(self):
return self.home_pg.go_to_project_network_networkspage()
def test_private_network_create(self):
"""tests the network creation and deletion functionalities:
* creates a new private network and a new subnet associated with it
* verifies the network appears in the networks table as active
* deletes the newly created network
* verifies the network does not appear in the table after deletion
"""
networks_page = self.networks_page
networks_page.create_network(self.NETWORK_NAME, self.SUBNET_NAME)
self.assertEqual(
networks_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(networks_page.is_network_present(self.NETWORK_NAME))
self.assertTrue(networks_page.is_network_active(self.NETWORK_NAME))
networks_page.delete_network(self.NETWORK_NAME)
self.assertEqual(
networks_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(networks_page.is_network_present(self.NETWORK_NAME))
@decorators.services_required("neutron")
class TestAdminNetworks(helpers.AdminTestCase, TestNetworks):
NETWORK_NAME = helpers.gen_random_resource_name("network")
SUBNET_NAME = helpers.gen_random_resource_name("subnet")
@property
def networks_page(self):
return self.home_pg.go_to_admin_network_networkspage()
@decorators.services_required("neutron")
class TestNetworksPagination(helpers.TestCase):
NETWORK_NAME = helpers.gen_random_resource_name("network")
SUBNET_NAME = helpers.gen_random_resource_name("subnet")
ITEMS_PER_PAGE = 2
@property
def networks_page(self):
return self.home_pg.go_to_project_network_networkspage()
def setUp(self):
super().setUp()
count = 3
networks_names = ["{0}_{1}".format(self.NETWORK_NAME, i)
for i in range(count)]
networks_page = self.networks_page
for network_name in networks_names:
networks_page.create_network(network_name, self.SUBNET_NAME)
self.assertEqual(
networks_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(networks_page.is_network_present(network_name))
self.assertTrue(networks_page.is_network_active(network_name))
# we have to get this now, before we change page size
self.names = networks_page.networks_table.get_column_data(
name_column=networks_page.NETWORKS_TABLE_NAME_COLUMN)
self._change_page_size_setting(self.ITEMS_PER_PAGE)
def cleanup():
self._change_page_size_setting()
networks_page = self.networks_page
for network_name in networks_names:
networks_page.delete_network(network_name)
self.assertEqual(
networks_page.find_messages_and_dismiss(),
{messages.SUCCESS})
self.assertFalse(networks_page.is_network_present(network_name))
self.addCleanup(cleanup)
def test_networks_pagination(self):
"""This test checks networks pagination
Steps:
1) Login to Horizon Dashboard
2) Go to Project -> Network -> Networks tab and create
three networks
3) Navigate to user settings page
4) Change 'Items Per Page' value to 2
5) Go to Project -> Network -> Networks tab or
Admin -> Network -> Networks tab (depends on user)
6) Check that only 'Next' link is available, only one network is
available (and it has correct name)
7) Click 'Next' and check that both 'Prev' and 'Next' links are
available, only one network is available (and it has correct name)
8) Click 'Next' and check that only 'Prev' link is available,
only one network is visible (and it has correct name)
9) Click 'Prev' and check result (should be the same as for step7)
10) Click 'Prev' and check result (should be the same as for step6)
11) Go to user settings page and restore 'Items Per Page'
12) Delete created networks
"""
networks_page = self.networks_page
definitions = []
i = 0
total = len(self.names)
while i < total:
has_prev = i >= self.ITEMS_PER_PAGE
has_next = total - i > self.ITEMS_PER_PAGE
count = (self.ITEMS_PER_PAGE if has_next
else total % self.ITEMS_PER_PAGE or self.ITEMS_PER_PAGE)
definition = {
'Next': has_next,
'Prev': has_prev,
'Count': count,
'Names': self.names[i:i + count],
}
definitions.append(definition)
networks_page.networks_table.assert_definition(
definition,
name_column=networks_page.NETWORKS_TABLE_NAME_COLUMN)
if has_next:
networks_page.networks_table.turn_next_page()
i = i + self.ITEMS_PER_PAGE
definitions.reverse()
for definition in definitions:
networks_page.networks_table.assert_definition(
definition,
name_column=networks_page.NETWORKS_TABLE_NAME_COLUMN)
if definition['Prev']:
networks_page.networks_table.turn_prev_page()
def _change_page_size_setting(self, items_per_page=None):
settings_page = self.home_pg.go_to_settings_usersettingspage()
if items_per_page:
settings_page.change_pagesize(items_per_page)
else:
settings_page.change_pagesize()
settings_page.find_messages_and_dismiss()

View File

@@ -0,0 +1,67 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
PROJECT_NAME = helpers.gen_random_resource_name("project")
class TestCreateDeleteProject(helpers.AdminTestCase):
def setUp(self):
super().setUp()
self.projects_page = self.home_pg.go_to_identity_projectspage()
def test_create_delete_project(self):
self.projects_page.create_project(PROJECT_NAME)
self.assertEqual(
self.projects_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(self.projects_page.is_project_present(PROJECT_NAME))
self.projects_page.delete_project(PROJECT_NAME)
self.assertEqual(
self.projects_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(self.projects_page.is_project_present(PROJECT_NAME))
class TestModifyProject(helpers.AdminTestCase):
def setUp(self):
super().setUp()
self.projects_page = self.home_pg.go_to_identity_projectspage()
self.projects_page.create_project(PROJECT_NAME)
self.assertEqual(
self.projects_page.find_messages_and_dismiss(), {messages.SUCCESS})
def cleanup():
if not self.projects_page.is_the_current_page():
self.home_pg.go_to_identity_projectspage()
self.projects_page.delete_project(PROJECT_NAME)
self.addCleanup(cleanup)
def test_add_member(self):
admin_name = self.CONFIG.identity.admin_username
regular_role_name = self.CONFIG.identity.default_keystone_role
admin_role_name = self.CONFIG.identity.default_keystone_admin_role
roles2add = {regular_role_name, admin_role_name}
self.projects_page.allocate_user_to_project(
admin_name, roles2add, PROJECT_NAME)
self.assertEqual(
self.projects_page.find_messages_and_dismiss(), {messages.SUCCESS})
user_roles = self.projects_page.get_user_roles_at_project(
admin_name, PROJECT_NAME)
self.assertEqual(roles2add, user_roles,
"The requested roles haven't been set for the user!")

View File

@@ -0,0 +1,210 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import decorators
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
@decorators.services_required("neutron")
class TestRouters(helpers.TestCase):
ROUTER_NAME = helpers.gen_random_resource_name("router")
NETWORK_NAME = helpers.gen_random_resource_name("network")
SUBNET_NAME = helpers.gen_random_resource_name("subnet")
@property
def routers_page(self):
return self.home_pg.go_to_project_network_routerspage()
def _create_router(self):
routers_page = self.routers_page
routers_page.create_router(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(routers_page.is_router_present(self.ROUTER_NAME))
self.assertTrue(routers_page.is_router_active(self.ROUTER_NAME))
def _delete_router(self):
routers_page = self.routers_page
routers_page.delete_router(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(routers_page.is_router_present(self.ROUTER_NAME))
def test_router_create(self):
"""tests the router creation and deletion functionalities:
* creates a new router for public network
* verifies the router appears in the routers table as active
* deletes the newly created router
* verifies the router does not appear in the table after deletion
"""
self._create_router()
self._delete_router()
def _create_interface(self, interfaces_page):
interfaces_page.create_interface(self.SUBNET_NAME)
self.assertEqual(
interfaces_page.find_messages_and_dismiss(), {messages.SUCCESS})
interface_name = interfaces_page.interface_name
self.assertTrue(interfaces_page.is_interface_present(interface_name))
def _delete_interface(self, interfaces_page, interface_name):
interfaces_page.delete_interface(interface_name)
self.assertEqual(
interfaces_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(interfaces_page.is_interface_present(interface_name))
def _create_subnet(self):
networks_page = self.home_pg.go_to_project_network_networkspage()
networks_page.create_network(self.NETWORK_NAME, self.SUBNET_NAME)
self.assertEqual(
networks_page.find_messages_and_dismiss(), {messages.SUCCESS})
def _delete_subnet(self):
networks_page = self.home_pg.go_to_project_network_networkspage()
networks_page.delete_network(self.NETWORK_NAME)
self.assertEqual(
networks_page.find_messages_and_dismiss(), {messages.SUCCESS})
def test_router_add_delete_interface(self):
"""Tests the router interface creation and deletion functionalities:
* Follows the steps to create a new router
* Clicks on the new router name from the routers table
* Moves to the Interfaces page/tab
* Adds a new Interface for the first subnet id available
* Verifies the new interface is in the routers table by checking that
the interface is present in the table
* Deletes the newly created interface
* Verifies the interface is no longer in the interfaces table
* Switches to the routers view by clicking on the breadcrumb link
* Follows the steps to delete the router
"""
self._create_subnet()
self._create_router()
routers_page = self.routers_page
router_interfaces_page = routers_page. \
go_to_interfaces_page(self.ROUTER_NAME)
self._create_interface(router_interfaces_page)
interface_name = router_interfaces_page.interface_name
self._delete_interface(router_interfaces_page, interface_name)
router_interfaces_page.switch_to_routers_page()
self._delete_router()
self._delete_subnet()
def test_router_delete_interface_by_row(self):
"""Tests the router interface creation and deletion by row action:
* Follows the steps to create a new router
* Clicks on the new router name from the routers table
* Moves to the Interfaces page/tab
* Adds a new Interface for the first subnet id available
* Verifies the new interface is in the routers table
* Deletes the newly created interface by row action
* Verifies the interface is no longer in the interfaces table
* Switches to the routers view by clicking on the breadcrumb link
* Follows the steps to delete the router
"""
self._create_subnet()
self._create_router()
routers_page = self.routers_page
router_interfaces_page = routers_page. \
go_to_interfaces_page(self.ROUTER_NAME)
self._create_interface(router_interfaces_page)
interface_name = router_interfaces_page.interface_name
router_interfaces_page.delete_interface_by_row_action(interface_name)
router_interfaces_page.switch_to_routers_page()
self._delete_router()
self._delete_subnet()
def test_router_overview_data(self):
self._create_router()
routers_page = self.routers_page
router_overview_page = routers_page.\
go_to_overview_page(self.ROUTER_NAME)
self.assertTrue(router_overview_page.
is_router_name_present(self.ROUTER_NAME))
self.assertTrue(router_overview_page.is_router_status("Active"))
network_overview_page = router_overview_page.go_to_router_network()
# By default the router is created in the 'public' network so the line
# below checks that such name is present in the network
# details/overview page
self.assertTrue(network_overview_page.is_network_name_present())
self.assertTrue(network_overview_page.is_network_status("Active"))
self._delete_router()
class TestAdminRouters(helpers.AdminTestCase):
ROUTER_NAME = helpers.gen_random_resource_name("router")
@decorators.services_required("neutron")
def test_router_create_admin(self):
"""tests the router creation and deletion functionalities:
* creates a new router for public network
* verifies the router appears in the routers table as active
* edits router name
* checks router name was updated properly
* deletes the newly created router
* verifies the router does not appear in the table after deletion
"""
routers_page = self.home_pg.go_to_project_network_routerspage()
routers_page.create_router(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(routers_page.is_router_present(self.ROUTER_NAME))
self.assertTrue(routers_page.is_router_active(self.ROUTER_NAME))
self.home_pg.go_to_admin_overviewpage()
admin_routers_page = self.home_pg.go_to_admin_network_routerspage()
self.assertTrue(routers_page.is_router_present(self.ROUTER_NAME))
self.assertTrue(routers_page.is_router_active(self.ROUTER_NAME))
new_name = "edited_" + self.ROUTER_NAME
admin_routers_page.edit_router(self.ROUTER_NAME, new_name=new_name)
self.assertEqual(
admin_routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(
admin_routers_page.is_router_present(new_name))
self.assertTrue(
admin_routers_page.is_router_active(new_name))
admin_routers_page.delete_router(new_name)
self.assertEqual(
admin_routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(admin_routers_page.is_router_present(new_name))

View File

@@ -0,0 +1,66 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import decorators
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestRouters(helpers.TestCase):
ROUTER_NAME = helpers.gen_random_resource_name("router")
@decorators.services_required("neutron")
def test_router_create(self):
"""Checks create, clear/set gateway, delete router functionality
Executed by non-admin user.
Steps:
1. Login to Horizon Dashboard as horizon user
2. Navigate to Project -> Network -> Routers page
3. Create new router
4. Check that the router appears in the routers table as active
5. Check that no Error messages present
6. Clear the gateway
7. Check that the router is still in the routers table
with no external network
8. Check that no Error messages present
9. Set the gateway to 'public' network
10. Check that no Error messages present
11. Check that router's external network is set to 'public'
12. Delete the router
13. Check that the router is absent in the routers table
14. Check that no Error messages present
"""
routers_page = self.home_pg.go_to_project_network_routerspage()
routers_page.create_router(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(routers_page.is_router_present(self.ROUTER_NAME))
self.assertTrue(routers_page.is_router_active(self.ROUTER_NAME))
routers_page.clear_gateway(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(routers_page.is_gateway_cleared(self.ROUTER_NAME))
routers_page.set_gateway(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(routers_page.is_gateway_set(self.ROUTER_NAME))
routers_page.delete_router(self.ROUTER_NAME)
self.assertEqual(
routers_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(routers_page.is_router_present(self.ROUTER_NAME))

View File

@@ -0,0 +1,118 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import random
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestSecuritygroup(helpers.TestCase):
SEC_GROUP_NAME = helpers.gen_random_resource_name("securitygroup")
RULE_PORT = str(random.randint(9000, 9999))
@property
def securitygroup_page(self):
return self.home_pg.\
go_to_project_network_securitygroupspage()
def _create_securitygroup(self):
page = self.securitygroup_page
rule_page = page.create_securitygroup(self.SEC_GROUP_NAME)
if rule_page:
self.assertEqual(
rule_page.find_messages_and_dismiss(), {messages.SUCCESS})
page = self.securitygroup_page
self.assertTrue(page.is_securitygroup_present(self.SEC_GROUP_NAME))
else:
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(page.is_securitygroup_present(self.SEC_GROUP_NAME))
def _delete_securitygroup(self):
page = self.securitygroup_page
page.delete_securitygroup(self.SEC_GROUP_NAME)
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(page.is_securitygroup_present(self.SEC_GROUP_NAME))
def _add_rule(self):
page = self.securitygroup_page
page = page.go_to_manage_rules(self.SEC_GROUP_NAME)
page.create_rule(self.RULE_PORT)
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(page.is_port_present(self.RULE_PORT))
def _delete_rule_by_table_action(self):
page = self.securitygroup_page
page = page.go_to_manage_rules(self.SEC_GROUP_NAME)
page.delete_rules(self.RULE_PORT)
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(page.is_port_present(self.RULE_PORT))
def _delete_rule_by_row_action(self):
page = self.securitygroup_page
page = page.go_to_manage_rules(self.SEC_GROUP_NAME)
page.delete_rule(self.RULE_PORT)
self.assertEqual(
page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(page.is_port_present(self.RULE_PORT))
def test_securitygroup_create_delete(self):
"""tests the security group creation and deletion functionalities:
* creates a new security group
* verifies the security group appears in the security groups table
* deletes the newly created security group
* verifies the security group does not appear in the table after
deletion
"""
self._create_securitygroup()
self._delete_securitygroup()
def test_managerules_create_delete_by_row(self):
"""tests the manage rules creation and deletion functionalities:
* create a new security group
* verifies the security group appears in the security groups table
* creates a new rule
* verifies the rule appears in the rules table
* delete the newly created rule
* verifies the rule does not appear in the table after deletion
* deletes the newly created security group
* verifies the security group does not appear in the table after
deletion
"""
self._create_securitygroup()
self._add_rule()
self._delete_rule_by_row_action()
self._delete_securitygroup()
def test_managerules_create_delete_by_table(self):
"""tests the manage rules creation and deletion functionalities:
* create a new security group
* verifies the security group appears in the security groups table
* creates a new rule
* verifies the rule appears in the rules table
* delete the newly created rule
* verifies the rule does not appear in the table after deletion
* deletes the newly created security group
* verifies the security group does not appear in the table after
deletion
"""
self._create_securitygroup()
self._add_rule()
self._delete_rule_by_table_action()
self._delete_securitygroup()

View File

@@ -0,0 +1,163 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestDashboardHelp(helpers.TestCase):
def test_dashboard_help_redirection(self):
"""Verifies Help link redirects to the right URL."""
self.home_pg.go_to_help_page()
self.home_pg._wait_until(lambda _: self.home_pg.is_nth_window_opened(2)
)
self.home_pg.switch_window()
self.home_pg.is_help_page()
self.assertIn(self.CONFIG.dashboard.help_url,
self.home_pg.get_url_current_page(),
"help link did not redirect to the right URL")
self.home_pg.close_window()
self.home_pg.switch_window()
class TestThemePicker(helpers.TestCase):
DEFAULT_THEME = 'default'
MATERIAL_THEME = 'material'
def test_switch_to_material_theme(self):
"""Verifies that material theme is available and switchable to."""
self.home_pg.choose_theme(self.MATERIAL_THEME)
self.assertTrue(self.home_pg.topbar.is_material_theme_enabled)
self.home_pg.choose_theme(self.DEFAULT_THEME)
self.assertFalse(self.home_pg.topbar.is_material_theme_enabled)
class TestPasswordChange(helpers.TestCase):
NEW_PASSWORD = "123"
def _reset_password(self):
passwordchange_page = self.home_pg.go_to_settings_changepasswordpage()
# Per unique_last_password policy, we need to do unique password reset
# before resetting default password.
unique_last_password_count = int(
self.CONFIG.identity.unique_last_password_count)
old_password = int(self.NEW_PASSWORD)
for x in range(1, unique_last_password_count):
new_password = old_password + 1
passwordchange_page = self.home_pg.\
go_to_settings_changepasswordpage()
passwordchange_page.change_password(
str(old_password), str(new_password))
self.home_pg = self.login_pg.login(
user=self.TEST_USER_NAME, password=new_password)
old_password = new_password
passwordchange_page = self.home_pg.go_to_settings_changepasswordpage()
passwordchange_page.reset_to_default_password(old_password)
def _login(self):
self.login_pg.login()
self.assertTrue(self.home_pg.is_logged_in,
"Failed to login with default password")
def test_password_change(self):
# Changes the password, verifies it was indeed changed and
# resets to default password.
passwordchange_page = self.home_pg.go_to_settings_changepasswordpage()
try:
passwordchange_page.change_password(self.TEST_PASSWORD,
self.NEW_PASSWORD)
self.home_pg = self.login_pg.login(
user=self.TEST_USER_NAME, password=self.NEW_PASSWORD)
self.assertTrue(self.home_pg.is_logged_in,
"Failed to login with new password")
finally:
self._reset_password()
self._login()
def test_show_message_after_logout(self):
# Ensure an informational message is shown on the login page
# after the user is logged out.
passwordchange_page = self.home_pg.go_to_settings_changepasswordpage()
try:
passwordchange_page.change_password(self.TEST_PASSWORD,
self.NEW_PASSWORD)
self.assertTrue(
self.login_pg.is_logout_reason_displayed(),
"The logout reason message was not found on the login page")
finally:
self.login_pg.login(
user=self.TEST_USER_NAME, password=self.NEW_PASSWORD)
self._reset_password()
self._login()
class TestUserSettings(helpers.TestCase):
def verify_user_settings_change(self, settings_page, changed_settings):
language = settings_page.settings_form.language.value
timezone = settings_page.settings_form.timezone.value
pagesize = settings_page.settings_form.pagesize.value
loglines = settings_page.settings_form.instance_log_length.value
user_settings = (("Language", changed_settings["language"], language),
("Timezone", changed_settings["timezone"], timezone),
("Pagesize", changed_settings["pagesize"],
pagesize), ("Loglines", changed_settings["loglines"],
loglines))
for (setting, expected, observed) in user_settings:
self.assertEqual(
expected, observed, "expected %s: %s, instead found: %s" %
(setting, expected, observed))
def test_user_settings_change(self):
"""tests the user's settings options:
* changes the system's language
* changes the timezone
* changes the number of items per page (page size)
* changes the number of log lines to be shown per instance
* verifies all changes were successfully executed
"""
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_language("es")
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
settings_page.change_timezone("Asia/Jerusalem")
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
settings_page.change_pagesize("30")
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
settings_page.change_loglines("50")
self.assertEqual(
settings_page.find_messages_and_dismiss(), {messages.SUCCESS})
changed_settings = {
"language": "es",
"timezone": "Asia/Jerusalem",
"pagesize": "30",
"loglines": "50"
}
self.verify_user_settings_change(settings_page, changed_settings)
settings_page.return_to_default_settings()
self.verify_user_settings_change(settings_page,
settings_page.DEFAULT_SETTINGS)

View File

@@ -0,0 +1,33 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestUser(helpers.AdminTestCase):
USER_NAME = helpers.gen_random_resource_name("user")
def test_create_delete_user(self):
users_page = self.home_pg.go_to_identity_userspage()
password = self.TEST_PASSWORD
users_page.create_user(self.USER_NAME, password=password,
project='admin', role='admin')
self.assertEqual(
users_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(users_page.is_user_present(self.USER_NAME))
users_page.delete_user(self.USER_NAME)
self.assertEqual(
users_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(users_page.is_user_present(self.USER_NAME))

View File

@@ -0,0 +1,282 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import pytest
from openstack_dashboard.test.integration_tests import config
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
CONFIG = config.get_config()
class TestVolumeSnapshotsBasic(helpers.TestCase):
"""Login as demo user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
VOLUME_SNAPSHOT_NAME = helpers.gen_random_resource_name("volume_snapshot")
@property
def volumes_snapshot_page(self):
return self.home_pg.go_to_project_volumes_snapshotspage()
def setUp(self):
"""Setup: create volume"""
super().setUp()
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.create_volume(self.VOLUME_NAME)
volumes_page.find_messages_and_dismiss()
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'Available'))
def cleanup():
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.delete_volume(self.VOLUME_NAME)
volumes_page.find_messages_and_dismiss()
self.assertTrue(volumes_page.is_volume_deleted(self.VOLUME_NAME))
self.addCleanup(cleanup)
def test_create_edit_delete_volume_snapshot(self):
"""Test checks create/delete volume snapshot action
Steps:
1. Login to Horizon Dashboard
2. Navigate to Project -> Volumes -> Volumes page
3. Create snapshot for existed volume
4. Check that no ERROR appears
5. Check that snapshot is in the list
6. Check that snapshot has reference to correct volume
7. Edit snapshot name and description
8. Delete volume snapshot from proper page
9. Check that volume snapshot not in the list
"""
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_snapshot_page = volumes_page.create_volume_snapshot(
self.VOLUME_NAME, self.VOLUME_SNAPSHOT_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_snapshot_page.is_volume_snapshot_available(
self.VOLUME_SNAPSHOT_NAME))
actual_volume_name = volumes_snapshot_page.get_volume_name(
self.VOLUME_SNAPSHOT_NAME)
self.assertEqual(self.VOLUME_NAME, actual_volume_name)
new_name = "new_" + self.VOLUME_SNAPSHOT_NAME
volumes_snapshot_page = \
self.home_pg.go_to_project_volumes_snapshotspage()
volumes_snapshot_page.edit_snapshot(self.VOLUME_SNAPSHOT_NAME,
new_name, "description")
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_snapshot_page.
is_volume_snapshot_available(new_name))
volumes_snapshot_page.delete_volume_snapshot(new_name)
self.assertEqual(
volumes_snapshot_page.find_messages_and_dismiss(),
{messages.SUCCESS})
self.assertTrue(volumes_snapshot_page.is_volume_snapshot_deleted(
new_name))
def test_volume_snapshots_pagination(self):
"""This test checks volumes snapshots pagination
Steps:
1) Login to Horizon Dashboard
2) Go to Project -> Volumes -> Volumes tab, create
volumes and 3 snapshots
3) Navigate to user settings page
4) Change 'Items Per Page' value to 1
5) Go to Project -> Volumes -> Snapshots tab
or Admin -> Volume -> Snapshots tab
(depends on user)
6) Check that only 'Next' link is available, only one snapshot is
available (and it has correct name)
7) Click 'Next' and check that both 'Prev' and 'Next' links are
available, only one snapshot is available (and it has correct name)
8) Click 'Next' and check that only 'Prev' link is available,
only one snapshot is visible (and it has correct name)
9) Click 'Prev' and check result (should be the same as for step7)
10) Click 'Prev' and check result (should be the same as for step6)
11) Go to user settings page and restore 'Items Per Page'
12) Delete created snapshots and volumes
"""
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
count = 3
items_per_page = 1
snapshot_names = ["{0}_{1}".format(self.VOLUME_SNAPSHOT_NAME, i) for i
in range(count)]
for i, name in enumerate(snapshot_names):
volumes_snapshot_page = volumes_page.create_volume_snapshot(
self.VOLUME_NAME, name)
volumes_page.find_messages_and_dismiss()
self.assertTrue(
volumes_snapshot_page.is_volume_snapshot_available(name))
if i < count - 1:
self.home_pg.go_to_project_volumes_volumespage()
first_page_definition = {'Next': True, 'Prev': False,
'Count': items_per_page,
'Names': [snapshot_names[2]]}
second_page_definition = {'Next': True, 'Prev': True,
'Count': items_per_page,
'Names': [snapshot_names[1]]}
third_page_definition = {'Next': False, 'Prev': True,
'Count': items_per_page,
'Names': [snapshot_names[0]]}
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(items_per_page)
settings_page.find_messages_and_dismiss()
volumes_snapshot_page = self.volumes_snapshot_page
volumes_snapshot_page.volumesnapshots_table.assert_definition(
first_page_definition)
volumes_snapshot_page.volumesnapshots_table.turn_next_page()
volumes_snapshot_page.volumesnapshots_table.assert_definition(
second_page_definition)
volumes_snapshot_page.volumesnapshots_table.turn_next_page()
volumes_snapshot_page.volumesnapshots_table.assert_definition(
third_page_definition)
volumes_snapshot_page.volumesnapshots_table.turn_prev_page()
volumes_snapshot_page.volumesnapshots_table.assert_definition(
second_page_definition)
volumes_snapshot_page.volumesnapshots_table.turn_prev_page()
volumes_snapshot_page.volumesnapshots_table.assert_definition(
first_page_definition)
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize()
settings_page.find_messages_and_dismiss()
volumes_snapshot_page = self.volumes_snapshot_page
volumes_snapshot_page.delete_volume_snapshots(snapshot_names)
volumes_snapshot_page.find_messages_and_dismiss()
for name in snapshot_names:
volumes_snapshot_page.is_volume_snapshot_deleted(name)
class TestVolumeSnapshotsAdmin(helpers.AdminTestCase,
TestVolumeSnapshotsBasic):
"""Login as admin user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
VOLUME_SNAPSHOT_NAME = helpers.gen_random_resource_name("volume_snapshot")
@property
def volumes_snapshot_page(self):
return self.home_pg.go_to_project_volumes_snapshotspage()
def test_create_edit_delete_volume_snapshot(self):
super().test_create_edit_delete_volume_snapshot()
def test_volume_snapshots_pagination(self):
super().test_volume_snapshots_pagination()
class TestVolumeSnapshotsAdvanced(helpers.TestCase):
"""Login as demo user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
VOLUME_SNAPSHOT_NAME = helpers.gen_random_resource_name("volume_snapshot")
@property
def volumes_snapshot_page(self):
return self.home_pg.go_to_project_volumes_snapshotspage()
def setUp(self):
"""Setup: create volume"""
super().setUp()
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.create_volume(self.VOLUME_NAME)
volumes_page.find_messages_and_dismiss()
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'Available'))
def cleanup():
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.delete_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_deleted(self.VOLUME_NAME))
self.addCleanup(cleanup)
def create_volume_from_snapshot(self):
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_snapshot_page = volumes_page.create_volume_snapshot(
self.VOLUME_NAME, self.VOLUME_SNAPSHOT_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_snapshot_page.is_volume_snapshot_available(
self.VOLUME_SNAPSHOT_NAME))
new_volume = 'new_' + self.VOLUME_NAME
volumes_snapshot_page.create_volume_from_snapshot(
self.VOLUME_SNAPSHOT_NAME, new_volume)
self.assertTrue(volumes_page.is_volume_present(new_volume))
self.assertTrue(volumes_page.is_volume_status(new_volume, 'Available'))
return new_volume
def delete_snapshot(self):
volumes_snapshot_page = self.volumes_snapshot_page
volumes_snapshot_page.delete_volume_snapshot(self.VOLUME_SNAPSHOT_NAME)
self.assertEqual(
volumes_snapshot_page.find_messages_and_dismiss(),
{messages.SUCCESS})
self.assertTrue(volumes_snapshot_page.is_volume_snapshot_deleted(
self.VOLUME_SNAPSHOT_NAME))
def delete_volume(self, new_volume):
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.delete_volume(new_volume)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_deleted(new_volume))
@pytest.mark.skipif(
not CONFIG.volume.allow_delete_snapshot_before_volume,
reason="Skipped due to allow_delete_snapshot_before_volume=False")
def test_create_volume_from_snapshot(self):
"""Test checks possibility to create volume from snapshot
Steps:
1. Login to Horizon Dashboard as regular user
2. Navigate to Project -> Volumes -> Volumes page
3. Create snapshot for existed volume
4. Create new volume from snapshot
5. Check the volume is created and has 'Available' status
6. Delete volume snapshot
7. Delete volume
"""
new_volume = self.create_volume_from_snapshot()
self.delete_snapshot()
self.delete_volume(new_volume)
def test_create_volume_from_snapshot_delete_volume_first(self):
"""Test checks possibility to create volume from snapshot
Steps:
1. Login to Horizon Dashboard as regular user
2. Navigate to Project -> Volumes -> Volumes page
3. Create snapshot for existed volume
4. Create new volume from snapshot
5. Check the volume is created and has 'Available' status
6. Delete volume
7. Delete volume snapshot
"""
new_volume = self.create_volume_from_snapshot()
self.delete_volume(new_volume)
self.delete_snapshot()

View File

@@ -0,0 +1,344 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import time
import pytest
from openstack_dashboard.test.integration_tests import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestVolumesBasic(helpers.TestCase):
"""Login as demo user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
@property
def volumes_page(self):
return self.home_pg.go_to_project_volumes_volumespage()
def test_volume_create_edit_delete(self):
"""This test case checks create, edit, delete volume functionality:
Steps:
1. Login to Horizon Dashboard
2. Navigate to Project -> Compute -> Volumes page
3. Create new volume
4. Check that the volume is in the list
5. Check that no Error messages present
6. Edit the volume
7. Check that the volume is still in the list
8. Check that no Error messages present
9. Delete the volume via proper page (depends on user)
10. Check that the volume is absent in the list
11. Check that no Error messages present
"""
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
volumes_page.create_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_present(self.VOLUME_NAME))
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'Available'))
new_name = "edited_" + self.VOLUME_NAME
volumes_page.edit_volume(self.VOLUME_NAME, new_name, "description")
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_present(new_name))
self.assertTrue(volumes_page.is_volume_status(new_name, 'Available'))
volumes_page = self.volumes_page
volumes_page.delete_volume(new_name)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_deleted(new_name))
# NOTE(tsufiev): A short regression test on bug 1553314: we try to
# re-open 'Create Volume' button after the volume was deleted. If the
# regression occurs, the form won't appear (because link is going to be
# invalid in this case). Give JavaScript callbacks an additional second
# to do all the job and possibly cause the regression.
if not isinstance(self, helpers.AdminTestCase):
time.sleep(1)
form = volumes_page.volumes_table.create_volume()
form.cancel()
def test_volumes_pagination(self):
"""This test checks volumes pagination
Steps:
1) Login to Horizon Dashboard
2) Go to Project -> Volumes -> Volumes tab and create
three volumes
3) Navigate to user settings page
4) Change 'Items Per Page' value to 1
5) Go to Project -> Volumes -> Volumes tab or
Admin -> Volume -> Volumes tab (depends on user)
6) Check that only 'Next' link is available, only one volume is
available (and it has correct name)
7) Click 'Next' and check that both 'Prev' and 'Next' links are
available, only one volume is available (and it has correct name)
8) Click 'Next' and check that only 'Prev' link is available,
only one volume is visible (and it has correct name)
9) Click 'Prev' and check result (should be the same as for step7)
10) Click 'Prev' and check result (should be the same as for step6)
11) Go to user settings page and restore 'Items Per Page'
12) Delete created volumes
"""
volumes_page = self.home_pg.go_to_project_volumes_volumespage()
# delete any old instances
garbage = volumes_page.volumes_table.get_column_data(
name_column='Name')
if garbage:
volumes_page.delete_volumes(garbage)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
count = 3
items_per_page = 1
volumes_names = ["{0}_{1}".format(self.VOLUME_NAME, i) for i in
range(count)]
for volume_name in volumes_names:
volumes_page.create_volume(volume_name)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_present(volume_name))
self.assertTrue(volumes_page.is_volume_status(volume_name,
'Available'))
first_page_definition = {'Next': True, 'Prev': False,
'Count': items_per_page,
'Names': [volumes_names[2]]}
second_page_definition = {'Next': True, 'Prev': True,
'Count': items_per_page,
'Names': [volumes_names[1]]}
third_page_definition = {'Next': False, 'Prev': True,
'Count': items_per_page,
'Names': [volumes_names[0]]}
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize(items_per_page)
settings_page.find_messages_and_dismiss()
volumes_page = self.volumes_page
volumes_page.volumes_table.assert_definition(first_page_definition)
volumes_page.volumes_table.turn_next_page()
volumes_page.volumes_table.assert_definition(second_page_definition)
volumes_page.volumes_table.turn_next_page()
volumes_page.volumes_table.assert_definition(third_page_definition)
volumes_page.volumes_table.turn_prev_page()
volumes_page.volumes_table.assert_definition(second_page_definition)
volumes_page.volumes_table.turn_prev_page()
volumes_page.volumes_table.assert_definition(first_page_definition)
settings_page = self.home_pg.go_to_settings_usersettingspage()
settings_page.change_pagesize()
settings_page.find_messages_and_dismiss()
volumes_page = self.volumes_page
volumes_page.delete_volumes(volumes_names)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.are_volumes_deleted(volumes_names))
class TestAdminVolumes(helpers.AdminTestCase, TestVolumesBasic):
"""Login as admin user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
@property
def volumes_page(self):
return self.home_pg.go_to_project_volumes_volumespage()
class TestVolumesAdvanced(helpers.TestCase):
"""Login as demo user"""
VOLUME_NAME = helpers.gen_random_resource_name("volume")
@property
def volumes_page(self):
return self.home_pg.go_to_project_volumes_volumespage()
def test_manage_volume_attachments(self):
"""This test case checks attach/detach actions for volume
Steps:
1. Login to Horizon Dashboard as horizon user
2. Navigate to Project -> Compute -> Instances, create instance
3. Navigate to Project -> Volumes -> Volumes, create volume
4. Attach volume to instance from step2
5. Check that volume status and link to instance
6. Detach volume from instance
7. Check volume status
8. Delete volume and instance
"""
instance_name = helpers.gen_random_resource_name('instance')
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.create_instance(instance_name)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_active(instance_name))
volumes_page = self.volumes_page
volumes_page.create_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'Available'))
volumes_page.attach_volume_to_instance(self.VOLUME_NAME, instance_name)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'In-use'))
self.assertTrue(
volumes_page.is_volume_attached_to_instance(self.VOLUME_NAME,
instance_name))
volumes_page.detach_volume_from_instance(self.VOLUME_NAME,
instance_name)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(volumes_page.is_volume_status(self.VOLUME_NAME,
'Available'))
volumes_page.delete_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_deleted(self.VOLUME_NAME))
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.delete_instance(instance_name)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_deleted(instance_name))
class TestVolumesActions(helpers.TestCase):
VOLUME_NAME = helpers.gen_random_resource_name("volume")
IMAGE_NAME = helpers.gen_random_resource_name("image")
INSTANCE_NAME = helpers.gen_random_resource_name("instance")
@property
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
volumes_page.create_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_present(self.VOLUME_NAME))
self.assertTrue(
volumes_page.is_volume_status(self.VOLUME_NAME, 'Available'))
def cleanup():
volumes_page = self.volumes_page
volumes_page.delete_volume(self.VOLUME_NAME)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
volumes_page.is_volume_deleted(self.VOLUME_NAME))
self.addCleanup(cleanup)
def test_volume_extend(self):
"""This test case checks extend volume functionality:
Steps:
1. Check current volume size
2. Extend volume
3. Check that no Error messages present
4. Check that the volume is still in the list
5. Check that the volume size is changed
"""
volumes_page = self.volumes_page
orig_size = volumes_page.get_size(self.VOLUME_NAME)
volumes_page.extend_volume(self.VOLUME_NAME, orig_size + 1)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(
volumes_page.is_volume_status(self.VOLUME_NAME, 'Available'))
new_size = volumes_page.get_size(self.VOLUME_NAME)
self.assertLess(orig_size, new_size)
def test_volume_upload_to_image(self):
"""This test case checks upload volume to image functionality:
Steps:
1. Upload volume to image with some disk format
2. Check that image is created
3. Check that no Error messages present
4. Delete the image
5. Repeat actions for all disk formats
"""
volumes_page = self.volumes_page
all_formats = {"qcow2": 'QCOW2', "raw": 'RAW', "vdi": 'VDI',
"vmdk": 'VMDK'}
for disk_format in all_formats:
volumes_page.upload_volume_to_image(
self.VOLUME_NAME, self.IMAGE_NAME, disk_format)
self.assertEqual(
volumes_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(volumes_page.is_volume_status(
self.VOLUME_NAME, 'Available'))
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.assertEqual(
images_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
volumes_page = \
self.home_pg.go_to_project_volumes_volumespage()
@pytest.mark.skip(reason="Bug 1930420")
def test_volume_launch_as_instance(self):
"""This test case checks launch volume as instance functionality:
Steps:
1. Launch volume as instance
2. Check that instance is created
3. Check that no Error messages present
4. Check that instance status is 'active'
5. Check that volume status is 'in use'
6. Delete instance
"""
self.volumes_page.launch_instance(self.VOLUME_NAME, self.INSTANCE_NAME)
self.assertEqual(
self.volumes_page.find_messages_and_dismiss(), {messages.SUCCESS})
instances_page = self.home_pg.go_to_project_compute_instancespage()
self.assertTrue(instances_page.is_instance_active(self.INSTANCE_NAME))
self.volumes_page = self.home_pg.go_to_project_volumes_volumespage()
self.assertTrue(self.volumes_page.is_volume_status(self.VOLUME_NAME,
'In-use'))
self.assertIn(self.INSTANCE_NAME,
self.volumes_page.get_attach_instance(self.VOLUME_NAME))
instances_page = self.home_pg.go_to_project_compute_instancespage()
instances_page.delete_instance(self.INSTANCE_NAME)
self.assertEqual(
instances_page.find_messages_and_dismiss(), {messages.INFO})
self.assertTrue(instances_page.is_instance_deleted(self.INSTANCE_NAME))
self.volumes_page = self.home_pg.go_to_project_volumes_volumespage()

View File

@@ -0,0 +1,114 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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 import helpers
from openstack_dashboard.test.integration_tests.regions import messages
class TestAdminVolumeTypes(helpers.AdminTestCase):
VOLUME_TYPE_NAME = helpers.gen_random_resource_name("volume_type")
def test_volume_type_create_delete(self):
"""This test case checks create, delete volume type:
Steps:
1. Login to Horizon Dashboard as admin user
2. Navigate to Admin -> Volume -> Volume Types page
3. Create new volume type
4. Check that the volume type is in the list
5. Check that no Error messages present
6. Delete the volume type
7. Check that the volume type is absent in the list
8. Check that no Error messages present
"""
volume_types_page = self.home_pg.go_to_admin_volume_volumetypespage()
volume_types_page.create_volume_type(self.VOLUME_TYPE_NAME)
self.assertEqual(
volume_types_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(volume_types_page.is_volume_type_present(
self.VOLUME_TYPE_NAME))
volume_types_page.delete_volume_type(self.VOLUME_TYPE_NAME)
self.assertEqual(
volume_types_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(volume_types_page.is_volume_type_deleted(
self.VOLUME_TYPE_NAME))
class TestQoSSpec(helpers.AdminTestCase):
QOS_SPEC_NAME = helpers.gen_random_resource_name("qos_spec")
def test_qos_spec_create_delete(self):
"""tests the QoS Spec creation and deletion functionality
* creates a new QoS Spec
* verifies the QoS Spec appears in the QoS Specs table
* deletes the newly created QoS Spec
* verifies the QoS Spec does not appear in the table after deletion
"""
qos_spec_page = self.home_pg.go_to_admin_volume_volumetypespage()
qos_spec_page.create_qos_spec(self.QOS_SPEC_NAME)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(qos_spec_page.is_qos_spec_present(self.QOS_SPEC_NAME))
qos_spec_page.delete_qos_specs(self.QOS_SPEC_NAME)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(qos_spec_page.is_qos_spec_present(self.QOS_SPEC_NAME))
def test_qos_spec_edit_consumer(self):
"""tests Edit Consumer of QoS Spec functionality
* creates a new QoS Spec
* verifies the QoS Spec appears in the QoS Specs table
* edit consumer of created QoS Spec (check all options - front-end,
both, back-end)
* verifies current consumer of the QoS Spec in the QoS Specs table
* deletes the newly created QoS Spec
* verifies the QoS Spec does not appear in the table after deletion
"""
qos_spec_name = helpers.gen_random_resource_name("qos_spec")
qos_spec_page = self.home_pg.go_to_admin_volume_volumetypespage()
nova_compute_consumer = 'front-end'
both_consumers = 'both'
cinder_consumer = 'back-end'
qos_spec_page.create_qos_spec(qos_spec_name)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertTrue(qos_spec_page.is_qos_spec_present(qos_spec_name))
qos_spec_page.edit_consumer(qos_spec_name, nova_compute_consumer)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertEqual(
qos_spec_page.get_consumer(qos_spec_name), nova_compute_consumer)
qos_spec_page.edit_consumer(qos_spec_name, both_consumers)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertEqual(
qos_spec_page.get_consumer(qos_spec_name), both_consumers)
qos_spec_page.edit_consumer(qos_spec_name, cinder_consumer)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertEqual(
qos_spec_page.get_consumer(qos_spec_name), cinder_consumer)
qos_spec_page.delete_qos_specs(qos_spec_name)
self.assertEqual(
qos_spec_page.find_messages_and_dismiss(), {messages.SUCCESS})
self.assertFalse(qos_spec_page.is_qos_spec_present(qos_spec_name))

View File

@@ -0,0 +1,84 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import signal
import subprocess
from tempfile import mktemp
from threading import Thread
import time
LOG = logging.getLogger(__name__)
class VideoRecorder(object):
def __init__(self, width, height, display='0.0', frame_rate=15):
self.is_launched = False
self.file_path = mktemp() + '.mp4'
# ffmpeg -video_size 1921x1080 -framerate 15 -f x11grab -i :0.0
self._cmd = ['ffmpeg',
'-video_size', '{}x{}'.format(width, height),
'-framerate', str(frame_rate),
'-f', 'x11grab',
'-i', ':{}'.format(display),
self.file_path]
def start(self):
if self.is_launched:
LOG.warning('Video recording is running already')
return
if not os.environ.get('FFMPEG_INSTALLED', False):
LOG.error("ffmpeg isn't installed. Video recording is skipped")
return
fnull = open(os.devnull, 'w')
LOG.info('Record video via %s', ' '.join(self._cmd))
self._popen = subprocess.Popen(self._cmd, stdout=fnull, stderr=fnull)
self.is_launched = True
def stop(self):
if not self.is_launched:
LOG.warning('Video recording is stopped already')
return
self._popen.send_signal(signal.SIGINT)
def terminate_avconv():
limit = time.time() + 10
while time.time() < limit:
time.sleep(0.1)
if self._popen.poll() is not None:
return
os.kill(self._popen.pid, signal.SIGTERM)
t = Thread(target=terminate_avconv)
t.start()
self._popen.communicate()
t.join()
self.is_launched = False
def clear(self):
if self.is_launched:
LOG.error("Video recording is running still")
return
if not os.path.isfile(self.file_path):
LOG.warning("%s is absent already", self.file_path)
return
os.remove(self.file_path)

View File

@@ -1,8 +0,0 @@
---
features:
- >
Removed all legacy integration test code and dependencies from the Horizon
codebase. This includes the complete removal of the old Selenium-based
integration test framework, page objects, test cases, and related utilities.
This change helps simplify the project structure by removing unused testing
infrastructure.

View File

@@ -6,4 +6,4 @@
./tools/gate/integration/pre_test_hook.sh
./tools/list-horizon-plugins.py
./tools/unit_tests.sh
./tools/selenium_tests.sh
./tools/selenium_tests.sh

13
tox.ini
View File

@@ -78,6 +78,19 @@ commands =
find . -type f -name "*.pyc" -delete
bash {toxinidir}/tools/selenium_tests.sh {toxinidir} {posargs}
[testenv:integration]
# Run integration tests only
passenv =
DISPLAY
FFMPEG_INSTALLED
XAUTHORITY
setenv =
INTEGRATION_TESTS=1
SELENIUM_HEADLESS=True
commands =
oslo-config-generator --namespace openstack_dashboard_integration_tests
pytest --ds=openstack_dashboard.test.settings -v -x --junitxml="{toxinidir}/test_reports/integration_test_results.xml" --html="{toxinidir}/test_reports/integration_test_results.html" --self-contained-html {posargs:{toxinidir}/openstack_dashboard/test/integration_tests}
[testenv:integration-pytest]
# Run pytest integration tests only
passenv =