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:
@@ -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>
|
||||
|
||||
31
openstack_dashboard/test/integration_tests/README.rst
Normal file
31
openstack_dashboard/test/integration_tests/README.rst
Normal 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
|
||||
169
openstack_dashboard/test/integration_tests/basewebobject.py
Normal file
169
openstack_dashboard/test/integration_tests/basewebobject.py
Normal 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)
|
||||
176
openstack_dashboard/test/integration_tests/decorators.py
Normal file
176
openstack_dashboard/test/integration_tests/decorators.py
Normal 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
|
||||
355
openstack_dashboard/test/integration_tests/helpers.py
Normal file
355
openstack_dashboard/test/integration_tests/helpers.py
Normal 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()
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
89
openstack_dashboard/test/integration_tests/pages/basepage.py
Normal file
89
openstack_dashboard/test/integration_tests/pages/basepage.py
Normal 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
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
341
openstack_dashboard/test/integration_tests/pages/navigation.py
Normal file
341
openstack_dashboard/test/integration_tests/pages/navigation.py
Normal 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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
61
openstack_dashboard/test/integration_tests/regions/bars.py
Normal file
61
openstack_dashboard/test/integration_tests/regions/bars.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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."
|
||||
652
openstack_dashboard/test/integration_tests/regions/forms.py
Normal file
652
openstack_dashboard/test/integration_tests/regions/forms.py
Normal 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))
|
||||
468
openstack_dashboard/test/integration_tests/regions/menus.py
Normal file
468
openstack_dashboard/test/integration_tests/regions/menus.py
Normal 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()
|
||||
@@ -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"
|
||||
520
openstack_dashboard/test/integration_tests/regions/tables.py
Normal file
520
openstack_dashboard/test/integration_tests/regions/tables.py
Normal 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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
))
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
384
openstack_dashboard/test/integration_tests/tests/test_images.py
Normal file
384
openstack_dashboard/test/integration_tests/tests/test_images.py
Normal 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))
|
||||
@@ -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('')
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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!")
|
||||
210
openstack_dashboard/test/integration_tests/tests/test_router.py
Normal file
210
openstack_dashboard/test/integration_tests/tests/test_router.py
Normal 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))
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
344
openstack_dashboard/test/integration_tests/tests/test_volumes.py
Normal file
344
openstack_dashboard/test/integration_tests/tests/test_volumes.py
Normal 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()
|
||||
@@ -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))
|
||||
84
openstack_dashboard/test/integration_tests/video_recorder.py
Normal file
84
openstack_dashboard/test/integration_tests/video_recorder.py
Normal 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)
|
||||
@@ -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.
|
||||
@@ -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
13
tox.ini
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user