Merge "Add pytest-based selenium tests"
This commit is contained in:
commit
638d411637
@ -19,6 +19,9 @@ DashboardGroup = [
|
||||
cfg.StrOpt('dashboard_url',
|
||||
default='http://localhost/dashboard/',
|
||||
help='Where the dashboard can be found'),
|
||||
cfg.StrOpt('auth_url',
|
||||
default='http://localhost/identity/v3',
|
||||
help='Where the keystone can be found'),
|
||||
cfg.StrOpt('help_url',
|
||||
default='https://docs.openstack.org/',
|
||||
help='Dashboard help page url'),
|
||||
|
@ -6,6 +6,9 @@
|
||||
# Where the dashboard can be found (string value)
|
||||
dashboard_url=http://localhost/dashboard/
|
||||
|
||||
# Where the keystone endpoint is
|
||||
auth_url=http://localhost/identity/v3
|
||||
|
||||
# Dashboard help page url (string value)
|
||||
help_url=https://docs.openstack.org/
|
||||
|
||||
|
233
openstack_dashboard/test/selenium/conftest.py
Normal file
233
openstack_dashboard/test/selenium/conftest.py
Normal file
@ -0,0 +1,233 @@
|
||||
# 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
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import xvfbwrapper
|
||||
|
||||
from horizon.test import webdriver
|
||||
from openstack_dashboard.test.integration_tests import config as horizon_config
|
||||
|
||||
|
||||
STASH_FAILED = pytest.StashKey[bool]()
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, driver, config):
|
||||
self.current_user = None
|
||||
self.current_project = None
|
||||
self.driver = driver
|
||||
self.credentials = {
|
||||
'user': (
|
||||
config.identity.username,
|
||||
config.identity.password,
|
||||
config.identity.home_project,
|
||||
),
|
||||
'admin': (
|
||||
config.identity.admin_username,
|
||||
config.identity.admin_password,
|
||||
config.identity.admin_home_project,
|
||||
),
|
||||
}
|
||||
self.logout_url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'auth',
|
||||
'logout',
|
||||
))
|
||||
|
||||
def login(self, user, project=None):
|
||||
if project is None:
|
||||
project = self.credentials[user][2]
|
||||
if self.current_user != user:
|
||||
username, password, home_project = self.credentials[user]
|
||||
self.driver.get(self.logout_url)
|
||||
user_field = self.driver.find_element_by_id('id_username')
|
||||
user_field.send_keys(username)
|
||||
pass_field = self.driver.find_element_by_id('id_password')
|
||||
pass_field.send_keys(password)
|
||||
button = self.driver.find_element_by_css_selector(
|
||||
'div.panel-footer button.btn')
|
||||
button.click()
|
||||
self.current_user = user
|
||||
self.current_project = self.driver.find_element_by_xpath(
|
||||
'//*[@class="context-project"]').text
|
||||
if self.current_project != project:
|
||||
dropdown_project = self.driver.find_element_by_xpath(
|
||||
'//*[@class="context-project"]//ancestor::ul')
|
||||
dropdown_project.click()
|
||||
selection = dropdown_project.find_element_by_xpath(
|
||||
f'//span[contains(text(),"{project}")]')
|
||||
selection.click()
|
||||
self.current_project = self.driver.find_element_by_xpath(
|
||||
'//*[@class="context-project"]').text
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def login(driver, config):
|
||||
session = Session(driver, config)
|
||||
return session.login
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""A hook to save the failure state of a test."""
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
|
||||
item.stash[STASH_FAILED] = item.stash.get(STASH_FAILED, False) or rep.failed
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def save_screenshot(request, report_dir, driver):
|
||||
yield None
|
||||
if not request.node.stash.get(STASH_FAILED, False):
|
||||
return
|
||||
screen_path = os.path.join(report_dir, 'screenshot.png')
|
||||
driver.get_screenshot_as_file(screen_path)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def save_page_source(request, report_dir, driver):
|
||||
yield None
|
||||
if not request.node.stash.get(STASH_FAILED, False):
|
||||
return
|
||||
source_path = os.path.join(report_dir, 'page.html')
|
||||
html_elem = driver.find_element_by_tag_name("html")
|
||||
page_source = html_elem.get_property("innerHTML")
|
||||
with open(source_path, 'w') as f:
|
||||
f.write(page_source)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function', autouse=True)
|
||||
def record_video(request, report_dir, xdisplay):
|
||||
if not os.environ.get('FFMPEG_INSTALLED', False):
|
||||
yield None
|
||||
return
|
||||
filepath = os.path.join(report_dir, 'video.mp4')
|
||||
frame_rate = 15
|
||||
display, width, height = xdisplay
|
||||
command = [
|
||||
'ffmpeg',
|
||||
'-video_size', '{}x{}'.format(width, height),
|
||||
'-framerate', str(frame_rate),
|
||||
'-f', 'x11grab',
|
||||
'-i', display,
|
||||
filepath,
|
||||
]
|
||||
fnull = open(os.devnull, 'w')
|
||||
popen = subprocess.Popen(command, stdout=fnull, stderr=fnull)
|
||||
yield None
|
||||
popen.send_signal(signal.SIGINT)
|
||||
|
||||
def terminate_process():
|
||||
limit = time.time() + 10
|
||||
while time.time() < limit:
|
||||
time.sleep(0.1)
|
||||
if popen.poll() is not None:
|
||||
return
|
||||
os.kill(popen.pid, signal.SIGTERM)
|
||||
|
||||
thread = Thread(target=terminate_process)
|
||||
thread.start()
|
||||
popen.communicate()
|
||||
thread.join()
|
||||
if not request.node.stash.get(STASH_FAILED, False):
|
||||
os.remove(filepath)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def xdisplay():
|
||||
IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False)
|
||||
if IS_SELENIUM_HEADLESS:
|
||||
width, height = 1920, 1080
|
||||
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(vdisplay, 'extra_xvfb_args'):
|
||||
# xvfbwrapper 0.2.8 or newer
|
||||
vdisplay.extra_xvfb_args.extend(args)
|
||||
else:
|
||||
vdisplay.xvfb_cmd.extend(args)
|
||||
vdisplay.start()
|
||||
display = vdisplay.new_display
|
||||
else:
|
||||
width, height = subprocess.check_output(
|
||||
'xdpyinfo | grep "dimensions:"', shell=True
|
||||
).decode().split(':', 1)[1].split()[0].strip().split('x')
|
||||
vdisplay = None
|
||||
display = subprocess.check_output(
|
||||
'xdpyinfo | grep "name of display:"', shell=True
|
||||
).decode().split(':', 1)[1].strip()
|
||||
yield display, width, height
|
||||
if vdisplay:
|
||||
vdisplay.stop()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def config():
|
||||
return horizon_config.get_config()
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def report_dir(request, config):
|
||||
root_path = os.path.dirname(os.path.abspath(horizon_config.__file__))
|
||||
test_name = request.node.nodeid.rsplit('/', 1)[1]
|
||||
report_dir = os.path.join(
|
||||
root_path, config.selenium.screenshots_directory, test_name)
|
||||
if not os.path.isdir(report_dir):
|
||||
os.makedirs(report_dir)
|
||||
yield report_dir
|
||||
try:
|
||||
os.rmdir(report_dir) # delete if empty
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def driver(config, xdisplay):
|
||||
# Start a virtual display server for running the tests headless.
|
||||
IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False)
|
||||
# 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'}
|
||||
driver = webdriver.WebDriver(
|
||||
desired_capabilities=desired_capabilities
|
||||
)
|
||||
if config.selenium.maximize_browser:
|
||||
driver.maximize_window()
|
||||
if IS_SELENIUM_HEADLESS: # force full screen in xvfb
|
||||
display, width, height = xdisplay
|
||||
driver.set_window_size(width, height)
|
||||
|
||||
driver.implicitly_wait(config.selenium.implicit_wait)
|
||||
driver.set_page_load_timeout(config.selenium.page_timeout)
|
||||
yield driver
|
||||
driver.quit()
|
59
openstack_dashboard/test/selenium/integration/conftest.py
Normal file
59
openstack_dashboard/test/selenium/integration/conftest.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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 openstack as openstack_sdk
|
||||
import pytest
|
||||
|
||||
|
||||
def create_conn(username, password, project, domain, auth_url):
|
||||
if not domain:
|
||||
domain = 'default'
|
||||
conn = openstack_sdk.connection.Connection(
|
||||
auth={
|
||||
"auth_url": auth_url,
|
||||
"user_domain_id": domain,
|
||||
"project_domain_id": domain,
|
||||
"project_name": project,
|
||||
"username": username,
|
||||
"password": password,
|
||||
},
|
||||
compute_api_version='2',
|
||||
verify=False,
|
||||
)
|
||||
conn.authorize()
|
||||
return conn
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def openstack_admin(config):
|
||||
conn = create_conn(
|
||||
config.identity.admin_username,
|
||||
config.identity.admin_password,
|
||||
config.identity.admin_home_project,
|
||||
config.identity.domain,
|
||||
config.dashboard.auth_url,
|
||||
)
|
||||
yield conn
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def openstack_demo(config):
|
||||
conn = create_conn(
|
||||
config.identity.username,
|
||||
config.identity.password,
|
||||
config.identity.home_project,
|
||||
config.identity.domain,
|
||||
config.dashboard.auth_url,
|
||||
)
|
||||
yield conn
|
||||
conn.close()
|
277
openstack_dashboard/test/selenium/integration/test_instances.py
Normal file
277
openstack_dashboard/test/selenium/integration/test_instances.py
Normal file
@ -0,0 +1,277 @@
|
||||
# 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 oslo_utils import uuidutils
|
||||
import pytest
|
||||
from selenium.common import exceptions
|
||||
|
||||
from openstack_dashboard.test.selenium import widgets
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def instance_name():
|
||||
return 'xhorizon_instance_%s' % uuidutils.generate_uuid(dashed=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_instance_demo(instance_name, openstack_demo, config):
|
||||
|
||||
instance = openstack_demo.create_server(
|
||||
instance_name,
|
||||
image=config.image.images_list[0],
|
||||
flavor=config.launch_instances.flavor,
|
||||
availability_zone=config.launch_instances.available_zone,
|
||||
network=config.network.external_network,
|
||||
wait=True,
|
||||
)
|
||||
yield instance
|
||||
openstack_demo.delete_server(instance_name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_instance_admin(instance_name, openstack_admin, config):
|
||||
|
||||
instance = openstack_admin.create_server(
|
||||
instance_name,
|
||||
image=config.image.images_list,
|
||||
flavor=config.launch_instances.flavor,
|
||||
availability_zone=config.launch_instances.available_zone,
|
||||
network=config.network.external_network,
|
||||
wait=True,
|
||||
)
|
||||
yield instance
|
||||
openstack_admin.delete_server(instance_name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_instance_demo(instance_name, openstack_demo):
|
||||
yield None
|
||||
openstack_demo.delete_server(
|
||||
instance_name,
|
||||
wait=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_instance_admin(instance_name, openstack_admin):
|
||||
yield None
|
||||
openstack_admin.delete_server(
|
||||
instance_name,
|
||||
wait=True,
|
||||
)
|
||||
|
||||
|
||||
def select_from_transfer_table(element, label):
|
||||
"""Choose row from available Images, Flavors, Networks, etc.
|
||||
|
||||
in launch tab for example: m1.tiny for Flavor, cirros for image, etc.
|
||||
"""
|
||||
|
||||
try:
|
||||
element.find_element_by_xpath(
|
||||
f"//*[text()='{label}']//ancestor::tr/td//*"
|
||||
f"[@class='btn btn-default fa fa-arrow-up']").click()
|
||||
except exceptions.NoSuchElementException:
|
||||
try:
|
||||
element.find_element_by_xpath(
|
||||
f"//*[text()='{label}']//ancestor::tr/td//*"
|
||||
f"[@class='btn btn-default fa fa-arrow-down']")
|
||||
except exceptions.NoSuchElementException:
|
||||
raise
|
||||
|
||||
|
||||
def create_new_volume_during_create_instance(driver, required_state):
|
||||
create_new_volume_btn = widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[@id='vol-create'][text()='{required_state}']", driver
|
||||
)
|
||||
create_new_volume_btn.click()
|
||||
|
||||
|
||||
def delete_volume_on_instance_delete(driver, required_state):
|
||||
delete_volume_btn = widgets.find_already_visible_element_by_xpath(
|
||||
f"//label[contains(@ng-model, 'vol_delete_on_instance_delete')]"
|
||||
f"[text()='{required_state}']", driver)
|
||||
delete_volume_btn.click()
|
||||
|
||||
|
||||
def test_create_instance_demo(login, driver, instance_name,
|
||||
clear_instance_demo, config):
|
||||
image = config.launch_instances.image_name
|
||||
network = config.network.external_network
|
||||
flavor = config.launch_instances.flavor
|
||||
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'instances',
|
||||
))
|
||||
driver.get(url)
|
||||
driver.find_element_by_link_text("Launch Instance").click()
|
||||
wizard = driver.find_element_by_css_selector("wizard")
|
||||
navigation = wizard.find_element_by_css_selector("div.wizard-nav")
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
"//*[@id='name']", wizard).send_keys(instance_name)
|
||||
navigation.find_element_by_link_text("Networks").click()
|
||||
network_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceNetworkForm]"
|
||||
)
|
||||
select_from_transfer_table(network_table, network)
|
||||
navigation.find_element_by_link_text("Flavor").click()
|
||||
flavor_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceFlavorForm]"
|
||||
)
|
||||
select_from_transfer_table(flavor_table, flavor)
|
||||
navigation.find_element_by_link_text("Source").click()
|
||||
source_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceSourceForm]"
|
||||
)
|
||||
# create_new_volume_during_create_instance(source_table, "No")
|
||||
delete_volume_on_instance_delete(source_table, "Yes")
|
||||
select_from_transfer_table(source_table, image)
|
||||
wizard.find_element_by_css_selector(
|
||||
"button.btn-primary.finish").click()
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[contains(text(),'{instance_name}')]//ancestor::tr/td"
|
||||
f"[contains(text(),'Active')]", driver)
|
||||
assert True
|
||||
|
||||
|
||||
def test_create_instance_from_volume_demo(login, driver, instance_name,
|
||||
volume_name, new_volume_demo,
|
||||
clear_instance_demo, config):
|
||||
network = config.network.external_network
|
||||
flavor = config.launch_instances.flavor
|
||||
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'instances',
|
||||
))
|
||||
driver.get(url)
|
||||
driver.find_element_by_link_text("Launch Instance").click()
|
||||
wizard = driver.find_element_by_css_selector("wizard")
|
||||
navigation = wizard.find_element_by_css_selector("div.wizard-nav")
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
"//*[@id='name']", wizard).send_keys(instance_name)
|
||||
navigation.find_element_by_link_text("Networks").click()
|
||||
network_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceNetworkForm]"
|
||||
)
|
||||
select_from_transfer_table(network_table, network)
|
||||
navigation.find_element_by_link_text("Flavor").click()
|
||||
flavor_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceFlavorForm]"
|
||||
)
|
||||
select_from_transfer_table(flavor_table, flavor)
|
||||
navigation.find_element_by_link_text("Source").click()
|
||||
source_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceSourceForm]"
|
||||
)
|
||||
select_boot_sources_type_tab = source_table.find_element_by_xpath(
|
||||
"//*[@id='boot-source-type']")
|
||||
select_boot_sources_type_tab.click()
|
||||
select_boot_sources_type_tab.find_element_by_xpath(
|
||||
"//option[@value='volume']").click()
|
||||
delete_volume_on_instance_delete(source_table, "No")
|
||||
select_from_transfer_table(source_table, volume_name)
|
||||
wizard.find_element_by_css_selector("button.btn-primary.finish").click()
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[contains(text(),'{instance_name}')]//ancestor::tr/td\
|
||||
[contains(text(),'Active')]", driver)
|
||||
assert True
|
||||
|
||||
|
||||
def test_delete_instance_demo(login, driver, instance_name,
|
||||
new_instance_demo, config):
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'instances',
|
||||
))
|
||||
driver.get(url)
|
||||
rows = driver.find_elements_by_css_selector(
|
||||
f"table#instances tr[data-display='{instance_name}']"
|
||||
)
|
||||
assert len(rows) == 1
|
||||
actions_column = rows[0].find_element_by_css_selector("td.actions_column")
|
||||
widgets.select_from_dropdown(actions_column, " Delete Instance")
|
||||
widgets.confirm_modal(driver)
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert f"Info: Scheduled deletion of Instance: {instance_name}" in messages
|
||||
|
||||
|
||||
# Admin tests
|
||||
|
||||
|
||||
def test_create_instance_admin(login, driver, instance_name,
|
||||
clear_instance_admin, config):
|
||||
image = config.launch_instances.image_name
|
||||
network = config.network.external_network
|
||||
flavor = config.launch_instances.flavor
|
||||
|
||||
login('admin')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'instances',
|
||||
))
|
||||
driver.get(url)
|
||||
driver.find_element_by_link_text("Launch Instance").click()
|
||||
wizard = driver.find_element_by_css_selector("wizard")
|
||||
navigation = wizard.find_element_by_css_selector("div.wizard-nav")
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
"//*[@id='name']", wizard).send_keys(instance_name)
|
||||
navigation.find_element_by_link_text("Networks").click()
|
||||
network_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceNetworkForm]"
|
||||
)
|
||||
select_from_transfer_table(network_table, network)
|
||||
navigation.find_element_by_link_text("Flavor").click()
|
||||
flavor_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceFlavorForm]"
|
||||
)
|
||||
select_from_transfer_table(flavor_table, flavor)
|
||||
navigation.find_element_by_link_text("Source").click()
|
||||
source_table = wizard.find_element_by_css_selector(
|
||||
"ng-include[ng-form=launchInstanceSourceForm]"
|
||||
)
|
||||
# create_new_volume_during_create_instance(source_table, "No")
|
||||
delete_volume_on_instance_delete(source_table, "Yes")
|
||||
select_from_transfer_table(source_table, image)
|
||||
wizard.find_element_by_css_selector(
|
||||
"button.btn-primary.finish").click()
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[contains(text(),'{instance_name}')]//ancestor::tr/td\
|
||||
[contains(text(),'Active')]", driver)
|
||||
assert True
|
||||
|
||||
|
||||
def test_delete_instance_admin(login, driver, instance_name,
|
||||
new_instance_admin, config):
|
||||
login('admin')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'instances',
|
||||
))
|
||||
driver.get(url)
|
||||
rows = driver.find_elements_by_css_selector(
|
||||
f"table#instances tr[data-display='{instance_name}']"
|
||||
)
|
||||
assert len(rows) == 1
|
||||
actions_column = rows[0].find_element_by_css_selector("td.actions_column")
|
||||
widgets.select_from_dropdown(actions_column, " Delete Instance")
|
||||
widgets.confirm_modal(driver)
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert f"Info: Scheduled deletion of Instance: {instance_name}" in messages
|
@ -1,5 +1,3 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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
|
||||
@ -12,12 +10,16 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from horizon.test import helpers as test
|
||||
|
||||
def test_user_login(login, driver):
|
||||
login('user')
|
||||
user_dropdown_menu = driver.find_element_by_css_selector(
|
||||
'.nav.navbar-nav.navbar-right')
|
||||
assert user_dropdown_menu.is_displayed()
|
||||
|
||||
|
||||
class BrowserTests(test.SeleniumTestCase):
|
||||
def test_splash(self):
|
||||
self.selenium.get(self.live_server_url)
|
||||
button = self.selenium.find_element_by_id("loginBtn")
|
||||
# Ensure button has something; must be language independent.
|
||||
self.assertGreater(len(button.text), 0)
|
||||
def test_admin_login(login, driver):
|
||||
login('admin')
|
||||
user_dropdown_menu = driver.find_element_by_css_selector(
|
||||
'.nav.navbar-nav.navbar-right')
|
||||
assert user_dropdown_menu.is_displayed()
|
146
openstack_dashboard/test/selenium/integration/test_volumes.py
Normal file
146
openstack_dashboard/test/selenium/integration/test_volumes.py
Normal file
@ -0,0 +1,146 @@
|
||||
# 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 oslo_utils import uuidutils
|
||||
import pytest
|
||||
|
||||
from openstack_dashboard.test.selenium import widgets
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def volume_name():
|
||||
return 'horizon_volume_%s' % uuidutils.generate_uuid(dashed=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_volume_demo(volume_name, openstack_demo, config):
|
||||
|
||||
volume = openstack_demo.create_volume(
|
||||
name=volume_name,
|
||||
image=config.launch_instances.image_name,
|
||||
size=1,
|
||||
wait=True,
|
||||
)
|
||||
yield volume
|
||||
openstack_demo.delete_volume(volume_name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_volume_admin(volume_name, openstack_admin, config):
|
||||
|
||||
volume = openstack_admin.create_volume(
|
||||
name=volume_name,
|
||||
image=config.launch_instances.image_name,
|
||||
size=1,
|
||||
wait=True,
|
||||
)
|
||||
yield volume
|
||||
openstack_admin.delete_volume(volume_name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_volume_demo(volume_name, openstack_demo):
|
||||
yield None
|
||||
openstack_demo. delete_volume(
|
||||
volume_name,
|
||||
wait=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def clear_volume_admin(volume_name, openstack_admin):
|
||||
yield None
|
||||
openstack_admin. delete_volume(
|
||||
volume_name,
|
||||
wait=True,
|
||||
)
|
||||
|
||||
|
||||
def select_from_dropdown_volume_tab(driver, dropdown_id, label):
|
||||
volume_dropdown = driver.find_element_by_xpath(
|
||||
f"//*[@for='{dropdown_id}']/following-sibling::div")
|
||||
volume_dropdown.click()
|
||||
volume_dropdown_tab = volume_dropdown.find_element_by_css_selector(
|
||||
"ul.dropdown-menu")
|
||||
volume_dropdown_tab.find_element_by_xpath(f"//*[text()='{label}']").click()
|
||||
|
||||
|
||||
def test_create_empty_volume_demo(login, driver, volume_name,
|
||||
clear_volume_demo, config):
|
||||
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'volumes',
|
||||
))
|
||||
driver.get(url)
|
||||
driver.find_element_by_link_text("Create Volume").click()
|
||||
volume_form = driver.find_element_by_css_selector(".modal-dialog form")
|
||||
volume_form.find_element_by_xpath(
|
||||
"//*[@id='id_name']").send_keys(volume_name)
|
||||
volume_form.find_element_by_xpath(
|
||||
"//*[@class='btn btn-primary'][@value='Create Volume']").click()
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert f'Info: Creating volume "{volume_name}"' in messages
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[contains(text(),'{volume_name}')]//ancestor::tr/td"
|
||||
f"[contains(text(),'Available')]", driver)
|
||||
assert True
|
||||
|
||||
|
||||
def test_create_volume_from_image_demo(login, driver, volume_name,
|
||||
clear_volume_demo, config):
|
||||
image_source_name = "cirros-0.6.2-x86_64-disk (20.4 MB)"
|
||||
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'volumes',
|
||||
))
|
||||
driver.get(url)
|
||||
driver.find_element_by_link_text("Create Volume").click()
|
||||
volume_form = driver.find_element_by_css_selector(".modal-dialog form")
|
||||
volume_form.find_element_by_xpath(
|
||||
"//*[@id='id_name']").send_keys(volume_name)
|
||||
select_from_dropdown_volume_tab(
|
||||
volume_form, 'id_volume_source_type', 'Image')
|
||||
select_from_dropdown_volume_tab(
|
||||
volume_form, 'id_image_source', image_source_name)
|
||||
volume_form.find_element_by_xpath(
|
||||
"//*[@class='btn btn-primary'][@value='Create Volume']").click()
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert f'Info: Creating volume "{volume_name}"' in messages
|
||||
widgets.find_already_visible_element_by_xpath(
|
||||
f"//*[contains(text(),'{volume_name}')]//ancestor::tr/td"
|
||||
f"[contains(text(),'Available')]", driver)
|
||||
assert True
|
||||
|
||||
|
||||
def test_delete_volume_demo(login, driver, volume_name,
|
||||
new_volume_demo, config):
|
||||
login('user')
|
||||
url = '/'.join((
|
||||
config.dashboard.dashboard_url,
|
||||
'project',
|
||||
'volumes',
|
||||
))
|
||||
driver.get(url)
|
||||
rows = driver.find_elements_by_css_selector(
|
||||
f"table#volumes tr[data-display='{volume_name}']"
|
||||
)
|
||||
assert len(rows) == 1
|
||||
actions_column = rows[0].find_element_by_css_selector("td.actions_column")
|
||||
widgets.select_from_dropdown(actions_column, " Delete Volume")
|
||||
widgets.confirm_modal(driver)
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert f"Info: Scheduled deletion of Volume: {volume_name}" in messages
|
90
openstack_dashboard/test/selenium/ui/conftest.py
Normal file
90
openstack_dashboard/test/selenium/ui/conftest.py
Normal file
@ -0,0 +1,90 @@
|
||||
# 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 unittest import mock
|
||||
import warnings
|
||||
|
||||
from django.test import testcases
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from keystoneauth1.identity import v3 as v3_auth
|
||||
from keystoneclient.v3 import client as client_v3
|
||||
from openstack_auth.tests import data_v3
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
yield
|
||||
warnings.simplefilter("default")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def auth_data():
|
||||
return data_v3.generate_test_data()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_requests(monkeypatch):
|
||||
class MockRequestsSession:
|
||||
adapters = []
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
raise RuntimeError("External request attempted, missed a mock?")
|
||||
|
||||
monkeypatch.setattr(requests, 'Session', MockRequestsSession)
|
||||
# enable request logging
|
||||
monkeypatch.setattr(testcases, 'QuietWSGIRequestHandler',
|
||||
testcases.WSGIRequestHandler)
|
||||
|
||||
# prevent pytest-django errors due to no database
|
||||
@pytest.fixture()
|
||||
def _django_db_helper():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_openstack_auth(settings, auth_data):
|
||||
with mock.patch.object(client_v3, 'Client') as mock_client, \
|
||||
mock.patch.object(v3_auth, 'Token') as mock_token, \
|
||||
mock.patch.object(v3_auth, 'Password') as mock_password:
|
||||
|
||||
keystone_url = settings.OPENSTACK_KEYSTONE_URL
|
||||
auth_password = mock.Mock(auth_url=keystone_url)
|
||||
mock_password.return_value = auth_password
|
||||
auth_password.get_access.return_value = auth_data.unscoped_access_info
|
||||
auth_token_unscoped = mock.Mock(auth_url=keystone_url)
|
||||
auth_token_scoped = mock.Mock(auth_url=keystone_url)
|
||||
mock_token.return_value = auth_token_scoped
|
||||
auth_token_unscoped.get_access.return_value = (
|
||||
auth_data.federated_unscoped_access_info
|
||||
)
|
||||
auth_token_scoped.get_access.return_value = (
|
||||
auth_data.unscoped_access_info
|
||||
)
|
||||
client_unscoped = mock.Mock()
|
||||
mock_client.return_value = client_unscoped
|
||||
projects = [auth_data.project_one, auth_data.project_two]
|
||||
client_unscoped.projects.list.return_value = projects
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_keystoneclient():
|
||||
with mock.patch.object(api.keystone, 'keystoneclient') as mock_client:
|
||||
keystoneclient = mock_client.return_value
|
||||
endpoint_data = mock.Mock()
|
||||
endpoint_data.api_version = (3, 10)
|
||||
keystoneclient.session.get_endpoint_data.return_value = endpoint_data
|
||||
yield
|
43
openstack_dashboard/test/selenium/ui/test_settings.py
Normal file
43
openstack_dashboard/test/selenium/ui/test_settings.py
Normal file
@ -0,0 +1,43 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from openstack_dashboard.test.selenium import widgets
|
||||
|
||||
|
||||
def test_login(live_server, driver, mock_openstack_auth, mock_keystoneclient):
|
||||
# We go to a page that doesn't do more api calls.
|
||||
driver.get(live_server.url + '/settings')
|
||||
assert driver.title == "Login - OpenStack Dashboard"
|
||||
user_field = driver.find_element_by_id('id_username')
|
||||
user_field.clear()
|
||||
user_field.send_keys("user")
|
||||
pass_field = driver.find_element_by_id('id_password')
|
||||
pass_field.clear()
|
||||
pass_field.send_keys("password")
|
||||
button = driver.find_element_by_css_selector('div.panel-footer button.btn')
|
||||
button.click()
|
||||
errors = [m.text for m in
|
||||
driver.find_elements_by_css_selector('div.alert-danger p')]
|
||||
assert errors == []
|
||||
assert driver.title != "Login - OpenStack Dashboard"
|
||||
|
||||
|
||||
def test_languages(live_server, driver, mock_openstack_auth,
|
||||
mock_keystoneclient):
|
||||
user_settings = driver.find_element_by_id('user_settings_modal')
|
||||
language_options = user_settings.find_element_by_id('id_language')
|
||||
language_options.click()
|
||||
language_options.find_element_by_xpath("//option[@value='de']").click()
|
||||
user_settings.find_element_by_xpath('//*[@class="btn btn-primary"]').click()
|
||||
messages = widgets.get_and_dismiss_messages(driver)
|
||||
assert "Success: Settings saved." in messages
|
||||
assert "Error" not in messages
|
||||
# ToDo - mock API switch page language.
|
49
openstack_dashboard/test/selenium/widgets.py
Normal file
49
openstack_dashboard/test/selenium/widgets.py
Normal file
@ -0,0 +1,49 @@
|
||||
# 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.by import By
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.wait import WebDriverWait
|
||||
|
||||
|
||||
def get_and_dismiss_messages(element):
|
||||
messages = element.find_elements_by_css_selector("div.messages div.alert")
|
||||
collect = []
|
||||
for message in messages:
|
||||
text = message.find_element_by_css_selector("p").text
|
||||
message.find_element_by_css_selector("a.close").click()
|
||||
collect.append(text)
|
||||
return collect
|
||||
|
||||
|
||||
def find_already_visible_element_by_xpath(element, driver):
|
||||
return WebDriverWait(driver, 160).until(
|
||||
EC.visibility_of_element_located((By.XPATH, element)))
|
||||
|
||||
|
||||
def select_from_dropdown(element, label):
|
||||
menu_button = element.find_element_by_css_selector(
|
||||
"a[data-toggle='dropdown']"
|
||||
)
|
||||
menu_button.click()
|
||||
options = element.find_element_by_css_selector("ul.dropdown-menu")
|
||||
selection = options.find_element_by_xpath(
|
||||
f"li/button[text()[contains(.,'{label}')]]"
|
||||
)
|
||||
selection.click()
|
||||
|
||||
|
||||
def confirm_modal(element):
|
||||
confirm = element.find_element_by_css_selector(
|
||||
"#modal_wrapper a.btn-danger"
|
||||
)
|
||||
confirm.click()
|
@ -3,5 +3,6 @@ ROOT=$1
|
||||
report_args="--junitxml=$ROOT/test_reports/selenium_test_results.xml"
|
||||
report_args+=" --html=$ROOT/test_reports/selenium_test_results.html"
|
||||
report_args+=" --self-contained-html"
|
||||
ignore="--ignore=$ROOT/openstack_dashboard/test/selenium"
|
||||
|
||||
pytest $ROOT/openstack_dashboard/ --ds=openstack_dashboard.test.settings -v -m selenium $report_args
|
||||
pytest $ROOT/openstack_dashboard/ --ds=openstack_dashboard.test.settings $ignore -v -m selenium $report_args
|
||||
|
@ -29,6 +29,7 @@ function run_test {
|
||||
local target
|
||||
local settings_module
|
||||
local report_args
|
||||
local ignore
|
||||
|
||||
tag="not selenium and not integration and not plugin_test"
|
||||
|
||||
@ -61,14 +62,16 @@ function run_test {
|
||||
fi
|
||||
fi
|
||||
|
||||
ignore="--ignore=$root/openstack_dashboard/test/selenium"
|
||||
|
||||
if [ "$coverage" -eq 1 ]; then
|
||||
coverage run -m pytest $target --ds=$settings_module -m "$tag"
|
||||
coverage run -m pytest $target $ignore --ds=$settings_module -m "$tag"
|
||||
else
|
||||
report_args="--junitxml=$report_dir/${project}_test_results.xml"
|
||||
report_args+=" --html=$report_dir/${project}_test_results.html"
|
||||
report_args+=" --self-contained-html"
|
||||
|
||||
pytest $target --ds=$settings_module -v -m "$tag" $report_args
|
||||
pytest $target --ds=$settings_module -v -m "$tag" $ignore $report_args
|
||||
fi
|
||||
return $?
|
||||
}
|
||||
|
24
tox.ini
24
tox.ini
@ -107,6 +107,30 @@ 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]
|
||||
envdir = {toxworkdir}/venv
|
||||
# Run pytest integration tests only
|
||||
passenv =
|
||||
DISPLAY
|
||||
FFMPEG_INSTALLED
|
||||
setenv =
|
||||
SELENIUM_HEADLESS=1
|
||||
commands =
|
||||
oslo-config-generator --namespace openstack_dashboard_integration_tests
|
||||
pytest -v --junitxml="{toxinidir}/test_reports/integration_pytest_results.xml" --html="{toxinidir}/test_reports/integration_pytest_results.html" --self-contained-html {posargs:{toxinidir}/openstack_dashboard/test/selenium/integration}
|
||||
|
||||
[testenv:ui-pytest]
|
||||
envdir = {toxworkdir}/venv
|
||||
# Run pytest ui tests only
|
||||
passenv =
|
||||
DISPLAY
|
||||
FFMPEG_INSTALLED
|
||||
setenv =
|
||||
SELENIUM_HEADLESS=1
|
||||
commands =
|
||||
oslo-config-generator --namespace openstack_dashboard_integration_tests
|
||||
pytest --ds=openstack_dashboard.settings -v --junitxml="{toxinidir}/test_reports/integration_uitest_results.xml" --html="{toxinidir}/test_reports/integration_uitest_results.html" --self-contained-html {posargs:{toxinidir}/openstack_dashboard/test/selenium/ui}
|
||||
|
||||
[testenv:npm]
|
||||
passenv =
|
||||
HOME
|
||||
|
Loading…
Reference in New Issue
Block a user