diff --git a/common/test/acceptance/pages/lms/account_settings.py b/common/test/acceptance/pages/lms/account_settings.py new file mode 100644 index 0000000000..4e55e8479c --- /dev/null +++ b/common/test/acceptance/pages/lms/account_settings.py @@ -0,0 +1,61 @@ +""" +Base class for account settings page. +""" +from . import BASE_URL + +from bok_choy.page_object import PageObject +from bok_choy.promise import EmptyPromise + +from .fields import FieldsMixin + + +class AccountSettingsPage(FieldsMixin, PageObject): + """ + Tests for Account Settings Page. + """ + + url = "{base}/{settings}".format(base=BASE_URL, settings='account/settings') + + def is_browser_on_page(self): + return 'Account Settings' in self.browser.title + + def sections_structure(self): + """ + Return list of section titles and field titles for each section. + + Example: [ + { + 'title': 'Section Title' + 'fields': ['Field 1 title', 'Field 2 title',...] + }, + ... + ] + """ + self.wait_for_ajax() + + structure = [] + + sections = self.q(css='.section') + for section in sections: + section_title_element = section.find_element_by_class_name('section-header') + field_title_elements = section.find_elements_by_class_name('u-field-title') + + structure.append({ + 'title': section_title_element.text, + 'fields': [element.text for element in field_title_elements], + }) + + return structure + + def _is_loading_in_progress(self): + """ + Check if loading indicator is visible. + """ + query = self.q(css='.ui-loading-indicator') + return query.present and 'is-hidden' not in query.attrs('class')[0].split() + + def wait_for_loading_indicator(self): + """ + Wait for loading indicator to become visible. + """ + EmptyPromise(self._is_loading_in_progress, "Loading is in progress.").fulfill() diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index e1954d13f2..10958a34d9 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -183,3 +183,22 @@ class DashboardPage(PageObject): def get_course_social_sharing_widget(self, widget_name): """ Retrieves the specified social sharing widget by its classification """ return self.q(css='a.action-{}'.format(widget_name)) + + def click_username_dropdown(self): + """ + Click username dropdown. + """ + self.q(css='.dropdown').first.click() + + @property + def username_dropdown_link_text(self): + """ + Return list username dropdown links. + """ + return self.q(css='.dropdown-menu li a').text + + def click_account_settings_link(self): + """ + Click on `Account Settings` link. + """ + self.q(css='.dropdown-menu li a').first.click() diff --git a/common/test/acceptance/pages/lms/fields.py b/common/test/acceptance/pages/lms/fields.py new file mode 100644 index 0000000000..62124ea4f1 --- /dev/null +++ b/common/test/acceptance/pages/lms/fields.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" +Mixins for fields. +""" +from bok_choy.promise import EmptyPromise + +from ...tests.helpers import get_selected_option_text, select_option_by_text + + +class FieldsMixin(object): + """ + Methods for testing fields in pages. + """ + + def field(self, field_id): + """ + Return field with field_id. + """ + self.wait_for_ajax() + + query = self.q(css='.u-field-{}'.format(field_id)) + return query.text[0] if query.present else None + + def wait_for_field(self, field_id): + """ + Wait for a field to appear in DOM. + """ + EmptyPromise( + lambda: self.field(field_id) is not None, + "Field with id \"{0}\" is in DOM.".format(field_id) + ).fulfill() + + def title_for_field(self, field_id): + """ + Return the title of a field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} .u-field-title'.format(field_id)) + return query.text[0] if query.present else None + + def message_for_field(self, field_id): + """ + Return the current message in a field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} .u-field-message'.format(field_id)) + return query.text[0] if query.present else None + + def wait_for_messsage(self, field_id, message): + """ + Wait for a message to appear in a field. + """ + EmptyPromise( + lambda: message in (self.message_for_field(field_id) or ''), + "Messsage \"{0}\" is visible.".format(message) + ).fulfill() + + def indicator_for_field(self, field_id): + """ + Return the name of the current indicator in a field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} .u-field-message i'.format(field_id)) + return [ + class_name for class_name + in query.attrs('class')[0].split(' ') + if class_name.startswith('message') + ][0].partition('-')[2] if query.present else None + + def wait_for_indicator(self, field_id, indicator): + """ + Wait for an indicator to appear in a field. + """ + EmptyPromise( + lambda: indicator == self.indicator_for_field(field_id), + "Indicator \"{0}\" is visible.".format(self.indicator_for_field(field_id)) + ).fulfill() + + def value_for_readonly_field(self, field_id): + """ + Return the value in a readonly field. + """ + self.wait_for_field(field_id) + + return self.value_for_text_field(field_id) + + def value_for_text_field(self, field_id, value=None): + """ + Get or set the value of a text field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} input'.format(field_id)) + if not query.present: + return None + + if value is not None: + current_value = query.attrs('value')[0] + query.results[0].send_keys(u'\ue003' * len(current_value)) # Delete existing value. + query.results[0].send_keys(value) # Input new value + query.results[0].send_keys(u'\ue007') # Press Enter + return query.attrs('value')[0] + + def value_for_dropdown_field(self, field_id, value=None): + """ + Get or set the value in a dropdown field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} select'.format(field_id)) + if not query.present: + return None + + if value is not None: + select_option_by_text(query, value) + return get_selected_option_text(query) + + def link_title_for_link_field(self, field_id): + """ + Return the title of the link in a link field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} a'.format(field_id)) + return query.text[0] if query.present else None + + def click_on_link_in_link_field(self, field_id): + """ + Click the link in a link field. + """ + self.wait_for_field(field_id) + + query = self.q(css='.u-field-{} a'.format(field_id)) + if query.present: + query.first.click() diff --git a/common/test/acceptance/tests/lms/test_account_settings.py b/common/test/acceptance/tests/lms/test_account_settings.py new file mode 100644 index 0000000000..4558171360 --- /dev/null +++ b/common/test/acceptance/tests/lms/test_account_settings.py @@ -0,0 +1,256 @@ +# -*- coding: utf-8 -*- +""" +End-to-end tests for the Account Settings page. +""" +from unittest import skip + +from bok_choy.web_app_test import WebAppTest + +from ...pages.lms.account_settings import AccountSettingsPage +from ...pages.lms.auto_auth import AutoAuthPage +from ...pages.lms.dashboard import DashboardPage + + +class AccountSettingsPageTest(WebAppTest): + """ + Tests that verify behaviour of the Account Settings page. + """ + + SUCCESS_MESSAGE = 'Your changes have been saved.' + USERNAME = "test" + PASSWORD = "testpass" + EMAIL = "test@example.com" + + def setUp(self): + """ + Initialize account and pages. + """ + super(AccountSettingsPageTest, self).setUp() + + AutoAuthPage(self.browser, username=self.USERNAME, password=self.PASSWORD, email=self.EMAIL).visit() + + self.account_settings_page = AccountSettingsPage(self.browser) + self.account_settings_page.visit() + + def test_link_on_dashboard_works(self): + """ + Scenario: Verify that account settings link is present in dashboard + page and we can use it to navigate to the page. + """ + dashboard_page = DashboardPage(self.browser) + dashboard_page.visit() + dashboard_page.click_username_dropdown() + self.assertIn('Account Settings', dashboard_page.username_dropdown_link_text) + dashboard_page.click_account_settings_link() + + def test_all_sections_and_fields_are_present(self): + """ + Scenario: Verify that all sections and fields are present on the page. + """ + expected_sections_structure = [ + { + 'title': 'Basic Account Information (required)', + 'fields': [ + 'Username', + 'Full Name', + 'Email Address', + 'Password', + 'Language', + ] + }, + { + 'title': 'Additional Information (optional)', + 'fields': [ + 'Education Completed', + 'Gender', + 'Year of Birth', + 'Country or Region', + 'Preferred Language', + ] + }, + { + 'title': 'Connected Accounts', + 'fields': [ + 'Facebook', + 'Google', + ] + } + ] + + self.assertEqual(self.account_settings_page.sections_structure(), expected_sections_structure) + + def _test_readonly_field(self, field_id, title, value): + """ + Test behavior of a readonly field. + """ + self.assertEqual(self.account_settings_page.title_for_field(field_id), title) + self.assertEqual(self.account_settings_page.value_for_readonly_field(field_id), value) + + def _test_text_field( + self, field_id, title, initial_value, new_invalid_value, new_valid_values, success_message=SUCCESS_MESSAGE, + assert_after_reload=True + ): + """ + Test behaviour of a text field. + """ + self.assertEqual(self.account_settings_page.title_for_field(field_id), title) + self.assertEqual(self.account_settings_page.value_for_text_field(field_id), initial_value) + + self.assertEqual( + self.account_settings_page.value_for_text_field(field_id, new_invalid_value), new_invalid_value + ) + self.account_settings_page.wait_for_indicator(field_id, 'validation-error') + self.browser.refresh() + self.assertNotEqual(self.account_settings_page.value_for_text_field(field_id), new_invalid_value) + + for new_value in new_valid_values: + self.assertEqual(self.account_settings_page.value_for_text_field(field_id, new_value), new_value) + self.account_settings_page.wait_for_messsage(field_id, success_message) + if assert_after_reload: + self.browser.refresh() + self.assertEqual(self.account_settings_page.value_for_text_field(field_id), new_value) + + def _test_dropdown_field( + self, field_id, title, initial_value, new_values, success_message=SUCCESS_MESSAGE, reloads_on_save=False + ): + """ + Test behaviour of a dropdown field. + """ + self.assertEqual(self.account_settings_page.title_for_field(field_id), title) + self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id), initial_value) + + for new_value in new_values: + self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id, new_value), new_value) + self.account_settings_page.wait_for_messsage(field_id, success_message) + if reloads_on_save: + self.account_settings_page.wait_for_loading_indicator() + else: + self.browser.refresh() + self.assertEqual(self.account_settings_page.value_for_dropdown_field(field_id), new_value) + + def _test_link_field(self, field_id, title, link_title, success_message): + """ + Test behaviour a link field. + """ + self.assertEqual(self.account_settings_page.title_for_field(field_id), title) + self.assertEqual(self.account_settings_page.link_title_for_link_field(field_id), link_title) + self.account_settings_page.click_on_link_in_link_field(field_id) + self.account_settings_page.wait_for_messsage(field_id, success_message) + + def test_username_field(self): + """ + Test behaviour of "Username" field. + """ + self._test_readonly_field( + 'username', + 'Username', + self.USERNAME, + ) + + def test_full_name_field(self): + """ + Test behaviour of "Full Name" field. + """ + self._test_text_field( + u'name', + u'Full Name', + self.USERNAME, + u'@', + [u'another name', self.USERNAME], + ) + + def test_email_field(self): + """ + Test behaviour of "Email" field. + """ + self._test_text_field( + u'email', + u'Email Address', + self.EMAIL, + u'@', + [u'me@here.com', u'you@there.com'], + success_message='Click the link in the message to update your email address.', + assert_after_reload=False + ) + + def test_password_field(self): + """ + Test behaviour of "Password" field. + """ + self._test_link_field( + u'password', + u'Password', + u'Reset Password', + success_message='Click the link in the message to reset your password.', + ) + + @skip( + 'On bokchoy test servers, language changes take a few reloads to fully realize ' + 'which means we can no longer reliably match the strings in the html in other tests.' + ) + def test_language_field(self): + """ + Test behaviour of "Language" field. + """ + self._test_dropdown_field( + u'pref-lang', + u'Language', + u'English', + [u'Dummy Language (Esperanto)', u'English'], + reloads_on_save=True, + ) + + def test_education_completed_field(self): + """ + Test behaviour of "Education Completed" field. + """ + self._test_dropdown_field( + u'level_of_education', + u'Education Completed', + u'', + [u'Bachelor\'s degree', u''], + ) + + def test_gender_field(self): + """ + Test behaviour of "Gender" field. + """ + self._test_dropdown_field( + u'gender', + u'Gender', + u'', + [u'Female', u''], + ) + + def test_year_of_birth_field(self): + """ + Test behaviour of "Year of Birth" field. + """ + self._test_dropdown_field( + u'year_of_birth', + u'Year of Birth', + u'', + [u'1980', u''], + ) + + def test_country_field(self): + """ + Test behaviour of "Country or Region" field. + """ + self._test_dropdown_field( + u'country', + u'Country or Region', + u'', + [u'Pakistan', u''], + ) + + def test_prefered_language_field(self): + """ + Test behaviour of "Preferred Language" field. + """ + self._test_dropdown_field( + u'language_proficiencies', + u'Preferred Language', + u'', + [u'Pushto', u''], + ) diff --git a/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_120.jpg b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_120.jpg new file mode 100755 index 0000000000..4feac03ee9 Binary files /dev/null and b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_120.jpg differ diff --git a/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_30.jpg b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_30.jpg new file mode 100755 index 0000000000..46be2ff684 Binary files /dev/null and b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_30.jpg differ diff --git a/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_50.jpg b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_50.jpg new file mode 100755 index 0000000000..a5ef6559b2 Binary files /dev/null and b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_50.jpg differ diff --git a/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_500.jpg b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_500.jpg new file mode 100755 index 0000000000..c460455897 Binary files /dev/null and b/test_root/uploads/profile-images/ebe754f90bc88acf8ec6a1d27b87f743_500.jpg differ diff --git a/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_120.jpg b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_120.jpg new file mode 100755 index 0000000000..4feac03ee9 Binary files /dev/null and b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_120.jpg differ diff --git a/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_30.jpg b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_30.jpg new file mode 100755 index 0000000000..46be2ff684 Binary files /dev/null and b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_30.jpg differ diff --git a/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_50.jpg b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_50.jpg new file mode 100755 index 0000000000..a5ef6559b2 Binary files /dev/null and b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_50.jpg differ diff --git a/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_500.jpg b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_500.jpg new file mode 100755 index 0000000000..c460455897 Binary files /dev/null and b/test_root/uploads/profile-images/f0d065035a5c4d32df318fbc54138765_500.jpg differ