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.
|
||||
|
||||
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
|
||||
-------------------------
|
||||
|
||||
|
@ -35,3 +50,38 @@ On your undercloud:
|
|||
|
||||
This will run all of the tests contained in the tempest-tripleo-ui plugin
|
||||
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
|
||||
six>=1.10.0 # MIT
|
||||
tempest>=17.1.0 # Apache-2.0
|
||||
selenium>=3.14.1 # Apache-2.0
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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 = [
|
||||
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')
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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,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
|
||||
# under the License.
|
||||
|
||||
from six.moves.urllib import request
|
||||
|
||||
from tempest import config
|
||||
from tempest import test
|
||||
|
||||
from tempest_tripleo_ui.base import GUITestCase
|
||||
|
||||
CONF = config.CONF
|
||||
|
||||
|
||||
class TestBasic(test.BaseTestCase):
|
||||
class TestBasic(GUITestCase):
|
||||
|
||||
"""Checks that the UI is available"""
|
||||
|
||||
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()
|
||||
def test_basic_plugin_functionality(self):
|
||||
pass
|
||||
|
|
|
@ -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,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