freeipa/ipatests/test_webui/ui_driver.py

1326 lines
41 KiB
Python

# Authors:
# Petr Vobornik <pvoborni@redhat.com>
#
# Copyright (C) 2013 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Base class for UI integration tests.
Contains browser driver and common tasks.
"""
import nose
import time
import re
import os
try:
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.ui import Select
NO_SELENIUM = False
except ImportError:
NO_SELENIUM = True
try:
import yaml
NO_YAML = False
except ImportError:
NO_YAML = True
from urllib2 import URLError
ENV_MAP = {
'MASTER': 'ipa_server',
'ADMINID': 'ipa_admin',
'ADMINPW': 'ipa_password',
'DOMAIN': 'ipa_domain',
'IPA_REALM': 'ipa_realm',
'IPA_IP': 'ipa_ip',
'IPA_NO_CA': 'no_ca',
'IPA_NO_DNS': 'no_dns',
'IPA_HAS_TRUSTS': 'has_trusts',
'IPA_HOST_CSR_PATH': 'host_csr_path',
'IPA_SERVICE_CSR_PATH': 'service_csr_path',
'AD_DOMAIN': 'ad_domain',
'AD_DC': 'ad_dc',
'AD_ADMIN': 'ad_admin',
'AD_PASSWORD': 'ad_password',
'AD_DC_IP': 'ad_dc_ip',
'TRUST_SECRET': 'trust_secret',
'SEL_TYPE': 'type',
'SEL_BROWSER': 'browser',
'SEL_HOST': 'host',
'FF_PROFILE': 'ff_profile',
}
DEFAULT_BROWSER = 'firefox'
DEFAULT_PORT = 4444
DEFAULT_TYPE = 'local'
class UI_driver(object):
"""
Base class for all UI integration tests
"""
@classmethod
def setUpClass(cls):
if NO_SELENIUM:
raise nose.SkipTest('Selenium not installed')
def __init__(self, driver=None, config=None):
self.driver = driver
self.config = config
if not config:
self.load_config()
def load_config(self):
"""
Load configuration
1) From ~/.ipa/ui_test.conf
2) From environmental variables
"""
# load config file
path = os.path.join(os.path.expanduser("~"), ".ipa/ui_test.conf")
if not NO_YAML and os.path.isfile(path):
try:
with open(path, 'r') as conf:
self.config = yaml.load(conf)
except yaml.YAMLError, e:
raise nose.SkipTest("Invalid Web UI config.\n%s" % e)
except IOError, e:
raise nose.SkipTest("Can't load Web UI test config: %s" %e)
else:
self.config = {}
c = self.config
# override with environmental variables
for k,v in ENV_MAP.iteritems():
val = os.environ.get(k)
if val is not None:
c[v] = val
# apply defaults
if 'port' not in c:
c['port'] = DEFAULT_PORT
if 'browser' not in c:
c['browser'] = DEFAULT_BROWSER
if 'type' not in c:
c['type'] = DEFAULT_TYPE
def setUp(self):
"""
Test setup
"""
if not self.driver:
self.driver = self.get_driver()
def tearDown(self):
"""
Test clean up
"""
self.driver.quit()
def get_driver(self):
"""
Get WebDriver according to configuration
"""
browser = self.config["browser"]
port = self.config["port"]
type = self.config["type"]
options = None
if browser == 'chromium':
options = ChromeOptions()
options.binary_location = '/usr/bin/chromium-browser'
if type == 'remote':
if not 'host' in self.config:
raise nose.SkipTest('Selenium server host not configured')
host = self.config["host"]
if browser == 'chrome':
capabilities = DesiredCapabilities.CHROME
elif browser == 'chromium':
capabilities = options.to_capabilities()
elif browser == 'ie':
capabilities = DesiredCapabilities.INTERNETEXPLORER
else:
capabilities = DesiredCapabilities.FIREFOX
try:
driver = webdriver.Remote(
command_executor='http://%s:%d/wd/hub' % (host, port),
desired_capabilities=capabilities)
except URLError, e:
raise nose.SkipTest('Error connecting to selenium server: %s' % e)
except RuntimeError, e:
raise nose.SkipTest('Error while establishing webdriver: %s' % e)
else:
try:
if browser == 'chrome' or browser == 'chromium':
driver = webdriver.Chrome(chrome_options=options)
elif browser == 'ie':
driver = webdriver.Ie()
else:
fp = None
if "ff_profile" in self.config:
fp = webdriver.FirefoxProfile(self.config["ff_profile"])
driver = webdriver.Firefox(fp)
except URLError, e:
raise nose.SkipTest('Error connecting to selenium server: %s' % e)
except RuntimeError, e:
raise nose.SkipTest('Error while establishing webdriver: %s' % e)
return driver
def find(self, expression, by='id', context=None, all=False, strict=False):
"""
Helper which calls selenium find_element_by_xxx methods.
expression: search expression
by: selenium.webdriver.common.by
context: element to search on. Default: driver
all: return first or array of all matching elements
strict: error out when element is not found
Returns None instead of raising exception when element is not found.
"""
assert expression, 'expression is missing'
if context is None:
context = self.driver
if not all:
method_name = 'find_element'
else:
method_name = 'find_elements'
try:
func = getattr(context, method_name)
result = func(by, expression)
except NoSuchElementException:
if strict:
raise
else:
result = None
return result
def files_loaded(self):
indicator = self.find("span.network-activity-indicator", By.CSS_SELECTOR)
return indicator is not None
def has_ca(self):
return 'no_ca' not in self.config
def has_dns(self):
return 'no_dns' not in self.config
def has_trusts(self):
return 'has_trusts' in self.config
def has_active_request(self):
"""
Check if there is running AJAX request
"""
indicator = self.find("span.network-activity-indicator", By.CSS_SELECTOR)
displayed = indicator and indicator.is_displayed()
return displayed
def wait(self, seconds=0.2):
time.sleep(seconds)
def wait_for_request(self, implicit=0.2, n=1, d=0):
"""
Wait for AJAX request to finish
"""
runner = self
for i in range(n):
self.wait(implicit)
WebDriverWait(self.driver, 10).until_not(lambda d: runner.has_active_request())
self.wait()
self.wait(d)
def xpath_has_val(self, attr, val):
"""
Create xpath expression for matching a presence of item in attribute
value where value is a list of items separated by space.
"""
return "contains(concat(' ',normalize-space(@%s), ' '),' %s ')" % (attr, val)
def init_app(self):
"""
Load and login
"""
self.load()
self.wait(0.5)
self.login()
# metadata + default page
self.wait_for_request(n=5)
def load(self):
self.driver.get(self.get_base_url())
runner = self
WebDriverWait(self.driver, 10).until(lambda d: runner.files_loaded())
def login(self):
"""
Log in if user is not logged in.
"""
self.wait_for_request(n=2)
if not self.logged_in():
auth = self.get_auth_dialog()
login_tb = self.find("//input[@type='text'][@name='username']", 'xpath', auth, strict=True)
psw_tb = self.find("//input[@type='password'][@name='password']", 'xpath', auth, strict=True)
login_tb.send_keys(self.config['ipa_admin'])
psw_tb.send_keys(self.config['ipa_password'])
psw_tb.send_keys(Keys.RETURN)
self.wait(0.5)
self.wait_for_request()
def logged_in(self):
"""
Check if user is logged in
"""
login_as = self.find('header-loggedinas', 'class name')
visible_name = login_as and login_as.is_displayed()
logged_in = not self.auth_dialog_opened() and visible_name
return logged_in
def get_auth_dialog(self):
"""
Return reference to authentication dialog
"""
return self.find('unauthorized_dialog', 'id')
def auth_dialog_opened(self):
"""
Check if authentication dialog is opened
"""
dialog = self.get_auth_dialog()
return dialog and dialog.is_displayed()
def navigate_to_entity(self, entity, facet=None):
self.driver.get(self.get_url(entity, facet))
self.wait_for_request(n=3,d=0.4)
def navigate_by_menu(self, item, complete=True):
"""
Navigate by using menu
"""
if complete:
parts = item.split('/')
if len(parts) > 1:
parent = parts[0:-1]
self.navigate_by_menu('/'.join(parent), complete)
s = ".navigation a[href='#%s']" % item
link = self.find(s, By.CSS_SELECTOR, strict=True)
assert link.is_displayed(), 'Navigation link is not displayed'
link.click()
self.wait_for_request()
self.wait_for_request(0.4)
def navigate_by_breadcrumb(self, item):
"""
Navigate by breadcrumb navigation
"""
facet = self.get_facet()
nav = self.find('div.breadcrumb', By.CSS_SELECTOR, facet, strict=True)
a = self.find(item, By.LINK_TEXT, nav, strict=True)
a.click()
self.wait_for_request()
self.wait_for_request(0.4)
def switch_to_facet(self, name):
"""
Click on tab with given name
"""
facet = self.get_facet()
s = "div.facet-tabs li[name='%s'] a" % name
link = self.find(s, By.CSS_SELECTOR, facet, strict=True)
link.click()
# double wait because of facet's paging
self.wait_for_request(0.5)
self.wait_for_request()
def get_url(self, entity, facet=None):
"""
Create entity url
"""
url = [self.get_base_url(), '#', 'e', entity]
if facet:
url.append(facet)
return '/'.join(url)
def get_base_url(self):
host = self.config.get('ipa_server')
if not host:
self.skip('FreeIPA server hostname not configured')
return 'https://%s/ipa/ui' % host
def get_facet(self):
"""
Get currently displayed facet
"""
facet = self.find('.active-facet', By.CSS_SELECTOR)
assert facet is not None, "Current facet not found"
return facet
def get_facet_info(self, facet=None):
"""
Get information of currently displayed facet
"""
info = {}
# get facet
if facet is None:
facet = self.get_facet()
info["element"] = facet
#get facet name and entity
info["name"] = facet.get_attribute('data-name')
info["entity"] = facet.get_attribute('data-entity')
# get facet title
el = self.find(".facet-header h3 *:first-child", By.CSS_SELECTOR, facet)
if el: info["title"] = el.text
# get facet pkey
el = self.find(".facet-header h3 span.facet-pkey", By.CSS_SELECTOR, facet)
if el: info["pkey"] = el.text
return info
def get_dialogs(self, strict=False, name=None):
"""
Get all dialogs in DOM
"""
s = 'div[role=dialog]'
if name:
s += " div[data-name='%s'" % name
dialogs = self.find(s, By.CSS_SELECTOR, all=True)
if strict:
assert dialogs, "No dialogs found"
return dialogs
def get_dialog(self, strict=False, name=None):
"""
Get last opened dialog
"""
dialogs = self.get_dialogs(strict, name)
dialog = None
if len(dialogs):
dialog = dialogs[-1]
return dialog
def get_last_error_dialog(self, dialog_name='error_dialog'):
"""
Get last opened error dialog or None.
"""
s = "div[role=dialog] div[data-name='%s']" % dialog_name
dialogs = self.find(s, By.CSS_SELECTOR, all=True)
dialog = None
if dialogs:
dialog = dialogs[-1]
return dialog
def get_dialog_info(self):
"""
Get last open dialog info: name, text if any.
Returns None if no dialog is open.
"""
dialog = self.get_dialog()
info = None
if dialog:
content = self.find('div.ui-dialog-content', By.CSS_SELECTOR, dialog, strict=True)
info = {
'name': content.get_attribute('data-name'),
'text': content.text,
}
return info
def click_on_link(self, text, parent=None):
"""
Click on link with given text and parent.
"""
if not parent:
parent = self.get_form()
link = self.find(text, By.LINK_TEXT, parent, strict=True)
link.click()
def facet_button_click(self, name):
"""
Click on facet button with given name
"""
facet = self.get_facet()
s = ".facet-controls a[name=%s]" % name
self._button_click(s, facet, name)
def dialog_button_click(self, name, dialog=None):
"""
Click on dialog button with given name
Chooses last dialog if none is supplied
"""
if not dialog:
dialog = self.get_dialog(strict=True)
s = "div.ui-dialog-buttonset button[name=%s]" % name
self._button_click(s, dialog, name)
def action_button_click(self, name, parent):
if not parent:
parent = self.get_form()
s = "a[name='%s'].action-button" % name
self._button_click(s, parent, name)
def button_click(self, name, parent=None):
if not parent:
parent = self.get_form()
s = "a[name='%s'].ui-button" % name
self._button_click(s, parent, name)
def _button_click(self, selector, parent, name=''):
btn = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
disabled = 'ui-state-disabled' in btn.get_attribute("class").split(' ')
assert btn.is_displayed(), 'Button is not displayed: %s' % name
assert not disabled, 'Invalid button state: disabled. Button: %s' % name
btn.click()
self.wait_for_request()
def get_form(self):
"""
Get last dialog or visible facet
"""
form = self.get_dialog()
if not form:
form = self.get_facet()
return form
def select(self, selector, value, parent=None):
if not parent:
parent = self.get_form()
el = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
Select(el).select_by_value(value)
def fill_text(self, selector, value, parent=None):
"""
Clear and enter text into input defined by selector.
Use for non-standard fields.
"""
if not parent:
parent = self.get_form()
tb = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
tb.clear()
tb.send_keys(value)
def fill_input(self, name, value, type="text", parent=None):
s = "div[name='%s'] input[type='%s'][name='%s']" % (name, type, name)
self.fill_text(s, value, parent)
def fill_textarea(self, name, value, parent=None):
"""
Clear and fill textarea.
"""
s = "textarea[name='%s']" % (name)
self.fill_text(s, value, parent)
def fill_textbox(self, name, value, parent=None):
"""
Clear and fill textbox.
"""
self.fill_input(name, value, "text", parent)
def fill_password(self, name, value, parent=None):
"""
Clear and fill input[type=password]
"""
self.fill_input(name, value, "password", parent)
def add_multivalued(self, name, value, parent=None):
"""
Adds new value to multivalued textbox
"""
if not parent:
parent = self.get_form()
s = "div[name='%s'].multivalued-widget" % name
w = self.find(s, By.CSS_SELECTOR, parent, strict=True)
add_btn = self.find("Add", By.LINK_TEXT, w, strict=True)
add_btn.click()
s = "div[name=value] input"
inputs = self.find(s, By.CSS_SELECTOR, w, all=True)
last = inputs[-1]
last.send_keys(value)
def delete_multivalued(self, name, value, parent=None):
if not parent:
parent = self.get_form()
s = "div[name='%s'].multivalued-widget" % name
w = self.find(s, By.CSS_SELECTOR, parent, strict=True)
s = "div[name=value] input"
inputs = self.find(s, By.CSS_SELECTOR, w, all=True)
clicked = False
for i in inputs:
val = i.get_attribute('value')
n = i.get_attribute('name')
if val == value:
s = "input[name='%s'] ~ a[name=remove]" % n
link = self.find(s, By.CSS_SELECTOR, w, strict=True)
link.click()
self.wait()
clicked = True
assert clicked, 'Value was not removed'
def check_option(self, name, value=None, parent=None):
"""
Find checkbox or radio with name which matches ^NAME\d$
"""
if not parent:
parent = self.get_form()
s = "//input[@type='checkbox' or 'radio'][contains(@name, '%s')]" % name
if value is not None:
s += "[@value='%s']" % value
opts = self.find(s, "xpath", parent, all=True)
opt = None
# Select only the one which matches exactly the name
for o in opts:
n = o.get_attribute("name")
if n == name or re.match("^%s\d$" % name, n):
opt = o
break
assert opt is not None, "Option not found: %s" % name
opt.click()
def select_combobox(self, name, value, parent=None):
"""
Select value in a combobox. Search if not found.
"""
if not parent:
parent = self.get_form()
s = "[name='%s'].combobox-widget" % name
cb = self.find(s, By.CSS_SELECTOR, parent, strict=True)
open_btn = self.find('a[name=open] span', By.CSS_SELECTOR, cb, strict=True)
open_btn.click()
self.wait()
self.wait_for_request()
search_btn = self.find('a[name=search] span', By.CSS_SELECTOR, cb, strict=True)
opt_s = "select[name=list] option[value='%s']" % value
option = self.find(opt_s, By.CSS_SELECTOR, cb)
if not option:
# try to search
self.fill_textbox('filter', value, cb)
search_btn.click()
self.wait_for_request()
option = self.find(opt_s, By.CSS_SELECTOR, cb, strict=True)
option.click()
# Chrome does not close search area on click
if search_btn.is_displayed():
self.driver.switch_to_active_element().send_keys(Keys.RETURN)
self.wait()
def get_undo_button(self, field, parent):
"""
Get field undo button
"""
if not parent:
parent = self.get_form()
s = "div[name='%s'].field span.undo" % (field)
undo = self.find(s, By.CSS_SELECTOR, parent, strict=True)
return undo
def get_rows(self, parent=None):
"""
Return all rows of search table.
"""
if not parent:
parent = self.get_form()
# select table rows
s = 'table.search-table tbody tr'
rows = self.find(s, By.CSS_SELECTOR, parent, all=True)
return rows
def navigate_to_row_record(self, row, pkey_column=None):
s = 'a'
if pkey_column:
s = "div[name='%s'] a" % pkey_column
link = self.find(s, By.CSS_SELECTOR, row, strict=True)
link.click()
self.wait_for_request(0.4)
self.wait_for_request()
def get_table_selector(self, name=None):
s = "table"
if name:
s += "[name='%s']" % name
s +='.search-table'
return s
def select_record(self, pkey, parent=None, table_name=None):
"""
Select record with given pkey in search table.
"""
if not parent:
parent = self.get_form()
s = self.get_table_selector(table_name)
s += " tbody td input[value='%s']" % pkey
checkbox = self.find(s, By.CSS_SELECTOR, parent, strict=True)
checkbox.click()
self.wait()
def has_record(self, pkey, parent=None, table_name=None):
if not parent:
parent = self.get_form()
s = self.get_table_selector(table_name)
s += " tbody td input[value='%s']" % pkey
checkbox = self.find(s, By.CSS_SELECTOR, parent)
return checkbox is not None
def navigate_to_record(self, pkey, parent=None, table_name=None, entity=None, facet='search'):
"""
Clicks on record with given pkey in search table and thus cause
navigation to the record.
"""
if entity:
self.navigate_to_entity(entity, facet)
if not parent:
parent = self.get_facet()
s = self.get_table_selector(table_name)
s += " tbody"
table = self.find(s, By.CSS_SELECTOR, parent, strict=True)
link = self.find(pkey, By.LINK_TEXT, table, strict=True)
link.click()
self.wait_for_request()
def delete_record(self, pkeys, fields=None, parent=None, table_name=None):
"""
Delete records with given pkeys in currently opened search table.
"""
if type(pkeys) is not list:
pkeys = [pkeys]
# select
selected = False
for pkey in pkeys:
delete = self.has_record(pkey, parent, table_name)
if delete:
self.select_record(pkey, parent, table_name)
selected = True
# exec and confirm
if selected:
if table_name and parent:
s = self.get_table_selector(table_name)
table = self.find(s, By.CSS_SELECTOR, parent, strict=True)
self.action_button_click('remove', table)
else:
self.facet_button_click('remove')
if fields:
self.fill_fields(fields)
self.dialog_button_click('ok')
self.wait_for_request(n=2)
self.wait()
def delete(self, entity, data_list, facet='search', navigate=True):
"""
Delete entity records:
"""
if navigate:
self.navigate_to_entity(entity, facet)
for data in data_list:
pkey = data.get('pkey')
fields = data.get('del')
self.delete_record(pkey, fields)
def fill_fields(self, fields, parent=None, undo=False):
"""
Fill dialog or facet inputs with give data.
Expected format:
[
('widget_type', 'key', value'),
('widget_type', 'key2', value2'),
]
"""
if not parent:
parent = self.get_form()
for field in fields:
type = field[0]
key = field[1]
val = field[2]
if undo:
self.assert_undo_button(key, False, parent)
if type == 'textbox':
self.fill_textbox(key, val, parent)
elif type == 'textarea':
self.fill_textarea(key, val, parent)
elif type == 'password':
self.fill_password(key, val, parent)
elif type == 'radio':
self.check_option(key, val, parent)
elif type == 'checkbox':
self.check_option(key, parent=parent)
elif type == 'combobox':
self.select_combobox(key, val, parent)
elif type == 'add_table_record':
self.add_table_record(key, val, parent)
elif type == 'add_table_association':
self.add_table_associations(key, val, parent)
elif type == 'table':
self.select_record(val, parent, key)
self.wait()
if undo:
self.assert_undo_button(key, True, parent)
def find_record(self, entity, data, facet='search', dummy='XXXXXXX'):
self.assert_facet(entity, facet)
facet = self.get_facet()
search_field_s = '.search-filter input[name=filter]'
key = data.get('pkey')
self.fill_text(search_field_s, dummy, facet)
self.action_button_click('find', facet)
self.wait_for_request(n=2)
self.assert_record(key, negative=True)
self.fill_text(search_field_s, key, facet)
self.action_button_click('find', facet)
self.wait_for_request(n=2)
self.assert_record(key)
self.fill_text(search_field_s, '', facet)
self.action_button_click('find', facet)
self.wait_for_request(n=2)
def add_record(self, entity, data, facet='search', facet_btn='add',
dialog_btn='add', delete=False, pre_delete=True,
dialog_name='add', navigate=True):
"""
Add records.
Expected data format:
{
'pkey': 'key',
add: [
('widget_type', 'key', 'value'),
('widget_type', 'key2', 'value2'),
],
}
"""
pkey = data['pkey']
if navigate:
self.navigate_to_entity(entity, facet)
# check facet
self.assert_facet(entity, facet)
# delete if exists, ie. from previous test fail
if pre_delete:
self.delete_record(pkey, data.get('del'))
# current row count
self.wait_for_request(0.5)
count = len(self.get_rows())
# open add dialog
self.assert_no_dialog()
self.facet_button_click(facet_btn)
self.assert_dialog(dialog_name)
# fill dialog
self.fill_fields(data['add'])
# confirm dialog
self.dialog_button_click(dialog_btn)
self.wait_for_request()
self.wait_for_request()
# check expected error/warning/info
expected = ['error_4304_info']
dialog_info = self.get_dialog_info()
if dialog_info and dialog_info['name'] in expected:
self.dialog_button_click('ok')
self.wait_for_request()
# check for error
self.assert_no_error_dialog()
self.wait_for_request()
self.wait_for_request(0.4)
# check if table has more rows
new_count = len(self.get_rows())
# adjust because of paging
expected = count + 1
if count == 20:
expected = 20
self.assert_row_count(expected, new_count)
# delete record
if delete:
self.delete_record(pkey)
new_count = len(self.get_rows())
self.assert_row_count(count, new_count)
def mod_record(self, entity, data, facet='details', facet_btn='update'):
"""
Mod record
Assumes that it is already on details page.
"""
self.assert_facet(entity, facet)
# TODO assert pkey
self.assert_facet_button_enabled(facet_btn, enabled=False)
self.fill_fields(data['mod'], undo=True)
self.assert_facet_button_enabled(facet_btn)
self.facet_button_click(facet_btn)
self.wait_for_request()
self.wait_for_request()
self.assert_facet_button_enabled(facet_btn, enabled=False)
def basic_crud(self, entity, data,
parent_entity = None,
details_facet = 'details',
search_facet = 'search',
default_facet = 'details',
add_facet_btn='add',
add_dialog_btn='add',
add_dialog_name='add',
update_btn='update',
breadcrumb=None,
navigate=True,
delete=True):
"""
Basic CRUD operation sequence.
Expected data format:
{
'pkey': 'key',
'add': [
('widget_type', 'key', 'value'),
('widget_type', 'key2', 'value2'),
],
'mod': [
('widget_type', 'key', 'value'),
('widget_type', 'key2', 'value2'),
],
}
"""
# important for nested entities. Ie. autoumount maps
if not parent_entity:
parent_entity = entity
pkey = data['pkey']
# 1. Open Search Facet
if navigate:
self.navigate_to_entity(parent_entity)
self.assert_facet(parent_entity, search_facet)
self.wait_for_request()
# 2. Add record
self.add_record(parent_entity, data, facet=search_facet, navigate=False)
# Find
self.find_record(parent_entity, data, search_facet)
# 3. Navigate to details facet
self.navigate_to_record(pkey)
self.assert_facet(entity, default_facet)
self.wait_for_request(0.5)
if default_facet != details_facet:
self.switch_to_facet(details_facet)
self.assert_facet(entity, details_facet)
# 4. Mod values
if data.get('mod'):
# TODO: assert values
self.mod_record(entity, data, details_facet, update_btn)
# TODO: assert modified values
if not breadcrumb:
self.navigate_to_entity(entity, search_facet)
else:
self.navigate_by_breadcrumb(breadcrumb)
# 5. Delete record
if delete:
self.delete_record(pkey, data.get('del'))
def add_table_record(self, name, data, parent=None):
"""
Add record to dnsrecord table, association table and similar
"""
if not parent:
parent = self.get_form()
s = self.get_table_selector(name)
table = self.find(s, By.CSS_SELECTOR, parent, strict=True)
s = "a[name=%s] span.button-label" % 'add'
btn = self.find(s, By.CSS_SELECTOR, table, strict=True)
btn.click()
self.wait()
self.fill_fields(data['fields'])
self.dialog_button_click('add')
self.wait_for_request()
def add_associations(self, pkeys, facet=None, delete=False):
"""
Add associations
"""
if facet:
self.switch_to_facet(facet)
self.facet_button_click('add')
self.wait_for_request()
for key in pkeys:
self.select_record(key, table_name='available')
self.button_click('add')
self.dialog_button_click('add')
self.wait_for_request()
for key in pkeys:
self.assert_record(key)
if delete:
self.delete_record(key)
self.assert_record(key, negative=True)
def add_table_associations(self, table_name, pkeys, parent=False, delete=False):
if not parent:
parent = self.get_form()
s = self.get_table_selector(table_name)
table = self.find(s, By.CSS_SELECTOR, parent, strict=True)
s = "a[name=%s] span.button-label" % 'add'
btn = self.find(s, By.CSS_SELECTOR, table, strict=True)
btn.click()
self.wait_for_request(0.4)
for key in pkeys:
self.select_record(key, table_name='available')
self.button_click('add')
self.dialog_button_click('add')
self.wait_for_request(n=2)
for key in pkeys:
self.assert_record(key, parent, table_name)
if delete:
self.delete_record(pkeys, None, parent, table_name)
for key in pkeys:
self.assert_record(key, parent, table_name, negative=True)
def action_list_action(self, name):
cont = self.find(".active-facet .facet-action-list", By.CSS_SELECTOR, strict=True)
select = self.find("select[name=action]", By.CSS_SELECTOR, cont, strict=True)
Select(select).select_by_value(name)
self.button_click('apply', cont)
self.wait()
def action_panel_action(self, panel_name, action):
s = "div[data-name='%s'].action-panel" % panel_name
s += " a[data-name='%s']" % action
link = self.find(s, By.CSS_SELECTOR, strict=True)
link.click()
self.wait()
def enable_action(self):
title = self.find('.active-facet div.facet-title', By.CSS_SELECTOR, strict=True)
self.action_list_action('enable')
self.wait_for_request(n=2)
self.assert_no_error_dialog()
self.assert_class(title, 'disabled', negative=True)
def disable_action(self):
title = self.find('.active-facet div.facet-title', By.CSS_SELECTOR, strict=True)
self.action_list_action('disable')
self.wait_for_request(n=2)
self.assert_no_error_dialog()
self.assert_class(title, 'disabled')
def delete_action(self, entity, pkey, facet='search'):
self.action_list_action('delete')
self.wait_for_request(n=4)
self.assert_no_error_dialog()
self.assert_facet(entity, facet)
self.assert_record(pkey, negative=True)
def mod_rule_tables(self, tables, categories, no_categories):
"""
Test functionality of rule table widgets in a facet
"""
def get_t_vals(t):
table = t[0]
k = t[1]
e = []
if len(t) > 2:
e = t[2]
return table, k, e
t_list = [t[0] for t in tables if t[0] not in no_categories]
# add values
for t in tables:
table, keys, exts = get_t_vals(t)
# add one by one to test for #3711
for key in keys:
self.add_table_associations(table, [key])
#disable tables
for cat in categories:
self.check_option(cat, 'all')
# update
self.assert_rule_tables_enabled(t_list, False)
self.facet_button_click('update')
self.wait_for_request(n=3, d=0.3)
self.assert_rule_tables_enabled(t_list, False)
p = self.get_form()
# now tables in categories should be empty, check it
for t in tables:
table, keys, exts = get_t_vals(t)
if table in no_categories:
# clear the rest
self.delete_record(keys, None, p, table)
continue
for key in keys:
self.assert_record(key, p, table, negative=True)
# enable tables
for cat in categories:
self.check_option(cat, '')
self.assert_rule_tables_enabled(t_list, True)
self.facet_button_click('update')
self.wait_for_request(n=3, d=0.3)
self.assert_rule_tables_enabled(t_list, True)
for t in tables:
table, keys, exts = get_t_vals(t)
# add multiple at once and test table delete button
self.add_table_associations(table, keys, delete=True)
def skip(self, reason):
raise nose.SkipTest(reason)
def assert_text(self, selector, value, parent=None):
"""
Assert read-only text value in details page or in a form
"""
if not parent:
parent = self.get_form()
el = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
text = el.text.strip()
value = value.strip()
assert text == value, "Invalid value: '%s' Expected: %s" % (text, value)
def assert_text_field(self, name, value, parent=None, element='label'):
"""
Assert read-only text value in details page or in a form
"""
s = "div[name='%s'] %s[name='%s']" % (name, element, name)
self.assert_text(s, value, parent)
def assert_no_dialog(self):
"""
Assert that no dialog is opened
"""
dialogs = self.get_dialogs()
assert not dialogs, 'Invalid state: dialog opened'
def assert_dialog(self, name=None):
"""
Assert that one dialog is opened or a dialog with given name
"""
dialogs = self.get_dialogs(name)
assert len(dialogs) == 1, 'No or more than one dialog opened'
def assert_no_error_dialog(self):
"""
Assert that no error dialog is opened
"""
dialog = self.get_last_error_dialog()
ok = dialog is None
if not ok:
msg = self.find('p', By.CSS_SELECTOR, dialog).text
assert ok, 'Unexpected error: %s' % msg
def assert_row_count(self, expected, current):
"""
Assert that row counts match
"""
assert expected == current, "Rows don't match. Expected: %d, Got: %d" % (expected, current)
def assert_button_enabled(self, name, context_selector=None, enabled=True):
"""
Assert that button is enabled or disabled
"""
s = ""
if context_selector:
s = context_selector
s += "a[name=%s]" % name
facet = self.get_facet()
btn = self.find(s, By.CSS_SELECTOR, facet, strict=True)
cls = 'action-button-disabled'
has_cls = cls in btn.get_attribute("class").split(' ')
valid = enabled ^ has_cls
assert btn.is_displayed(), 'Button is not displayed'
assert valid, 'Button has incorrect enabled state.'
def assert_facet_button_enabled(self, name, enabled=True):
"""
Assert that facet button is enabled or disabled
"""
self.assert_button_enabled(name, ".facet-controls ", enabled)
def assert_table_button_enabled(self, name, table_name, enabled=True):
"""
Assert that button in table is enabled/disabled
"""
s = "table[name='%s'] " % table_name
self.assert_button_enabled(name, s, enabled)
def assert_facet(self, entity, facet=None):
"""
Assert that current facet is correct
"""
info = self.get_facet_info()
if not facet is None:
assert info["name"] == facet, "Invalid facet. Expected: %s, Got: %s " % (facet, info["name"])
assert info["entity"] == entity, "Invalid entity. Expected: %s, Got: %s " % (entity, info["entity"])
def assert_undo_button(self, field, visible=True, parent=None):
"""
Assert that undo button is or is not visible
"""
undo = self.get_undo_button(field, parent)
state = undo.is_displayed()
assert state == visible, 'Undo button has invalid state. Field: %s' % field
def assert_visible(self, selector, parent=None, negative=False):
"""
Assert that element defined by selector is visible
"""
if not parent:
parent = self.get_form()
el = self.find(selector, By.CSS_SELECTOR, parent, strict=True)
visible = el.is_displayed()
if negative:
assert not visible, "Element visible: %s" % selector
else:
assert visible, "Element not visible: %s" % selector
def assert_record(self, pkey, parent=None, table_name=None, negative=False):
"""
Assert that record is in current search table
"""
has = self.has_record(pkey, parent, table_name)
if negative:
assert not has, "Record exists when it shouldn't: %s" % pkey
else:
assert has, 'Record does not exist: %s' % pkey
def assert_indirect_record(self, pkey, entity, facet, negative=False, switch=True, lower=True):
"""
Switch to indirect facet and assert record.
Lowers the key by default.
"""
if switch:
self.switch_to_facet(facet)
radio_name = "%s-%s-type-radio" % (entity, facet.replace('_', '-'))
self.check_option(radio_name, 'indirect')
self.wait_for_request(n=2)
key = pkey
if lower:
key = key.lower()
self.assert_record(key)
def assert_class(self, element, cls, negative=False):
"""
Assert that element has certain class
"""
valid = cls in element.get_attribute('class').split(' ')
if negative:
assert not valid, "Element contains unwanted class: %s" % cls
else:
assert valid, "Element doesn't contain required class: %s" % cls
def assert_rule_tables_enabled(self, tables, enabled):
for table in tables:
self.assert_table_button_enabled('add', table, enabled)