Add form regions to integration tests

I have implemented some new form regions and used them in currently
existing pageobjects. I also created method for switching on/off
web driver implicit wait.

* Form has automatic field discovery and form's field properties are
  initialized on the form object initialization. One must submit tuple
  of field names that must be submited in the same order as in the page
  html code.

* All newly created field regions are added automaticaly to FieldFactory
  field list.

* Forms can be initialized without specifying the source element, as a
  result it will be assumed that source element shoud be found according to
  default locator (as it is quite common that forms are located under
  this locator)

Partially implements blueprint: selenium-integration-testing

Change-Id: Ic954ea1829135efdc5dd6a34840de5045b4a7a56
This commit is contained in:
Tomas Novacik
2014-07-24 14:37:55 +02:00
committed by Tomáš Nováčik
parent b4b0e9c677
commit 3d6cb82646
9 changed files with 268 additions and 134 deletions

View File

@@ -52,3 +52,9 @@ class BaseWebObject(object):
def _select_dropdown_by_value(self, value, element):
select = Support.Select(element)
select.select_by_value(value)
def _turn_off_implicit_wait(self):
self.driver.implicitly_wait(0)
def _turn_on_implicit_wait(self):
self.driver.implicitly_wait(self.conf.dashboard.page_timeout)

View File

@@ -16,62 +16,54 @@
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.regions import forms
from openstack_dashboard.test.integration_tests.regions import tables
class KeypairPage(basepage.BasePage):
_keypair_create_button_locator = (by.By.CSS_SELECTOR,
'#keypairs__action_create')
_keypair_name_field_locator = (by.By.CSS_SELECTOR, '#id_name')
_keypair_submit_button_locator = (by.By.CSS_SELECTOR,
'.btn.btn-primary.pull-right')
_keypair_delete_cnf_button_locator = (by.By.CSS_SELECTOR,
'.btn.btn-primary')
_key_pairs_table_locator = (by.By.CSS_SELECTOR, 'table#keypairs')
KEY_PAIRS_TABLE_ACTIONS = ("create_key_pair", "import_key_pair",
"delete_key_pair")
KEY_PAIRS_TABLE_ROW_ACTION = "delete_key_pair"
KEY_PAIRS_TABLE_NAME_COLUMN_INDEX = 0
CREATE_KEY_PAIR_FORM_FIELDS = ('name',)
def __init__(self, driver, conf):
super(KeypairPage, self).__init__(driver, conf)
self._page_title = "Access & Security"
@property
def keypair_create(self):
return self._get_element(*self._keypair_create_button_locator)
def _get_row_with_keypair_name(self, name):
return self.keypairs_table.get_row(
self.KEY_PAIRS_TABLE_NAME_COLUMN_INDEX, name)
@property
def keypair_name_field(self):
return self._get_element(*self._keypair_name_field_locator)
def keypairs_table(self):
src_elem = self._get_element(*self._key_pairs_table_locator)
return tables.SimpleActionsTableRegion(self.driver, self.conf,
src_elem,
self.KEY_PAIRS_TABLE_ACTIONS,
self.KEY_PAIRS_TABLE_ROW_ACTION)
@property
def keypair_submit_button(self):
return self._get_element(*self._keypair_submit_button_locator)
def create_keypair_form(self):
return forms.FormRegion(self.driver, self.conf, None,
self.CREATE_KEY_PAIR_FORM_FIELDS)
@property
def keypair_delete_cnf_button(self):
return self._get_element(*self._keypair_delete_cnf_button_locator)
def delete_keypair_form(self):
return forms.BaseFormRegion(self.driver, self.conf, None)
def _click_on_keypair_create(self):
self.keypair_create.click()
def _click_on_keypair_submit_button(self):
self.keypair_submit_button.click()
def _click_on_keypair_delete_cnf_button(self):
self.keypair_delete_cnf_button.click()
def get_keypair_status(self, keypair_name):
keypair_locator = (by.By.CSS_SELECTOR,
'#keypairs__row__%s' % keypair_name)
keypair_status = self._is_element_present(*keypair_locator)
return keypair_status
def is_keypair_present(self, name):
return bool(self._get_row_with_keypair_name(name))
def create_keypair(self, keypair_name):
self._click_on_keypair_create()
self.keypair_name_field.send_keys(keypair_name)
self._click_on_keypair_submit_button()
self.keypairs_table.create_key_pair.click()
self.create_keypair_form.name.text = keypair_name
self.create_keypair_form.submit.click()
def delete_keypair(self, keypair_name):
keypair_delete_check_locator = (
by.By.CSS_SELECTOR,
"#keypairs__row_%s__action_delete" % keypair_name)
self.driver.find_element(
*keypair_delete_check_locator).click()
self._click_on_keypair_delete_cnf_button()
def delete_keypair(self, name):
self._get_row_with_keypair_name(name).delete_key_pair.click()
self.delete_keypair_form.submit.click()

View File

