Reduce number of calls to Selenium for form fields
Dynamic form fields storage was significantly simplified, removing most of its 'black magic' part, which included storing properties as methods and binding new property methods to an existing class instance at runtime. Now for every form only the fields at current tab (all fields in case of single-tab form) are being mapped to page regions. Before the refactoring each form field initialization initiated M calls to Selenium, where M is the number of possible field widgets/ page region classes. This lead to total number of calls growing as O(N*M), where N is the number of fields in form. Now the number of calls grows as O(M+N), thanks to the refactoring of FieldFactory class. Implements blueprint: integration-tests-improvements-part1 Change-Id: I3d60041ed8d89b39e3895a90b73d2dec0ff640c5
This commit is contained in:
parent
06eb0ef043
commit
726ea9992f
openstack_dashboard/test/integration_tests
@ -19,7 +19,7 @@ from selenium.webdriver.support import wait
|
||||
|
||||
class BaseWebObject(unittest.TestCase):
|
||||
"""Base class for all web objects."""
|
||||
_spinner_locator = (by.By.CSS_SELECTOR, 'div.modal-backdrop')
|
||||
_spinner_locator = (by.By.CSS_SELECTOR, '.modal-body > .spinner')
|
||||
|
||||
def __init__(self, driver, conf):
|
||||
self.driver = driver
|
||||
|
@ -25,8 +25,9 @@ class FlavorsTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_flavor(self, create_button):
|
||||
create_button.click()
|
||||
return forms.TabbedFormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_FLAVOR_FORM_FIELDS)
|
||||
return forms.TabbedFormRegion(
|
||||
self.driver, self.conf,
|
||||
form_field_names=self.CREATE_FLAVOR_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_flavor(self, delete_button):
|
||||
|
@ -22,8 +22,9 @@ class ProjectsTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_project(self, create_button):
|
||||
create_button.click()
|
||||
return forms.TabbedFormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_PROJECT_FORM_FIELDS)
|
||||
return forms.TabbedFormRegion(
|
||||
self.driver, self.conf,
|
||||
form_field_names=self.CREATE_PROJECT_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_project(self, delete_button):
|
||||
|
@ -23,8 +23,8 @@ class UsersTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_user(self, create_button):
|
||||
create_button.click()
|
||||
return forms.FormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_USER_FORM_FIELDS)
|
||||
return forms.FormRegion(self.driver, self.conf,
|
||||
form_field_names=self.CREATE_USER_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_user(self, delete_button):
|
||||
|
5
openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py
5
openstack_dashboard/test/integration_tests/pages/project/compute/access_and_security/keypairspage.py
@ -25,8 +25,9 @@ class KeypairsTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_keypair(self, create_button):
|
||||
create_button.click()
|
||||
return forms.FormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_KEY_PAIR_FORM_FIELDS)
|
||||
return forms.FormRegion(
|
||||
self.driver, self.conf,
|
||||
form_field_names=self.CREATE_KEY_PAIR_FORM_FIELDS)
|
||||
|
||||
@tables.bind_row_action('delete', primary=True)
|
||||
def delete_keypair(self, delete_button, row):
|
||||
|
@ -22,8 +22,9 @@ class SecurityGroupsTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_group(self, create_button):
|
||||
create_button.click()
|
||||
return forms.FormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_SECURITYGROUP_FORM_FIELDS)
|
||||
return forms.FormRegion(
|
||||
self.driver, self.conf,
|
||||
form_field_names=self.CREATE_SECURITYGROUP_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_group(self, delete_button):
|
||||
|
@ -29,8 +29,8 @@ class ImagesTable(tables.TableRegion):
|
||||
@tables.bind_table_action('create')
|
||||
def create_image(self, create_button):
|
||||
create_button.click()
|
||||
return forms.FormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_IMAGE_FORM_FIELDS)
|
||||
return forms.FormRegion(self.driver, self.conf,
|
||||
form_field_names=self.CREATE_IMAGE_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_image(self, delete_button):
|
||||
|
@ -31,8 +31,9 @@ class InstancesTable(tables.TableRegion):
|
||||
@tables.bind_table_action('launch')
|
||||
def launch_instance(self, launch_button):
|
||||
launch_button.click()
|
||||
return forms.TabbedFormRegion(self.driver, self.conf, None,
|
||||
self.CREATE_INSTANCE_FORM_FIELDS)
|
||||
return forms.TabbedFormRegion(
|
||||
self.driver, self.conf,
|
||||
form_field_names=self.CREATE_INSTANCE_FORM_FIELDS)
|
||||
|
||||
@tables.bind_table_action('delete')
|
||||
def delete_instance(self, delete_button):
|
||||
|
@ -9,8 +9,6 @@
|
||||
# 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 types
|
||||
|
||||
from openstack_dashboard.test.integration_tests import basewebobject
|
||||
|
||||
|
||||
@ -56,69 +54,11 @@ class BaseRegion(basewebobject.BaseWebObject):
|
||||
is created, which is one of the requirement of page object pattern.
|
||||
"""
|
||||
try:
|
||||
return self._dynamic_properties[name]()
|
||||
return self._dynamic_properties[name]
|
||||
except KeyError:
|
||||
msg = "'{0}' object has no attribute '{1}'"
|
||||
raise AttributeError(msg.format(type(self).__name__, name))
|
||||
|
||||
# protected methods and classes
|
||||
class _DynamicProperty(object):
|
||||
"""Serves as new property holder."""
|
||||
|
||||
def __init__(self, method, name=None, id_pattern=None):
|
||||
"""Invocation of `method` should return either single property, or
|
||||
a dictionary of properties, or a list of them.
|
||||
|
||||
In case it's single, neither name, nor id_pattern is required.
|
||||
|
||||
In case it's a dictionary, it's expected that it has a value for
|
||||
the key equal to `name` argument. That's a standard way of
|
||||
fetching a form field).
|
||||
|
||||
In case it's a list, the element with an id equal to the result of
|
||||
`id_pattern % name` is supposed to be there. That's a standard way
|
||||
of fetching a table action (either table-wise or row-wise).
|
||||
"""
|
||||
self.method = method
|
||||
self.name = name
|
||||
self.id_pattern = id_pattern
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
result = self.method()
|
||||
if self.name is None:
|
||||
return result
|
||||
else:
|
||||
if isinstance(result, list) and self.id_pattern is not None:
|
||||
# NOTE(tsufiev): map table actions to action names using
|
||||
# action tag's ids
|
||||
actual_id = self.id_pattern % self.name
|
||||
result = {self.name: entry for entry in result
|
||||
if entry.get_attribute('id') == actual_id}
|
||||
if isinstance(result, dict):
|
||||
return result[self.name]
|
||||
return result
|
||||
|
||||
def _init_dynamic_properties(self, new_attr_names, method,
|
||||
id_pattern=None):
|
||||
"""Create new object's 'properties' at runtime."""
|
||||
for new_attr_name in new_attr_names:
|
||||
self._init_dynamic_property(new_attr_name, method, id_pattern)
|
||||
|
||||
def _init_dynamic_property(self, new_attr_name, method, id_pattern=None):
|
||||
"""Create new object's property at runtime. See _DynamicProperty's
|
||||
__init__ docstring for a description of arguments.
|
||||
"""
|
||||
if (new_attr_name in dir(self) or
|
||||
new_attr_name in self._dynamic_properties):
|
||||
raise AttributeError("%s class has already attribute %s."
|
||||
"The new property could not be "
|
||||
"created." % (self.__class__.__name__,
|
||||
new_attr_name))
|
||||
new_method = self.__class__._DynamicProperty(
|
||||
method, new_attr_name, id_pattern)
|
||||
inst_method = types.MethodType(new_method, self)
|
||||
self._dynamic_properties[new_attr_name] = inst_method
|
||||
|
||||
def _get_element(self, *locator):
|
||||
return self.src_elem.find_element(*locator)
|
||||
|
||||
|
@ -15,7 +15,6 @@ from selenium.webdriver.common import by
|
||||
import selenium.webdriver.support.ui as Support
|
||||
|
||||
from openstack_dashboard.test.integration_tests.regions import baseregion
|
||||
from openstack_dashboard.test.integration_tests.regions import exceptions
|
||||
from openstack_dashboard.test.integration_tests.regions import menus
|
||||
|
||||
|
||||
@ -23,12 +22,19 @@ class FieldFactory(baseregion.BaseRegion):
|
||||
"""Factory for creating form field objects."""
|
||||
|
||||
FORM_FIELDS_TYPES = set()
|
||||
_element_locator_str_prefix = 'div.form-group'
|
||||
|
||||
def make_form_field(self):
|
||||
for form_type in self.FORM_FIELDS_TYPES:
|
||||
if self._is_element_present(*form_type._element_locator):
|
||||
return form_type(self.driver, self.conf, self.src_elem)
|
||||
raise exceptions.UnknownFormFieldTypeException()
|
||||
def __init__(self, driver, conf, src_elem=None):
|
||||
super(FieldFactory, self).__init__(driver, conf, 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, self.conf, element)
|
||||
|
||||
@classmethod
|
||||
def register_field_cls(cls, field_class, base_classes=None):
|
||||
@ -62,7 +68,7 @@ class BaseFormFieldRegion(baseregion.BaseRegion):
|
||||
|
||||
@property
|
||||
def element(self):
|
||||
return self._get_element(*self._element_locator)
|
||||
return self.src_elem
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -92,21 +98,19 @@ class CheckBoxMixin(object):
|
||||
class CheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin):
|
||||
"""Checkbox field."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR,
|
||||
'label > input[type=checkbox]')
|
||||
_element_locator_str_suffix = 'label > input[type=checkbox]'
|
||||
|
||||
|
||||
class ProjectPageCheckBoxFormFieldRegion(BaseFormFieldRegion, CheckBoxMixin):
|
||||
"""Checkbox field for Project-page."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR,
|
||||
'div > input[type=checkbox]')
|
||||
_element_locator_str_suffix = 'div > input[type=checkbox]'
|
||||
|
||||
|
||||
class ChooseFileFormFieldRegion(BaseFormFieldRegion):
|
||||
"""Choose file field."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=file]')
|
||||
_element_locator_str_suffix = 'div > input[type=file]'
|
||||
|
||||
def choose(self, path):
|
||||
self.element.send_keys(path)
|
||||
@ -128,14 +132,14 @@ class BaseTextFormFieldRegion(BaseFormFieldRegion):
|
||||
class TextInputFormFieldRegion(BaseTextFormFieldRegion):
|
||||
"""Text input box."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=text],'
|
||||
'div > input[type=None]')
|
||||
_element_locator_str_suffix = \
|
||||
'div > input[type=text], div > input[type=None]'
|
||||
|
||||
|
||||
class FileInputFormFieldRegion(BaseFormFieldRegion):
|
||||
"""Text input box."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=file]')
|
||||
_element_locator_str_suffix = 'div > input[type=file]'
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
@ -151,25 +155,25 @@ class FileInputFormFieldRegion(BaseFormFieldRegion):
|
||||
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
|
||||
"""Password text input box."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=password]')
|
||||
_element_locator_str_suffix = 'div > input[type=password]'
|
||||
|
||||
|
||||
class EmailInputFormFieldRegion(BaseTextFormFieldRegion):
|
||||
"""Email text input box."""
|
||||
|
||||
_element_locator = (by.By.ID, 'id_email')
|
||||
_element_locator_str_suffix = 'div > input[type=email]'
|
||||
|
||||
|
||||
class TextAreaFormFieldRegion(BaseTextFormFieldRegion):
|
||||
"""Multi-line text input box."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > textarea')
|
||||
_element_locator_str_suffix = 'div > textarea'
|
||||
|
||||
|
||||
class IntegerFormFieldRegion(BaseFormFieldRegion):
|
||||
"""Integer input box."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=number]')
|
||||
_element_locator_str_suffix = 'div > input[type=number]'
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
@ -183,15 +187,14 @@ class IntegerFormFieldRegion(BaseFormFieldRegion):
|
||||
class SelectFormFieldRegion(BaseFormFieldRegion):
|
||||
"""Select box field."""
|
||||
|
||||
_element_locator = (by.By.CSS_SELECTOR, 'div > select')
|
||||
_element_locator_str_suffix = 'div > select'
|
||||
|
||||
def is_displayed(self):
|
||||
return self.element._el.is_displayed()
|
||||
|
||||
@property
|
||||
def element(self):
|
||||
select = self._get_element(*self._element_locator)
|
||||
return Support.Select(select)
|
||||
return Support.Select(self.src_elem)
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
@ -259,31 +262,27 @@ class FormRegion(BaseFormRegion):
|
||||
|
||||
_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 > div.form-group')
|
||||
_fields_locator = (by.By.CSS_SELECTOR, 'fieldset')
|
||||
|
||||
# private methods
|
||||
def __init__(self, driver, conf, src_elem, form_field_names):
|
||||
def __init__(self, driver, conf, src_elem=None, form_field_names=None):
|
||||
super(FormRegion, self).__init__(driver, conf, src_elem)
|
||||
self.form_field_names = form_field_names
|
||||
self.wait_till_spinner_disappears()
|
||||
self._init_form_fields()
|
||||
|
||||
# protected methods
|
||||
def _init_form_fields(self):
|
||||
self._init_dynamic_properties(self.form_field_names,
|
||||
self._get_form_fields)
|
||||
self.fields_src_elem = self._get_element(*self._fields_locator)
|
||||
self._dynamic_properties.update(self._get_form_fields())
|
||||
|
||||
def _get_form_fields(self):
|
||||
fields_els = self._get_elements(*self._fields_locator)
|
||||
form_fields = {}
|
||||
factory = FieldFactory(self.driver, self.conf, self.fields_src_elem)
|
||||
try:
|
||||
self._turn_off_implicit_wait()
|
||||
for elem in fields_els:
|
||||
field_factory = FieldFactory(self.driver, self.conf, elem)
|
||||
field = field_factory.make_form_field()
|
||||
form_fields[field.name] = field
|
||||
return {field.name: field for field in factory.fields()}
|
||||
finally:
|
||||
self._turn_on_implicit_wait()
|
||||
return form_fields
|
||||
|
||||
def set_field_values(self, data):
|
||||
"""Set fields values
|
||||
@ -346,34 +345,24 @@ class TabbedFormRegion(FormRegion):
|
||||
|
||||
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary[type=submit]')
|
||||
_side_info_locator = (by.By.CSS_SELECTOR, "td.help_text")
|
||||
_fields_locator = (by.By.CSS_SELECTOR, "div.form-group")
|
||||
|
||||
class GetFieldsMethod(object):
|
||||
def __init__(self, driver, conf, form_field_names=None, default_tab=0):
|
||||
self.current_tab = default_tab
|
||||
super(TabbedFormRegion, self).__init__(
|
||||
driver, conf, form_field_names=form_field_names)
|
||||
|
||||
def __init__(self, get_fields_method, tab_index, switch_tab_method):
|
||||
self.get_fields = get_fields_method
|
||||
self.tab_index = tab_index
|
||||
self.switch_to_tab = switch_tab_method
|
||||
def _init_form_fields(self):
|
||||
self._init_tab_fields(self.current_tab)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.switch_to_tab(self.tab_index)
|
||||
fields = self.get_fields()
|
||||
if isinstance(fields, dict):
|
||||
return {key: field for (key, field)
|
||||
in six.iteritems(fields) if field.is_displayed()}
|
||||
else:
|
||||
return [field for field in fields if field.is_displayed()]
|
||||
def _init_tab_fields(self, tab_index):
|
||||
fieldsets = self._get_elements(*self._fields_locator)
|
||||
self.fields_src_elem = fieldsets[tab_index]
|
||||
self._dynamic_properties.update(self._get_form_fields())
|
||||
|
||||
@property
|
||||
def tabs(self):
|
||||
return menus.TabbedMenuRegion(self.driver, self.conf)
|
||||
|
||||
def _init_form_fields(self):
|
||||
for index, tab_names in enumerate(self.form_field_names):
|
||||
get_fields = self.GetFieldsMethod(self._get_form_fields, index,
|
||||
self.tabs.switch_to)
|
||||
self._init_dynamic_properties(tab_names, get_fields)
|
||||
|
||||
|
||||
class DateFormRegion(BaseFormRegion):
|
||||
"""Form that queries data to table that is regularly below the form,
|
||||
@ -394,7 +383,7 @@ class DateFormRegion(BaseFormRegion):
|
||||
def query(self, start, end):
|
||||
self._set_from_field(start)
|
||||
self._set_to_field(end)
|
||||
self.submit.click()
|
||||
self.submit()
|
||||
|
||||
def _set_from_field(self, value):
|
||||
self._fill_field_element(value, self.from_date)
|
||||
|
Loading…
x
Reference in New Issue
Block a user