33756ac899
Include: - util modules. such as table_parser, ssh/localhost clients, cli module, exception, logger, etc. Util modules are mostly used by keywords. - keywords modules. These are helper functions that are used directly by test functions. - platform (with platform or platform_sanity marker) and stx-openstack (with sanity, sx_sanity, cpe_sanity, or storage_sanity marker) sanity testcases - pytest config conftest, and test fixture modules - test config file template/example Required packages: - python3.4 or python3.5 - pytest >=3.10,<4.0 - pexpect - requests - pyyaml - selenium (firefox, ffmpeg, pyvirtualdisplay, Xvfb or Xephyr or Xvnc) Limitations: - Anything that requires copying from Test File Server will not work until a public share is configured to shared test files. Tests skipped for now. Co-Authored-By: Maria Yousaf <maria.yousaf@windriver.com> Co-Authored-By: Marvin Huang <marvin.huang@windriver.com> Co-Authored-By: Yosief Gebremariam <yosief.gebremariam@windriver.com> Co-Authored-By: Paul Warner <paul.warner@windriver.com> Co-Authored-By: Xueguang Ma <Xueguang.Ma@windriver.com> Co-Authored-By: Charles Chen <charles.chen@windriver.com> Co-Authored-By: Daniel Graziano <Daniel.Graziano@windriver.com> Co-Authored-By: Jordan Li <jordan.li@windriver.com> Co-Authored-By: Nimalini Rasa <nimalini.rasa@windriver.com> Co-Authored-By: Senthil Mukundakumar <senthil.mukundakumar@windriver.com> Co-Authored-By: Anuejyan Manokeran <anujeyan.manokeran@windriver.com> Co-Authored-By: Peng Peng <peng.peng@windriver.com> Co-Authored-By: Chris Winnicki <chris.winnicki@windriver.com> Co-Authored-By: Joe Vimar <Joe.Vimar@windriver.com> Co-Authored-By: Alex Kozyrev <alex.kozyrev@windriver.com> Co-Authored-By: Jack Ding <jack.ding@windriver.com> Co-Authored-By: Ming Lei <ming.lei@windriver.com> Co-Authored-By: Ankit Jain <ankit.jain@windriver.com> Co-Authored-By: Eric Barrett <eric.barrett@windriver.com> Co-Authored-By: William Jia <william.jia@windriver.com> Co-Authored-By: Joseph Richard <Joseph.Richard@windriver.com> Co-Authored-By: Aldo Mcfarlane <aldo.mcfarlane@windriver.com> Story: 2005892 Task: 33750 Signed-off-by: Yang Liu <yang.liu@windriver.com> Change-Id: I7a88a47e09733d39f024144530f5abb9aee8cad2
589 lines
19 KiB
Python
589 lines
19 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
from selenium.common import exceptions
|
|
from selenium.webdriver.common import by
|
|
import selenium.webdriver.support.ui as Support
|
|
import six
|
|
|
|
from utils.horizon.regions import baseregion
|
|
from utils.horizon.regions import menus
|
|
from time import sleep
|
|
|
|
|
|
class FieldFactory(baseregion.BaseRegion):
|
|
"""Factory for creating form field objects."""
|
|
|
|
FORM_FIELDS_TYPES = set()
|
|
_element_locator_str_prefix = 'div.form-group'
|
|
|
|
def __init__(self, driver, src_elem=None):
|
|
super(FieldFactory, self).__init__(driver, src_elem)
|
|
|
|
def fields(self):
|
|
for field_cls in self.FORM_FIELDS_TYPES:
|
|
locator = (by.By.CSS_SELECTOR,
|
|
'%s %s' % (self._element_locator_str_prefix,
|
|
field_cls._element_locator_str_suffix))
|
|
elements = super(FieldFactory, self)._get_elements(*locator)
|
|
for element in elements:
|
|
yield field_cls(self.driver, src_elem=element)
|
|
|
|
@classmethod
|
|
def register_field_cls(cls, field_class, base_classes=None):
|
|
"""Register new field class.
|
|
|
|
Add new field class and remove all base classes from the set of
|
|
registered classes as they should not be in.
|
|
"""
|
|
cls.FORM_FIELDS_TYPES.add(field_class)
|
|
cls.FORM_FIELDS_TYPES -= set(base_classes)
|
|
|
|
|
|
class MetaBaseFormFieldRegion(type):
|
|
"""Register form field class in FieldFactory."""
|
|
|
|
def __init__(cls, name, bases, dct):
|
|
FieldFactory.register_field_cls(cls, bases)
|
|
super(MetaBaseFormFieldRegion, cls).__init__(name, bases, dct)
|
|
|
|
|
|
@six.add_metaclass(MetaBaseFormFieldRegion)
|
|
class BaseFormFieldRegion(baseregion.BaseRegion):
|
|
"""Base class for form fields classes."""
|
|
|
|
_label_locator = None
|
|
_element_locator = None
|
|
|
|
@property
|
|
def label(self):
|
|
return self._get_element(*self._label_locator)
|
|
|
|
@property
|
|
def element(self):
|
|
return self.src_elem
|
|
|
|
@property
|
|
def name(self):
|
|
return self.element.get_attribute('name') \
|
|
or self.element.get_attribute('id')
|
|
# some region in create instance form don't have attribute `name`, use `id` instead
|
|
|
|
def is_required(self):
|
|
classes = self.driver.get_attribute('class')
|
|
return 'required' in classes
|
|
|
|
def is_displayed(self):
|
|
return self.element.is_displayed()
|
|
|
|
|
|
class CheckBoxMixin(object):
|
|
|
|
@property
|
|
def label(self):
|
|
id_attribute = self.element.get_attribute('id')
|
|
return self.element.find_element(
|
|
by.By.XPATH, '../..//label[@for="{}"]'.format(id_attribute))
|
|
|
|
def is_marked(self):
|
|
return self.element.is_selected()
|
|
|
|
def mark(self):
|
|
if not self.is_marked():
|
|
self.label.click()
|
|
|
|
def unmark(self):
|
|
if self.is_marked():
|
|
self.label.click()
|
|
|
|
|
|
class CheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
|
|
"""Checkbox field."""
|
|
|
|
_element_locator_str_suffix = 'input[type=checkbox]'
|
|
|
|
|
|
class RadioCheckBoxFormFieldRegion(CheckBoxMixin, BaseFormFieldRegion):
|
|
"""Checkbox field."""
|
|
|
|
_element_locator_str_suffix = 'input[type=radio]'
|
|
|
|
|
|
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
|
|
"""Choose file field."""
|
|
|
|
_element_locator_str_suffix = 'input[type=file]'
|
|
|
|
def choose(self, path):
|
|
self.element.send_keys(path)
|
|
|
|
|
|
class BaseTextFormFieldRegion(BaseFormFieldRegion):
|
|
|
|
_element_locator = None
|
|
|
|
@property
|
|
def text(self):
|
|
return self.element.text
|
|
|
|
@text.setter
|
|
def text(self, text):
|
|
self._fill_field_element(text, self.element)
|
|
|
|
|
|
class TextInputFormFieldRegion(BaseTextFormFieldRegion):
|
|
"""Text input box."""
|
|
|
|
_element_locator_str_suffix = \
|
|
'input[type=text], input[type=None]' # 'div > input[type=text], div > input[type=None]'
|
|
|
|
|
|
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
|
|
"""Password text input box."""
|
|
|
|
_element_locator_str_suffix = 'input[type=password]'
|
|
|
|
|
|
class EmailInputFormFieldRegion(BaseTextFormFieldRegion):
|
|
"""Email text input box."""
|
|
|
|
_element_locator_str_suffix = 'input[type=email]'
|
|
|
|
|
|
class TextAreaFormFieldRegion(BaseTextFormFieldRegion):
|
|
"""Multi-line text input box."""
|
|
|
|
_element_locator_str_suffix = 'textarea'
|
|
|
|
|
|
class IntegerFormFieldRegion(BaseFormFieldRegion):
|
|
"""Integer input box."""
|
|
|
|
_element_locator_str_suffix = 'input[type=number]'
|
|
|
|
@property
|
|
def value(self):
|
|
return self.element.get_attribute("value")
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
self._fill_field_element(value, self.element)
|
|
|
|
|
|
class SelectFormFieldRegion(BaseFormFieldRegion):
|
|
"""Select box field."""
|
|
|
|
_element_locator_str_suffix = 'select.form-control'
|
|
|
|
def is_displayed(self):
|
|
return self.element._el.is_displayed()
|
|
|
|
@property
|
|
def element(self):
|
|
return Support.Select(self.src_elem)
|
|
|
|
@property
|
|
def values(self):
|
|
results = []
|
|
for option in self.element.all_selected_options:
|
|
results.append(option.get_attribute('value'))
|
|
return results
|
|
|
|
@property
|
|
def options(self):
|
|
results = collections.OrderedDict()
|
|
for option in self.element.options:
|
|
results[option.get_attribute('value')] = option.text
|
|
return results
|
|
|
|
@property
|
|
def name(self):
|
|
return self.element._el.get_attribute('name') \
|
|
or self.element._el.get_attribute('id')
|
|
|
|
@property
|
|
def text(self):
|
|
return self.element.first_selected_option.text
|
|
|
|
@text.setter
|
|
def text(self, text):
|
|
self.element.select_by_visible_text(text)
|
|
|
|
@property
|
|
def value(self):
|
|
return self.element.first_selected_option.get_attribute('value')
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
self.element.select_by_value(value)
|
|
|
|
|
|
class ThemableSelectFormFieldRegion(BaseFormFieldRegion):
|
|
"""Select box field."""
|
|
|
|
_element_locator_str_suffix = 'div > .themable-select'
|
|
_raw_select_locator = (by.By.CSS_SELECTOR, 'select')
|
|
_selected_label_locator = (by.By.CSS_SELECTOR, '.dropdown-title')
|
|
_dropdown_menu_locator = (by.By.CSS_SELECTOR, 'ul.dropdown-menu > li > a')
|
|
|
|
def __init__(self, driver, strict_options_match=False, **kwargs):
|
|
super(ThemableSelectFormFieldRegion, self).__init__(
|
|
driver, **kwargs)
|
|
self.strict_options_match = strict_options_match
|
|
|
|
@property
|
|
def hidden_element(self):
|
|
elem = self._get_element(*self._raw_select_locator)
|
|
return SelectFormFieldRegion(self.driver, src_elem=elem)
|
|
|
|
@property
|
|
def name(self):
|
|
return self.hidden_element.name
|
|
|
|
@property
|
|
def text(self):
|
|
return self._get_element(*self._selected_label_locator).text.strip()
|
|
|
|
@property
|
|
def value(self):
|
|
return self.hidden_element.value
|
|
|
|
@property
|
|
def options(self):
|
|
return self._get_elements(*self._dropdown_menu_locator)
|
|
|
|
@text.setter
|
|
def text(self, text):
|
|
if text != self.text:
|
|
self.src_elem.click()
|
|
for option in self.options:
|
|
if self.strict_options_match:
|
|
match = text == option.text.strip()
|
|
else:
|
|
match = option.text.startswith(text)
|
|
if match:
|
|
option.click()
|
|
return
|
|
raise ValueError('Widget "%s" does have an option with text "%s"'
|
|
% (self.name, text))
|
|
|
|
@value.setter
|
|
def value(self, value):
|
|
if value != self.value:
|
|
self.src_elem.click()
|
|
for option in self.options:
|
|
if value == option.get_attribute('data-select-value'):
|
|
option.click()
|
|
return
|
|
raise ValueError('Widget "%s" does have an option with value "%s"'
|
|
% (self.name, value))
|
|
|
|
|
|
class YesOrNoFormFieldRegion(BaseFormFieldRegion):
|
|
_element_locator_str_suffix = 'div.btn-group'
|
|
_buttons_locator = (by.By.CSS_SELECTOR, 'label')
|
|
_name_locator = (by.By.XPATH, '..//label/span')
|
|
|
|
def click_yes(self):
|
|
button = self._get_elements(*self._buttons_locator)[0]
|
|
button.click()
|
|
|
|
def click_no(self):
|
|
button = self._get_elements(*self._buttons_locator)[1]
|
|
button.click()
|
|
|
|
@property
|
|
def name(self):
|
|
name_element = self._get_element(*self._name_locator)
|
|
return name_element.text
|
|
|
|
|
|
class BaseFormRegion(baseregion.BaseRegion):
|
|
"""Base class for forms."""
|
|
|
|
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary')
|
|
_submit_danger_locator = (by.By.CSS_SELECTOR, '*.btn.btn-danger')
|
|
_cancel_locator = (by.By.CSS_SELECTOR, '*.btn.cancel')
|
|
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
|
|
|
|
def __init__(self, driver, src_elem=None):
|
|
# In most cases forms can be located through _default_form_locator,
|
|
# so specifying source element can be skipped.
|
|
if src_elem is None:
|
|
# fake self.src_elem must be set up in order self._get_element work
|
|
self.src_elem = driver
|
|
# bind the topmost modal form in a modal stack
|
|
src_elem = self._get_elements(*self._default_form_locator)[-1]
|
|
super(BaseFormRegion, self).__init__(driver, src_elem)
|
|
|
|
@property
|
|
def _submit_element(self):
|
|
try:
|
|
submit_element = self._get_element(*self._submit_locator)
|
|
except exceptions.NoSuchElementException:
|
|
submit_element = self._get_element(*self._submit_danger_locator)
|
|
return submit_element
|
|
|
|
def submit(self):
|
|
self._submit_element.click()
|
|
self.wait_till_spinner_disappears()
|
|
|
|
@property
|
|
def _cancel_element(self):
|
|
return self._get_element(*self._cancel_locator)
|
|
|
|
def cancel(self):
|
|
self._cancel_element.click()
|
|
|
|
|
|
class FormRegion(BaseFormRegion):
|
|
"""Standard form."""
|
|
FIELDS = None
|
|
_header_locator = (by.By.CSS_SELECTOR, 'div.modal-header > h3')
|
|
_side_info_locator = (by.By.CSS_SELECTOR, 'div.right')
|
|
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset')
|
|
|
|
# private methods
|
|
def __init__(self, driver, src_elem=None, field_mappings=None):
|
|
super(FormRegion, self).__init__(driver, src_elem)
|
|
self.field_mappings = self._prepare_mappings(field_mappings)
|
|
self._init_form_fields()
|
|
|
|
def _prepare_mappings(self, field_mappings):
|
|
if isinstance(field_mappings, tuple):
|
|
return {item: item for item in field_mappings}
|
|
else:
|
|
return field_mappings
|
|
|
|
# protected methods
|
|
def _init_form_fields(self):
|
|
self.fields_src_elem = self._get_element(*self._fields_locator)
|
|
self.FIELDS = self._get_form_fields()
|
|
for accessor_name, accessor_expr in self.field_mappings.items():
|
|
if isinstance(accessor_expr, six.string_types):
|
|
try:
|
|
self._dynamic_properties[accessor_name] = self.FIELDS[accessor_expr]
|
|
except:
|
|
self._dynamic_properties[accessor_name] = None
|
|
else: # it is a class
|
|
self._dynamic_properties[accessor_name] = accessor_expr(
|
|
self.driver)
|
|
|
|
def _get_form_fields(self):
|
|
factory = FieldFactory(self.driver, self.fields_src_elem)
|
|
try:
|
|
self._turn_off_implicit_wait()
|
|
form_fields = {}
|
|
for field in factory.fields():
|
|
if 'ThemableSelectFormFieldRegion' in str(field):
|
|
# ThemableSelectFormFieldRegion is a special SelectFormFieldRegion
|
|
form_fields[field.name] = field
|
|
elif not field.name in form_fields:
|
|
form_fields[field.name] = field
|
|
return form_fields
|
|
finally:
|
|
self._turn_on_implicit_wait()
|
|
|
|
def set_field_values(self, data):
|
|
"""Set fields values
|
|
|
|
data - {field_name: field_value, field_name: field_value ...}
|
|
"""
|
|
for field_name in data:
|
|
field = getattr(self, field_name, None)
|
|
# Field form does not exist
|
|
if field is None:
|
|
raise AttributeError("Unknown form field name.")
|
|
value = data[field_name]
|
|
# if None - default value is left in field
|
|
if value is not None:
|
|
# all text fields
|
|
if hasattr(field, "text"):
|
|
field.text = value
|
|
# file upload field
|
|
elif hasattr(field, "path"):
|
|
field.path = value
|
|
# integers fields
|
|
elif hasattr(field, "value"):
|
|
field.value = value
|
|
|
|
# properties
|
|
@property
|
|
def header(self):
|
|
"""Form header."""
|
|
return self._get_element(*self._header_locator)
|
|
|
|
@property
|
|
def sideinfo(self):
|
|
"""Right part of form, usually contains description."""
|
|
return self._get_element(*self._side_info_locator)
|
|
|
|
@property
|
|
def fields(self):
|
|
"""List of all fields that form contains."""
|
|
return self._get_form_fields()
|
|
|
|
|
|
class TabbedFormRegion(FormRegion):
|
|
"""Forms that are divided with tabs.
|
|
|
|
As example is taken form under the
|
|
Project/Network/Networks/Create Network, on initialization form needs
|
|
to have form field names divided into tuples, that represents the tabs
|
|
and the fields located under them.
|
|
|
|
Usage:
|
|
|
|
form_field_names = (("network_name", "admin_state"),
|
|
("create_subnet", "subnet_name", "network_address",
|
|
"ip_version", "gateway_ip", "disable_gateway"),
|
|
("enable_dhcp", "allocation_pools", "dns_name_servers",
|
|
"host_routes"))
|
|
form = TabbedFormRegion(self.driver, None, form_field_names)
|
|
form.network_name.text = "test_network_name"
|
|
"""
|
|
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary[type=submit]')
|
|
_side_info_locator = (by.By.CSS_SELECTOR, "td.help_text")
|
|
|
|
def __init__(self, driver, field_mappings=None, default_tab=0):
|
|
self.current_tab = default_tab
|
|
super(TabbedFormRegion, self).__init__(
|
|
driver, field_mappings=field_mappings)
|
|
|
|
def _prepare_mappings(self, field_mappings):
|
|
return [super(TabbedFormRegion, self)._prepare_mappings(tab_mappings)
|
|
for tab_mappings in field_mappings]
|
|
|
|
def _init_form_fields(self):
|
|
self.switch_to(self.current_tab)
|
|
|
|
def _init_tab_fields(self, tab_index):
|
|
self.src_elem = self.driver
|
|
fieldsets = self._get_elements(*self._fields_locator)
|
|
self.fields_src_elem = fieldsets[tab_index]
|
|
# self.src_elem = fieldsets[tab_index]
|
|
self.FIELDS = self._get_form_fields()
|
|
current_tab_mappings = self.field_mappings[tab_index]
|
|
for accessor_name, accessor_expr in current_tab_mappings.items():
|
|
if isinstance(accessor_expr, six.string_types):
|
|
self._dynamic_properties[accessor_name] = self.FIELDS[accessor_expr]
|
|
else: # it is a class
|
|
self._dynamic_properties[accessor_name] = accessor_expr(
|
|
self.driver)
|
|
|
|
def switch_to(self, tab_index=0):
|
|
self.tabs.switch_to(index=tab_index)
|
|
self._init_tab_fields(tab_index)
|
|
|
|
@property
|
|
def tabs(self):
|
|
return menus.TabbedMenuRegion(self.driver,
|
|
src_elem=self.src_elem)
|
|
|
|
|
|
class DateFormRegion(BaseFormRegion):
|
|
"""Form that queries data to table that is regularly below the form.
|
|
|
|
A typical example is located on Project/Compute/Overview page.
|
|
"""
|
|
|
|
_from_field_locator = (by.By.CSS_SELECTOR, 'input#id_start')
|
|
_to_field_locator = (by.By.CSS_SELECTOR, 'input#id_end')
|
|
|
|
@property
|
|
def from_date(self):
|
|
return self._get_element(*self._from_field_locator)
|
|
|
|
@property
|
|
def to_date(self):
|
|
return self._get_element(*self._to_field_locator)
|
|
|
|
def query(self, start, end):
|
|
self._set_from_field(start)
|
|
self._set_to_field(end)
|
|
self.submit()
|
|
|
|
def _set_from_field(self, value):
|
|
self._fill_field_element(value, self.from_date)
|
|
|
|
def _set_to_field(self, value):
|
|
self._fill_field_element(value, self.to_date)
|
|
|
|
|
|
class MetadataFormRegion(BaseFormRegion):
|
|
|
|
_input_fields = (by.By.CSS_SELECTOR, 'div.input-group')
|
|
_custom_input_field = (by.By.XPATH, "//input[@name='customItem']")
|
|
_custom_input_button = (by.By.CSS_SELECTOR, 'span.input-group-btn > .btn')
|
|
_submit_locator = (by.By.CSS_SELECTOR, '.modal-footer > .btn.btn-primary')
|
|
_cancel_locator = (by.By.CSS_SELECTOR, '.modal-footer > .btn.btn-default')
|
|
|
|
def _form_getter(self):
|
|
return self.driver.find_element(*self._default_form_locator)
|
|
|
|
@property
|
|
def custom_field_value(self):
|
|
return self._get_element(*self._custom_input_field)
|
|
|
|
@property
|
|
def add_button(self):
|
|
return self._get_element(*self._custom_input_button)
|
|
|
|
def add_custom_field(self, field_name, field_value):
|
|
self.custom_field_value.send_keys(field_name)
|
|
sleep(5)
|
|
self.add_button.click()
|
|
for div in self._get_elements(*self._input_fields):
|
|
if div.text in field_name:
|
|
field = div.find_element(by.By.CSS_SELECTOR, 'input')
|
|
if not hasattr(self, field_name):
|
|
self._dynamic_properties[field_name] = field
|
|
self.set_field_value(field_name, field_value)
|
|
|
|
def set_field_value(self, field_name, field_value):
|
|
if hasattr(self, field_name):
|
|
field = getattr(self, field_name)
|
|
field.send_keys(field_value)
|
|
else:
|
|
raise AttributeError("Unknown form field '{}'.".format(field_name))
|
|
|
|
def wait_till_spinner_disappears(self):
|
|
# No spinner is invoked after the 'Save' button click
|
|
# Will wait till the form itself disappears
|
|
try:
|
|
self.wait_till_element_disappears(self._form_getter)
|
|
except exceptions.StaleElementReferenceException:
|
|
# The form might be absent already by the time the first check
|
|
# occurs. So just suppress the exception here.
|
|
pass
|
|
|
|
|
|
class ItemTextDescription(baseregion.BaseRegion):
|
|
|
|
_separator_locator = (by.By.CSS_SELECTOR, 'dl.dl-horizontal')
|
|
_key_locator = (by.By.CSS_SELECTOR, 'dt')
|
|
_value_locator = (by.By.CSS_SELECTOR, 'dd')
|
|
|
|
def __init__(self, driver, src=None):
|
|
super(ItemTextDescription, self).__init__(driver, src)
|
|
|
|
def get_content(self):
|
|
keys = []
|
|
values = []
|
|
for section in self._get_elements(*self._separator_locator):
|
|
keys.extend([x.text for x in
|
|
section.find_elements(*self._key_locator)])
|
|
values.extend([x.text for x in
|
|
section.find_elements(*self._value_locator)])
|
|
return dict(zip(keys, values))
|