@@ -13,24 +13,28 @@
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages import pageobject
from openstack_dashboard.test.integration_tests.regions import forms
class ChangePasswordPage(basepage.BasePage):
_password_form_locator = (by.By.CSS_SELECTOR,
'div#change_password_modal')
CHANGE_PASSWORD_FORM_FIELDS = ("current_password", "new_password",
"confirm_new_password")
@property
def modal(self):
return ChangePasswordPage.ChangePasswordModal(self.driver,
self.conf)
def password_form(self):
src_elem = self._get_element(*self._password_form_locator)
return forms.FormRegion(self.driver, self.conf, src_elem,
self.CHANGE_PASSWORD_FORM_FIELDS)
def change_password(self, current, new):
self._fill_field_element(
current, self.modal.current_password)
self._fill_field_element(
new, self.modal.new_password)
self._fill_field_element(
new, self.modal.confirm_new_password)
self.modal.click_on_change_button()
self.password_form.current_password.text = current
self.password_form.new_password.text = new
self.password_form.confirm_new_password.text = new
self.password_form.submit.click()
def reset_to_default_password(self, current):
if self.topbar.user.text == self.conf.identity.admin_username:
@@ -39,32 +43,3 @@ class ChangePasswordPage(basepage.BasePage):
else:
return self.change_password(current,
self.conf.identity.password)
class ChangePasswordModal(pageobject.PageObject):
_current_password_locator = (by.By.CSS_SELECTOR,
'input#id_current_password')
_new_password_locator = (by.By.CSS_SELECTOR,
'input#id_new_password')
_confirm_new_password_locator = (by.By.CSS_SELECTOR,
'input#id_confirm_password')
_change_submit_button_locator = (by.By.CSS_SELECTOR,
'div.modal-footer button.btn')
@property
def current_password(self):
return self._get_element(*self._current_password_locator)
@property
def new_password(self):
return self._get_element(*self._new_password_locator)
@property
def confirm_new_password(self):
return self._get_element(*self._confirm_new_password_locator)
@property
def change_button(self):
return self._get_element(*self._change_submit_button_locator)
def click_on_change_button(self):
self.change_button.click()

View File

@@ -13,9 +13,9 @@
from selenium.webdriver.common import by
from openstack_dashboard.test.integration_tests.pages import basepage
from openstack_dashboard.test.integration_tests.pages import pageobject
from openstack_dashboard.test.integration_tests.pages.settings import \
changepasswordpage
from openstack_dashboard.test.integration_tests.regions import forms
class SettingsPage(basepage.BasePage):
@@ -28,6 +28,9 @@ class SettingsPage(basepage.BasePage):
"pagesize": DEFAULT_PAGESIZE
}
SETTINGS_FORM_FIELDS = ("language", "timezone", "pagesize")
_settings_form_locator = (by.By.CSS_SELECTOR, 'div#user_settings_modal')
_change_password_tab_locator = (by.By.CSS_SELECTOR,
'a[href*="/settings/password/"]')
@@ -36,8 +39,10 @@ class SettingsPage(basepage.BasePage):
self._page_title = "User Settings"
@property
def modal(self):
return SettingsPage.UserSettingsModal(self.driver, self.conf)
def settings_form(self):
src_elem = self._get_element(*self._settings_form_locator)
return forms.FormRegion(self.driver, self.conf, src_elem,
self.SETTINGS_FORM_FIELDS)
@property
def changepassword(self):
@@ -48,18 +53,16 @@ class SettingsPage(basepage.BasePage):
return self._get_element(*self._change_password_tab_locator)
def change_language(self, lang=DEFAULT_LANGUAGE):
self._select_dropdown_by_value(lang,
self.modal.language_selection)
self.modal.click_on_save_button()
self.settings_form.language.value = lang
self.settings_form.submit.click()
def change_timezone(self, timezone=DEFAULT_TIMEZONE):
self._select_dropdown_by_value(timezone,
self.modal.timezone_selection)
self.modal.click_on_save_button()
self.settings_form.timezone.value = timezone
self.settings_form.submit.click()
def change_pagesize(self, size=DEFAULT_PAGESIZE):
self._fill_field_element(size, self.modal.pagesize)
self.modal.click_on_save_button()
self.settings_form.pagesize.value = size
self.settings_form.submit.click()
def return_to_default_settings(self):
self.change_language()
@@ -69,32 +72,3 @@ class SettingsPage(basepage.BasePage):
def go_to_change_password_page(self):
self.change_password_tab.click()
return changepasswordpage.ChangePasswordPage(self.driver, self.conf)
class UserSettingsModal(pageobject.PageObject):
_language_selection_locator = (by.By.CSS_SELECTOR,
'select#id_language')
_timezone_selection_locator = (by.By.CSS_SELECTOR,
'select#id_timezone')
_items_per_page_input_locator = (by.By.CSS_SELECTOR,
'input#id_pagesize')
_save_submit_button_locator = (by.By.CSS_SELECTOR,
'div.modal-footer button.btn')
@property
def language_selection(self):
return self._get_element(*self._language_selection_locator)
@property
def timezone_selection(self):
return self._get_element(*self._timezone_selection_locator)
@property
def pagesize(self):
return self._get_element(*self._items_per_page_input_locator)
@property
def save_button(self):
return self._get_element(*self._save_submit_button_locator)
def click_on_save_button(self):
self.save_button.click()

