Selenium infra
Add Selenium to the tempest plugin Change-Id: I53207b321e8dabda1e47c4d9f01cdd6e1682bb69
This commit is contained in:
parent
a0fc5efc88
commit
3048d7eed3
50
README.rst
50
README.rst
@ -18,6 +18,21 @@ Using
|
|||||||
|
|
||||||
Install this plugin in the same python environment as tempest.
|
Install this plugin in the same python environment as tempest.
|
||||||
|
|
||||||
|
Installing the web driver
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Selenium drives the browser by using a web driver. Currently Firefox and Chrome are supported.
|
||||||
|
|
||||||
|
#. To install the Firefox driver:
|
||||||
|
Download the latest driver from https://github.com/mozilla/geckodriver/releases/. Place the
|
||||||
|
executable somewhere in your ``$PATH``, and configure your tempest.conf with the full path to
|
||||||
|
it. See an example tempest.conf file below.
|
||||||
|
|
||||||
|
#. To install the Chrome driver:
|
||||||
|
Download the latest driver from https://sites.google.com/a/chromium.org/chromedriver/downloads.
|
||||||
|
Unzip the downloaded file you'll get an executable called "chromedriver". Place the executable
|
||||||
|
somewhere in your ``$PATH``.
|
||||||
|
|
||||||
Configuration for testing
|
Configuration for testing
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
@ -35,3 +50,38 @@ On your undercloud:
|
|||||||
|
|
||||||
This will run all of the tests contained in the tempest-tripleo-ui plugin
|
This will run all of the tests contained in the tempest-tripleo-ui plugin
|
||||||
against your undercloud.
|
against your undercloud.
|
||||||
|
|
||||||
|
Sample tempest.conf
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
For the UI tests to work, a minimal tempest.conf should include:
|
||||||
|
1) The credentials to log in (same credentials which are used on the command line)
|
||||||
|
2) The URL where the login screen to the UI can be found
|
||||||
|
3) The webdriver to use, which could be one of: "Chrome" or "Firefox"
|
||||||
|
4) If using Firefox, set marionette_binary to point to the path to the driver
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
log_dir = /home/tester/src/tempest/cloud-01/logs
|
||||||
|
log_file = tempest.log
|
||||||
|
|
||||||
|
[oslo_concurrency]
|
||||||
|
lock_path = /home/tester/src/tempest/cloud-01/tempest_lock
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
admin_username = admin
|
||||||
|
admin_password = password
|
||||||
|
admin_project_name = admin
|
||||||
|
admin_domain_name = default
|
||||||
|
|
||||||
|
[identity]
|
||||||
|
auth_version = v3
|
||||||
|
uri_v3 = https://server:443/keystone/v3
|
||||||
|
|
||||||
|
[tripleo_ui]
|
||||||
|
webdriver = "Chrome"
|
||||||
|
marionette_binary = "/home/tester/bin/wires"
|
||||||
|
url = "https://server"
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level=DEBUG
|
||||||
|
handlers=file
|
||||||
|
@ -2,3 +2,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
|||||||
oslo.config>=5.1.0 # Apache-2.0
|
oslo.config>=5.1.0 # Apache-2.0
|
||||||
six>=1.10.0 # MIT
|
six>=1.10.0 # MIT
|
||||||
tempest>=17.1.0 # Apache-2.0
|
tempest>=17.1.0 # Apache-2.0
|
||||||
|
selenium>=3.14.1 # Apache-2.0
|
||||||
|
68
tempest_tripleo_ui/alerts.py
Normal file
68
tempest_tripleo_ui/alerts.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from selenium.common import exceptions
|
||||||
|
from tempest_tripleo_ui.core import SeleniumElement
|
||||||
|
from tempest_tripleo_ui.timer import Timer
|
||||||
|
from time import sleep
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
alert_success_close_button = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "alert-success")]/button[@class="close"]')
|
||||||
|
alert_success = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "alert-success")]')
|
||||||
|
alert_danger_close_button = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "alert-danger")]/button[@class="close"]')
|
||||||
|
alert_danger = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "alert-danger")]')
|
||||||
|
|
||||||
|
# the spinner that we see while waiting for dialogs to open etc'
|
||||||
|
spinner = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "spinner")]')
|
||||||
|
|
||||||
|
|
||||||
|
def clear_success_message(timeout=3):
|
||||||
|
if alert_success_close_button.wait_for_element(timeout) and \
|
||||||
|
not alert_success_close_button.is_stale():
|
||||||
|
try:
|
||||||
|
logger.info("Clearing message: {}".format(
|
||||||
|
alert_success.get_all_text()))
|
||||||
|
alert_success_close_button.click()
|
||||||
|
except exceptions.WebDriverException:
|
||||||
|
pass # sometimes the message disappears in this critical moment
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def clear_danger_message(timeout=3):
|
||||||
|
if alert_danger_close_button.wait_for_element(timeout) and \
|
||||||
|
not alert_danger_close_button.is_stale():
|
||||||
|
logger.error("Clearing alert: {}".format(
|
||||||
|
alert_danger.get_all_text()))
|
||||||
|
alert_danger_close_button.click()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def clear_all_success_messages():
|
||||||
|
while clear_success_message():
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_all_danger_messages():
|
||||||
|
while clear_danger_message():
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_spinner(timeout=60, spinner_element=None):
|
||||||
|
logger.debug("waiting for spinners...")
|
||||||
|
if not spinner_element:
|
||||||
|
spinner_element = spinner
|
||||||
|
timer = Timer("spinner")
|
||||||
|
while spinner_element.wait_for_element(0) and \
|
||||||
|
timer.get_duration() < timeout:
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
# not using timer.assert_timer() because we're not interested
|
||||||
|
# in the error message it generates
|
||||||
|
return timer.get_duration() < timeout
|
36
tempest_tripleo_ui/base.py
Normal file
36
tempest_tripleo_ui/base.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import logging
|
||||||
|
from tempest.lib import exceptions
|
||||||
|
from tempest import test
|
||||||
|
from tempest_tripleo_ui import browser
|
||||||
|
from tempest_tripleo_ui.models import login
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CouldNotLogIn(exceptions.TempestException):
|
||||||
|
message = "Could not log in"
|
||||||
|
|
||||||
|
|
||||||
|
class GUITestCase(test.BaseTestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resource_setup(cls):
|
||||||
|
"""Login to the GUI. This will also open up the browser and get the
|
||||||
|
home page - according to the parameters configured in the [UI]
|
||||||
|
section in the conf file.
|
||||||
|
"""
|
||||||
|
super(GUITestCase, cls).resource_setup()
|
||||||
|
if not login.login():
|
||||||
|
logger.error(
|
||||||
|
"Unable to login to TripleO UI (check the "
|
||||||
|
"credentials in the tempest conf file)")
|
||||||
|
browser.quit_browser()
|
||||||
|
raise CouldNotLogIn
|
||||||
|
|
||||||
|
cls.addClassResourceCleanup(cls.logout_and_close_browser)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def logout_and_close_browser(cls):
|
||||||
|
if login.is_logged_in():
|
||||||
|
login.logout()
|
||||||
|
browser.quit_browser()
|
111
tempest_tripleo_ui/browser.py
Normal file
111
tempest_tripleo_ui/browser.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import logging
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||||
|
from tempest import config
|
||||||
|
from tempest.lib import exceptions
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidWebdriverSetting(exceptions.TempestException):
|
||||||
|
message = "Invalid webdriver: %(browser_driver)s. Should be either "\
|
||||||
|
"'Chrome' or 'Firefox'"
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
CONF = config.CONF
|
||||||
|
browser = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_browser():
|
||||||
|
global browser
|
||||||
|
if browser is not None:
|
||||||
|
return browser
|
||||||
|
|
||||||
|
browser_driver = CONF.tripleo_ui.webdriver
|
||||||
|
if browser_driver.lower() == "chrome":
|
||||||
|
browser_factory = ChromeFactory()
|
||||||
|
elif browser_driver.lower() == "firefox":
|
||||||
|
browser_factory = MarionetteFactory(
|
||||||
|
CONF.tripleo_ui.marionette_binary)
|
||||||
|
else:
|
||||||
|
raise InvalidWebdriverSetting(browser_driver=browser_driver)
|
||||||
|
|
||||||
|
browser = Browser(browser_factory, CONF.tripleo_ui.url)
|
||||||
|
return browser
|
||||||
|
|
||||||
|
|
||||||
|
def quit_browser():
|
||||||
|
global browser
|
||||||
|
if browser is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
browser.quit_browser()
|
||||||
|
browser = None
|
||||||
|
|
||||||
|
|
||||||
|
def save_screenshot(filename=None):
|
||||||
|
global browser
|
||||||
|
if browser is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
filename = "/tmp/{}.png".format(uuid.uuid4())
|
||||||
|
browser.get_driver().save_screenshot(filename)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def execute_script(script_to_run):
|
||||||
|
return get_browser().execute_script(script_to_run)
|
||||||
|
|
||||||
|
|
||||||
|
class Browser(object):
|
||||||
|
|
||||||
|
def __init__(self, browser_factory, homepage=None):
|
||||||
|
self.session = None
|
||||||
|
self.driver = browser_factory.create()
|
||||||
|
if homepage and len(homepage.strip()):
|
||||||
|
self.get(homepage)
|
||||||
|
|
||||||
|
def get_session(self):
|
||||||
|
if self.session:
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
self.session = uuid.uuid4()
|
||||||
|
return self.session
|
||||||
|
|
||||||
|
def get(self, url):
|
||||||
|
self.driver.get(url)
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self.driver.refresh()
|
||||||
|
|
||||||
|
def get_driver(self):
|
||||||
|
return self.driver
|
||||||
|
|
||||||
|
def quit_browser(self):
|
||||||
|
self.driver.quit()
|
||||||
|
self.driver = None
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def execute_script(self, script_to_run):
|
||||||
|
return self.driver.execute_script(script_to_run)
|
||||||
|
|
||||||
|
|
||||||
|
class ChromeFactory(object):
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
return webdriver.Chrome()
|
||||||
|
|
||||||
|
|
||||||
|
class MarionetteFactory(object):
|
||||||
|
|
||||||
|
def __init__(self, path_to_binary):
|
||||||
|
self.path_to_binary = path_to_binary
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
capabilities = DesiredCapabilities.FIREFOX
|
||||||
|
capabilities['marionette'] = True
|
||||||
|
args = dict()
|
||||||
|
args['capabilities'] = capabilities
|
||||||
|
args['executable_path'] = self.path_to_binary
|
||||||
|
return webdriver.Firefox(**args)
|
@ -21,5 +21,9 @@ ui_group = cfg.OptGroup(name='tripleo_ui', title='TripleO UI Options')
|
|||||||
|
|
||||||
UIGroup = [
|
UIGroup = [
|
||||||
cfg.StrOpt('url', default='http://localhost',
|
cfg.StrOpt('url', default='http://localhost',
|
||||||
help='where the TripleO UI can be found')
|
help='where the TripleO UI can be found'),
|
||||||
|
cfg.StrOpt('webdriver', default='Chrome',
|
||||||
|
help='the browser to use for the test [Chrome/Marionette]'),
|
||||||
|
cfg.StrOpt('marionette_binary', default='path_to_wires',
|
||||||
|
help='path to the marionette driver binary')
|
||||||
]
|
]
|
||||||
|
395
tempest_tripleo_ui/core.py
Normal file
395
tempest_tripleo_ui/core.py
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
import logging
|
||||||
|
from selenium.common import exceptions
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.webdriver.support import select
|
||||||
|
from selenium.webdriver.support import ui
|
||||||
|
from tempest_tripleo_ui import browser
|
||||||
|
from tempest_tripleo_ui.decorators import selenium_action
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Identifier(object):
|
||||||
|
def __init__(self, by, identifier):
|
||||||
|
self.by = by
|
||||||
|
self.identifier = identifier
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def xpath(cls, locator):
|
||||||
|
return Identifier(By.XPATH, locator)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def css_selector(cls, locator):
|
||||||
|
return Identifier(By.CSS_SELECTOR, locator)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def id(cls, locator):
|
||||||
|
return Identifier(By.ID, locator)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def class_name(cls, locator):
|
||||||
|
return Identifier(By.CLASS_NAME, locator)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def link_text(cls, locator):
|
||||||
|
return Identifier(By.LINK_TEXT, locator)
|
||||||
|
|
||||||
|
|
||||||
|
class SeleniumElement(object):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
identifier=None,
|
||||||
|
webelement=None,
|
||||||
|
session=None):
|
||||||
|
self.identifier = identifier
|
||||||
|
self.webelement = webelement
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_id(cls, name):
|
||||||
|
return SeleniumElement(Identifier(By.ID, name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_class(cls, name):
|
||||||
|
return SeleniumElement(Identifier(By.CLASS_NAME, name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_css(cls, name):
|
||||||
|
return SeleniumElement(Identifier(By.CSS_SELECTOR, name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_xpath(cls, name):
|
||||||
|
return SeleniumElement(Identifier(By.XPATH, name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_link_text(cls, name):
|
||||||
|
return SeleniumElement(Identifier(By.LINK_TEXT, name))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def by_element(cls, elem, browser_session):
|
||||||
|
new_elem = SeleniumElement(webelement=elem, session=browser_session)
|
||||||
|
return new_elem
|
||||||
|
|
||||||
|
def get_element(self):
|
||||||
|
if self.webelement and \
|
||||||
|
self.session == browser.get_browser().get_session():
|
||||||
|
return self.webelement
|
||||||
|
|
||||||
|
elem = self.wait_for_element()
|
||||||
|
if elem is None:
|
||||||
|
logger.error("can't find element: {}".format(
|
||||||
|
self.get_identifier_string()))
|
||||||
|
|
||||||
|
return elem
|
||||||
|
|
||||||
|
def get_identifier_string(self):
|
||||||
|
name = "No identifier"
|
||||||
|
if self.identifier:
|
||||||
|
name = self.identifier.identifier
|
||||||
|
return name
|
||||||
|
|
||||||
|
def wait_for_element(self, timeout_in_seconds=10):
|
||||||
|
if self.identifier is None:
|
||||||
|
logger.warning("wait_for_element() is irrelevant for elements "
|
||||||
|
"that were not found via an identifier")
|
||||||
|
return self.webelement
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.webelement = ui.WebDriverWait(
|
||||||
|
browser.get_browser().get_driver(),
|
||||||
|
timeout_in_seconds).until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(self.identifier.by, self.identifier.identifier))
|
||||||
|
)
|
||||||
|
self.session = browser.get_browser().get_session()
|
||||||
|
return self.webelement
|
||||||
|
except exceptions.TimeoutException:
|
||||||
|
return None # because the element is not found
|
||||||
|
|
||||||
|
def wait_for_present_text(self, text, timeout_in_seconds=10):
|
||||||
|
try:
|
||||||
|
if self.identifier is None:
|
||||||
|
logger.warning(
|
||||||
|
'wait_for_present_text will not work properly, '
|
||||||
|
'because there is missing .identifier.')
|
||||||
|
self.webelement = ui.WebDriverWait(
|
||||||
|
browser.get_browser().get_driver(),
|
||||||
|
timeout_in_seconds).until(
|
||||||
|
EC.text_to_be_present_in_element(
|
||||||
|
(self.identifier.by, self.identifier.identifier), text)
|
||||||
|
)
|
||||||
|
return self.webelement
|
||||||
|
except exceptions.TimeoutException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def wait_for_staleness(self, timeout_in_seconds=10):
|
||||||
|
if self.webelement is None:
|
||||||
|
logger.warning("wait_for_staleness() is irrelevant for elements "
|
||||||
|
"that were not found yet")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ui.WebDriverWait(browser.get_browser().get_driver(),
|
||||||
|
timeout_in_seconds).until(
|
||||||
|
EC.staleness_of(self.webelement))
|
||||||
|
except exceptions.TimeoutException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_stale(self):
|
||||||
|
try:
|
||||||
|
self.webelement.is_enabled()
|
||||||
|
return False
|
||||||
|
except exceptions.StaleElementReferenceException:
|
||||||
|
return True
|
||||||
|
except exceptions.WebDriverException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def is_visible(self, webelement):
|
||||||
|
return webelement.is_displayed()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def is_selected(self, webelement):
|
||||||
|
return webelement.is_selected()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def is_enabled(self, webelement):
|
||||||
|
return webelement.is_enabled()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_tag_name(self, webelement):
|
||||||
|
return webelement.tag_name
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_text(self, webelement):
|
||||||
|
return webelement.text
|
||||||
|
|
||||||
|
def get_all_text(self):
|
||||||
|
text = self.get_text()
|
||||||
|
if text is None or len(text) == 0:
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
child_elements = self.find_child_elements(Identifier.xpath("*"))
|
||||||
|
if child_elements is not None and len(child_elements) > 0:
|
||||||
|
for child_element in child_elements:
|
||||||
|
child_text = child_element.get_all_text()
|
||||||
|
if child_text is not None and len(child_text) > 0:
|
||||||
|
if len(text) > 0:
|
||||||
|
text = text + " " + child_text
|
||||||
|
else:
|
||||||
|
text = child_text
|
||||||
|
return text
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_id(self, webelement):
|
||||||
|
return webelement.get_attribute("id")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_name(self, webelement):
|
||||||
|
return webelement.get_attribute("name")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_value(self, webelement):
|
||||||
|
return webelement.get_attribute("value")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_class(self, webelement):
|
||||||
|
return webelement.get_attribute("class")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_href(self, webelement):
|
||||||
|
return webelement.get_attribute("href")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def get_title(self, webelement):
|
||||||
|
return webelement.get_attribute("title")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def click_minimum_retries(self, webelement):
|
||||||
|
webelement.click()
|
||||||
|
|
||||||
|
# this WON'T be a @selenium_action because we're handling retries
|
||||||
|
# internally
|
||||||
|
def click(self, use_js=False):
|
||||||
|
|
||||||
|
# try to click and catch exception
|
||||||
|
try:
|
||||||
|
self.click_minimum_retries()
|
||||||
|
return True
|
||||||
|
except exceptions.WebDriverException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
# click failed - scroll the item into view (bottom) and try again
|
||||||
|
try:
|
||||||
|
self.scroll_into_view(False)
|
||||||
|
self.click_minimum_retries()
|
||||||
|
return True
|
||||||
|
except exceptions.WebDriverException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
# click failed again - scroll the item into view (top) and try again
|
||||||
|
try:
|
||||||
|
self.scroll_into_view(True)
|
||||||
|
self.click_minimum_retries()
|
||||||
|
return True
|
||||||
|
except exceptions.WebDriverException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if use_js:
|
||||||
|
try:
|
||||||
|
logger.warning("almost giving up on all click attempts !!")
|
||||||
|
browser.get_browser().save_screenshot()
|
||||||
|
|
||||||
|
browser.get_browser().get_driver().execute_script(
|
||||||
|
'return arguments[0].click();', self.get_element()
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"click was successful only when using "
|
||||||
|
"javascript... {}".format(self.get_identifier_string()))
|
||||||
|
sleep(1)
|
||||||
|
browser.get_browser().save_screenshot()
|
||||||
|
return True
|
||||||
|
except BaseException:
|
||||||
|
logger.warning(
|
||||||
|
"click was unsuccessful even with javascript! {}".format(
|
||||||
|
self.get_identifier_string()))
|
||||||
|
|
||||||
|
# all failed...
|
||||||
|
logger.error(
|
||||||
|
"all click attempts failed on: {}".format(
|
||||||
|
self.get_identifier_string()))
|
||||||
|
return False
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def click_at_offset(self, webelement, x, y):
|
||||||
|
actions = webdriver.ActionChains(browser.get_browser().get_driver())
|
||||||
|
actions.move_to_element_with_offset(webelement, x, y)
|
||||||
|
actions.click()
|
||||||
|
return actions.perform()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def move_to(self, webelement):
|
||||||
|
actions = webdriver.ActionChains(browser.get_browser().get_driver())
|
||||||
|
actions.move_to_element(webelement)
|
||||||
|
actions.perform()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def scroll_into_view(self, webelement, align_to_top=True):
|
||||||
|
driver = browser.get_browser().get_driver()
|
||||||
|
if align_to_top:
|
||||||
|
driver.execute_script(
|
||||||
|
"arguments[0].scroll_into_view(true);", webelement)
|
||||||
|
else:
|
||||||
|
driver.execute_script(
|
||||||
|
"arguments[0].scroll_into_view(false);", webelement)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def submit(self, webelement):
|
||||||
|
return webelement.submit()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def clear(self, webelement):
|
||||||
|
webelement.clear()
|
||||||
|
return webelement.get_attribute('value') == ''
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def send_keys(self, webelement, text, tokenize=True):
|
||||||
|
text = "{}".format(text)
|
||||||
|
if not tokenize:
|
||||||
|
webelement.send_keys(text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# don't send more than 40 characters at a time, as it
|
||||||
|
# can hang some scripts
|
||||||
|
token_size = 40
|
||||||
|
while len(text) > 0:
|
||||||
|
webelement.send_keys(text[:token_size])
|
||||||
|
text = text[token_size:]
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def send_ctrl_something(self, webelement, key):
|
||||||
|
return webelement.send_keys(Keys.CONTROL, self.key)
|
||||||
|
|
||||||
|
# start xpath with ".//"
|
||||||
|
@selenium_action
|
||||||
|
def find_child_elements(self, webelement, identifier):
|
||||||
|
elements = None
|
||||||
|
try:
|
||||||
|
elements = webelement.find_elements(
|
||||||
|
by=self.identifier.by,
|
||||||
|
value=self.identifier.identifier)
|
||||||
|
except BaseException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if elements is not None:
|
||||||
|
ret = []
|
||||||
|
for child_element in elements:
|
||||||
|
ret.append(SeleniumElement.by_element(child_element,
|
||||||
|
self.session))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def drag_and_drop(self, target):
|
||||||
|
if self.is_visible() and target.is_visible():
|
||||||
|
elem_from = self.get_element()
|
||||||
|
elem_to = target.get_element()
|
||||||
|
if elem_from and elem_to:
|
||||||
|
webdriver.ActionChains(
|
||||||
|
browser.get_browser().get_driver().drag_and_drop(
|
||||||
|
elem_from, elem_to).perform())
|
||||||
|
else:
|
||||||
|
logger.error("drag-n-drop: items are not visible")
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def deselect_all(self, webelement):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.deselect_all()
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def select_by_index(self, webelement, index):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.select_by_index(index)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def select_by_value(self, webelement, value):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.select_by_value(value)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def select_by_visible_text(self, webelement, value):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.select_by_visible_text(value)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def deselect_by_index(self, webelement, index):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.deselect_by_index(index)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def deselect_by_value(self, webelement, value):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.deselect_by_value(value)
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def deselect_by_visible_text(self, webelement, value):
|
||||||
|
sel = select.Select(webelement)
|
||||||
|
return sel.deselect_by_visible_text(value)
|
||||||
|
|
||||||
|
|
||||||
|
def selenium_elements(identifier):
|
||||||
|
elements = []
|
||||||
|
current_browser = browser.get_browser()
|
||||||
|
browser_session = current_browser.get_session()
|
||||||
|
results = current_browser.get_driver().find_elements(
|
||||||
|
by=identifier.by, value=identifier.identifier)
|
||||||
|
for elem in results:
|
||||||
|
elements.append(SeleniumElement.by_element(elem, browser_session))
|
||||||
|
return elements
|
51
tempest_tripleo_ui/decorators.py
Normal file
51
tempest_tripleo_ui/decorators.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from selenium.common.exceptions import (
|
||||||
|
StaleElementReferenceException, InvalidElementStateException
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def selenium_action(f):
|
||||||
|
"""The selenium_action decorator
|
||||||
|
|
||||||
|
This decorator can be used to wrap methods of ``SeleniumElement`` to
|
||||||
|
provide exception handling, and automatic retries.
|
||||||
|
|
||||||
|
It expects that the method being decorated accepts a ``webelement``
|
||||||
|
as its first positional argument after ``self``.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
|
||||||
|
@selenium_action
|
||||||
|
def click_button(self, webelement):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(self, *args, **kwargs):
|
||||||
|
webelement = self.get_element()
|
||||||
|
|
||||||
|
if not webelement:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return f(self, webelement, *args, **kwargs)
|
||||||
|
except (StaleElementReferenceException,
|
||||||
|
InvalidElementStateException):
|
||||||
|
logger.debug("stale element: {}".format(
|
||||||
|
self.get_identifier_string()))
|
||||||
|
self.wait_for_element(3)
|
||||||
|
webelement = self.get_element()
|
||||||
|
if webelement is None:
|
||||||
|
return None
|
||||||
|
return f(self, webelement, *args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("selenium exception on '{}': {}"
|
||||||
|
.format(self.get_identifier_string(), e))
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
0
tempest_tripleo_ui/models/__init__.py
Normal file
0
tempest_tripleo_ui/models/__init__.py
Normal file
109
tempest_tripleo_ui/models/login.py
Normal file
109
tempest_tripleo_ui/models/login.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from tempest_tripleo_ui.widgets import login_page
|
||||||
|
from tempest_tripleo_ui import alerts
|
||||||
|
from tempest_tripleo_ui.timer import Timer
|
||||||
|
from time import sleep
|
||||||
|
from tempest import config
|
||||||
|
import logging
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def logout():
|
||||||
|
logger.info("Logging out...")
|
||||||
|
if not is_logged_in():
|
||||||
|
logger.warning("Hey! Was already logged out...")
|
||||||
|
return
|
||||||
|
|
||||||
|
while alerts.clear_danger_message(timeout=1) or \
|
||||||
|
alerts.clear_success_message(timeout=1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# First, we need to click on the toggle to open the user dropdown menu
|
||||||
|
if login_page.user_toggle_button.wait_for_element(0):
|
||||||
|
if not login_page.user_toggle_button.click():
|
||||||
|
logger.error("click failed on the 'user toggle' button")
|
||||||
|
|
||||||
|
if not login_page.logout_button.click():
|
||||||
|
logger.error("click failed on the 'Logout' button")
|
||||||
|
# FIXME: this should be an assertion
|
||||||
|
if not login_page.password.wait_for_element(5):
|
||||||
|
logger.error("Logout failed (didn't see the login screen after 5 "
|
||||||
|
"seconds)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_logged_in():
|
||||||
|
for _retries in range(0, 5):
|
||||||
|
# find the logout button or the password field,
|
||||||
|
# whichever comes first
|
||||||
|
if login_page.logout_button.wait_for_element(1):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if login_page.password.wait_for_element(1):
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.error("login.is_logged_in(): could not determine if "
|
||||||
|
"logged in or not, no recognizable elements found "
|
||||||
|
"on the page within 10 seconds")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def login(username=None, password=None):
|
||||||
|
logger.info("Logging in...")
|
||||||
|
if is_logged_in():
|
||||||
|
logger.error("Already logged in... Make sure "
|
||||||
|
"you log out first!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
username = CONF.auth.admin_username
|
||||||
|
if password is None:
|
||||||
|
password = CONF.auth.admin_password
|
||||||
|
if username is None or password is None:
|
||||||
|
logger.error("Missing username or password. Make sure they are "
|
||||||
|
"configured in the tempest conf file")
|
||||||
|
return False
|
||||||
|
|
||||||
|
login_page.username.clear()
|
||||||
|
login_page.username.send_keys(username)
|
||||||
|
login_page.password.clear()
|
||||||
|
login_page.password.send_keys(password)
|
||||||
|
login_page.password.submit()
|
||||||
|
|
||||||
|
for _retry in range(6):
|
||||||
|
if not login_page.logout_button.wait_for_element():
|
||||||
|
if login_page.unauthorized_div.wait_for_element(1):
|
||||||
|
logger.warning(login_page.unauthorized_div.get_all_text())
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not login_page.logout_button.wait_for_element(0):
|
||||||
|
logger.error(
|
||||||
|
"Login failed (waited 60 seconds and no sign of "
|
||||||
|
"a logout button...)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# wait for the modal div that covers the page to go away...
|
||||||
|
timeout = 20
|
||||||
|
timer = Timer("wait for modal after login", timeout=timeout)
|
||||||
|
while login_page.modal_div.wait_for_element(1) and \
|
||||||
|
timer.get_duration() < timeout:
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
if not timer.assert_timer():
|
||||||
|
logger.error("exceeded timer")
|
||||||
|
return False
|
||||||
|
if timer.get_duration() > 5:
|
||||||
|
logger.warning("login took {} seconds".format(
|
||||||
|
timer.get_duration()))
|
||||||
|
|
||||||
|
# wait for all spinners
|
||||||
|
alerts.wait_for_spinner()
|
||||||
|
|
||||||
|
# clear all alerts that sometimes appear straight after login
|
||||||
|
while alerts.clear_danger_message():
|
||||||
|
pass
|
||||||
|
|
||||||
|
return True
|
@ -13,29 +13,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from six.moves.urllib import request
|
|
||||||
|
|
||||||
from tempest import config
|
from tempest import config
|
||||||
from tempest import test
|
from tempest_tripleo_ui.base import GUITestCase
|
||||||
|
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
|
|
||||||
|
|
||||||
class TestBasic(test.BaseTestCase):
|
class TestBasic(GUITestCase):
|
||||||
|
|
||||||
"""Checks that the UI is available"""
|
def test_basic_plugin_functionality(self):
|
||||||
|
pass
|
||||||
opener = None
|
|
||||||
|
|
||||||
def check_if_ui_accessible(self):
|
|
||||||
response = self._get_opener().open(CONF.tripleo_ui.url).read()
|
|
||||||
self.assertIn('tripleo_ui_config.js', response)
|
|
||||||
|
|
||||||
def _get_opener(self):
|
|
||||||
if not self.opener:
|
|
||||||
self.opener = request.build_opener(request.HTTPCookieProcessor())
|
|
||||||
return self.opener
|
|
||||||
|
|
||||||
def test_basic_scenario(self):
|
|
||||||
self.check_if_ui_accessible()
|
|
||||||
|
40
tempest_tripleo_ui/timer.py
Normal file
40
tempest_tripleo_ui/timer.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from time import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Timer(object):
|
||||||
|
|
||||||
|
def __init__(self, operation_name, timeout=10):
|
||||||
|
self.timer = time()
|
||||||
|
self.operation_name = operation_name
|
||||||
|
self.timeout = timeout
|
||||||
|
self.duration = None
|
||||||
|
|
||||||
|
def assert_timer(self, message=None):
|
||||||
|
if self.timer is None:
|
||||||
|
logger.error("trying to use the same timer twice - please "
|
||||||
|
"create a new timer to start a new measurement")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.duration = time() - self.timer
|
||||||
|
self.timer = None
|
||||||
|
|
||||||
|
if self.duration > self.timeout:
|
||||||
|
err_msg = ("operation '{}' took {} seconds, allowed timeout "
|
||||||
|
"is {} seconds").format(self.operation_name,
|
||||||
|
self.duration,
|
||||||
|
self.timeout)
|
||||||
|
if message is not None:
|
||||||
|
err_msg += ": {}".format(message)
|
||||||
|
logger.error(err_msg)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_duration(self):
|
||||||
|
if self.duration is not None:
|
||||||
|
return self.duration
|
||||||
|
|
||||||
|
return time() - self.timer
|
0
tempest_tripleo_ui/widgets/__init__.py
Normal file
0
tempest_tripleo_ui/widgets/__init__.py
Normal file
13
tempest_tripleo_ui/widgets/login_page.py
Normal file
13
tempest_tripleo_ui/widgets/login_page.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from tempest_tripleo_ui.core import SeleniumElement
|
||||||
|
|
||||||
|
|
||||||
|
username = SeleniumElement.by_id("username")
|
||||||
|
password = SeleniumElement.by_id("password")
|
||||||
|
logout_button = SeleniumElement.by_id('NavBar__logoutLink')
|
||||||
|
user_toggle_button = SeleniumElement.by_id('UserDropdown__toggle')
|
||||||
|
|
||||||
|
unauthorized_div = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "login")]/div[contains'
|
||||||
|
'(@class, "alert-danger")]')
|
||||||
|
modal_div = SeleniumElement.by_xpath(
|
||||||
|
'//div[contains(@class, "modal")][contains(@role, "dialog")]')
|
Loading…
Reference in New Issue
Block a user