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:
Timur Sufiev 2015-12-23 14:07:12 +03:00
parent 06eb0ef043
commit 726ea9992f
10 changed files with 64 additions and 130 deletions
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):

@ -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)