Merge "Add pytest-based selenium tests"

This commit is contained in:
Zuul 2023-08-14 10:10:13 +00:00 committed by Gerrit Code Review
commit 638d411637
14 changed files with 945 additions and 12 deletions

View File

@ -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'),

View File

@ -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/

View 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()

View 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()

View 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

View File

@ -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()

View 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

View 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

View File

@ -0,0 +1,43 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from openstack_dashboard.test.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.

View 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()

View File

@ -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

View File

@ -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
View File

@ -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