View File

@@ -0,0 +1,22 @@
# 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.
class BaseRegionException(Exception):
"""Base exception class for region module."""
pass
class UnknownFormFieldTypeException(BaseRegionException):
def __str__(self):
return "No FormField class matched the scope of web content."

View File

@@ -9,13 +9,46 @@
# 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 six
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
class FieldFactory(baseregion.BaseRegion):
"""Factory for creating form field objects."""
FORM_FIELDS_TYPES = set()
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()
@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."""
@@ -35,14 +68,150 @@ class BaseFormFieldRegion(baseregion.BaseRegion):
return 'required' in classes
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 = (by.By.CSS_SELECTOR, 'div > input[type=text]')
class PasswordInputFormFieldRegion(BaseTextFormFieldRegion):
"""Password text input box."""
_element_locator = (by.By.CSS_SELECTOR, 'div > input[type=password]')
class IntegerFormFieldRegion(BaseFormFieldRegion):
"""Integer input box."""
_element_locator = (by.By.CSS_SELECTOR, 'div > 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 = (by.By.CSS_SELECTOR, 'div > select')
@property
def element(self):
select = self._get_element(*self._element_locator)
return Support.Select(select)
@property
def values(self):
results = []
for option in self.element.all_selected_options:
results.append(option.get_attribute('value'))
return results
@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 BaseFormRegion(baseregion.BaseRegion):
"""Base class for forms."""
_submit_locator = (by.By.CSS_SELECTOR, 'button.btn.btn-primary,'
' a.btn.btn-primary')
_submit_locator = (by.By.CSS_SELECTOR, '*.btn.btn-primary')
_cancel_locator = (by.By.CSS_SELECTOR, '*.btn.cancel')
_default_form_locator = (by.By.CSS_SELECTOR, 'div.modal-dialog')
def __init__(self, driver, conf, 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
src_elem = self._get_element(*self._default_form_locator)
super(BaseFormRegion, self).__init__(driver, conf, src_elem)
@property
def submit(self):
self._get_element(*self._submit_locator).click()
return self._get_element(*self._submit_locator)
@property
def cancel(self):
return self._get_element(*self._cancel_locator)
class FormRegion(BaseFormRegion):
"""Standard form."""
_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')
# private methods
def __init__(self, driver, conf, src_elem, form_field_names):
super(self.__class__, self).__init__(driver, conf, src_elem)
self.form_field_names = form_field_names
self._init_form_fields()
# protected methods
def _init_form_fields(self):
self._init_dynamic_properties(self.form_field_names,
self._get_form_fields)
def _get_form_fields(self):
fields_els = self._get_elements(*self._fields_locator)
form_fields = []
try:
self._turn_off_implicit_wait()
for elem in fields_els:
field_factory = FieldFactory(self.driver, self.conf, elem)
form_fields.append(field_factory.make_form_field())
finally:
self._turn_on_implicit_wait()
return form_fields
# 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 DateFormRegion(BaseFormRegion):
@@ -64,7 +233,7 @@ class DateFormRegion(BaseFormRegion):
def query(self, start, end):
self._set_from_field(start)
self._set_to_field(end)
self.submit()
self.submit.click()
def _set_from_field(self, value):
self._fill_field_element(value, self.from_date)

View File

@@ -29,7 +29,7 @@ class TestKeypair(helpers.TestCase):
keypair_page.create_keypair(self.KEYPAIR_NAME)
accesssecurity_page = self.home_pg.go_to_accesssecurity_page()
keypair_page = accesssecurity_page.go_to_keypair_page()
self.assertTrue(keypair_page.get_keypair_status(self.KEYPAIR_NAME))
self.assertTrue(keypair_page.is_keypair_present(self.KEYPAIR_NAME))
keypair_page.delete_keypair(self.KEYPAIR_NAME)
self.assertFalse(keypair_page.get_keypair_status(self.KEYPAIR_NAME))
self.assertFalse(keypair_page.is_keypair_present(self.KEYPAIR_NAME))

View File

@@ -21,7 +21,6 @@ class TestPasswordChange(helpers.TestCase):
"""Changes the password, verifies it was indeed changed and resets to
default password.
"""
settings_page = self.home_pg.go_to_settings_page()
passwordchange_page = settings_page.go_to_change_password_page()

View File

@@ -16,12 +16,9 @@ from openstack_dashboard.test.integration_tests import helpers
class TestUserSettings(helpers.TestCase):
def verify_user_settings_change(self, changed_settings):
language = self.settings_page.modal.language_selection.\
get_attribute("value")
timezone = self.settings_page.modal.timezone_selection.\
get_attribute("value")
pagesize = self.settings_page.modal.pagesize.\
get_attribute("value")
language = self.settings_page.settings_form.language.value
timezone = self.settings_page.settings_form.timezone.value
pagesize = self.settings_page.settings_form.pagesize.value
user_settings = (("Language", changed_settings["language"], language),
("Timezone", changed_settings["timezone"], timezone),