From 9c2d18710c35c7bf477f9f9984dc1dadde5bdcf1 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Sat, 14 Mar 2015 08:32:54 +0000 Subject: [PATCH] Jasmine tests for account settings page. TNL-1499 --- .../js/spec/views/group_configuration_spec.js | 2 +- .../spec/views/pages/course_outline_spec.js | 2 +- cms/static/js/spec/views/pages/index_spec.js | 2 +- common/static/js/spec_helpers/ajax_helpers.js | 6 +- lms/static/js/spec/main.js | 5 + .../account_settings_factory_spec.js | 166 +++++++++++++++ .../account_settings_fields_spec.js | 96 +++++++++ .../account_settings_view_spec.js | 101 +++++++++ lms/static/js/spec/student_account/helpers.js | 100 +++++++++ lms/static/js/spec/views/fields_helpers.js | 195 ++++++++++++++++++ lms/static/js/spec/views/fields_spec.js | 161 +++++++++++++++ .../views/account_settings_factory.js | 6 +- lms/static/js/views/fields.js | 6 +- lms/static/js_test.yml | 1 + 14 files changed, 837 insertions(+), 12 deletions(-) create mode 100644 lms/static/js/spec/student_account/account_settings_factory_spec.js create mode 100644 lms/static/js/spec/student_account/account_settings_fields_spec.js create mode 100644 lms/static/js/spec/student_account/account_settings_view_spec.js create mode 100644 lms/static/js/spec/student_account/helpers.js create mode 100644 lms/static/js/spec/views/fields_helpers.js create mode 100644 lms/static/js/spec/views/fields_spec.js diff --git a/cms/static/js/spec/views/group_configuration_spec.js b/cms/static/js/spec/views/group_configuration_spec.js index 400715a858..3283726cb2 100644 --- a/cms/static/js/spec/views/group_configuration_spec.js +++ b/cms/static/js/spec/views/group_configuration_spec.js @@ -123,7 +123,7 @@ define([ patchAndVerifyRequest(requests, url, notificationSpy); - AjaxHelpers.respondToDelete(requests); + AjaxHelpers.respondWithNoContent(requests); ViewHelpers.verifyNotificationHidden(notificationSpy); expect($(SELECTORS.itemView)).not.toExist(); }; diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js index 98d55a0dd7..c50e7fb985 100644 --- a/cms/static/js/spec/views/pages/course_outline_spec.js +++ b/cms/static/js/spec/views/pages/course_outline_spec.js @@ -281,7 +281,7 @@ define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/vie expect($('.wrapper-alert-announcement')).not.toHaveClass('is-hidden'); $('.dismiss-button').click(); AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); - AjaxHelpers.respondToDelete(requests); + AjaxHelpers.respondWithNoContent(requests); expect($('.wrapper-alert-announcement')).toHaveClass('is-hidden'); }); }); diff --git a/cms/static/js/spec/views/pages/index_spec.js b/cms/static/js/spec/views/pages/index_spec.js index 3217df1b2c..622cff39db 100644 --- a/cms/static/js/spec/views/pages/index_spec.js +++ b/cms/static/js/spec/views/pages/index_spec.js @@ -33,7 +33,7 @@ define(["jquery", "js/common_helpers/ajax_helpers", "js/spec_helpers/view_helper var reloadSpy = spyOn(ViewUtils, 'reload'); $('.dismiss-button').click(); AjaxHelpers.expectJsonRequest(requests, 'DELETE', 'dummy_dismiss_url'); - AjaxHelpers.respondToDelete(requests); + AjaxHelpers.respondWithNoContent(requests); expect(reloadSpy).toHaveBeenCalled(); }); diff --git a/common/static/js/spec_helpers/ajax_helpers.js b/common/static/js/spec_helpers/ajax_helpers.js index f2be1a44fb..6ceda7efcc 100644 --- a/common/static/js/spec_helpers/ajax_helpers.js +++ b/common/static/js/spec_helpers/ajax_helpers.js @@ -1,6 +1,6 @@ define(['sinon', 'underscore'], function(sinon, _) { var fakeServer, fakeRequests, expectRequest, expectJsonRequest, - respondWithJson, respondWithError, respondWithTextError, respondToDelete; + respondWithJson, respondWithError, respondWithTextError, responseWithNoContent; /* These utility methods are used by Jasmine tests to create a mock server or * get reference to mock requests. In either case, the cleanup (restore) is done with @@ -109,7 +109,7 @@ define(['sinon', 'underscore'], function(sinon, _) { ); }; - respondToDelete = function(requests, requestIndex) { + respondWithNoContent = function(requests, requestIndex) { if (_.isUndefined(requestIndex)) { requestIndex = requests.length - 1; } @@ -125,6 +125,6 @@ define(['sinon', 'underscore'], function(sinon, _) { 'respondWithJson': respondWithJson, 'respondWithError': respondWithError, 'respondWithTextError': respondWithTextError, - 'respondToDelete': respondToDelete + 'respondWithNoContent': respondWithNoContent, }; }); diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 39bb5a70b2..b68ba7a4fa 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -59,6 +59,7 @@ // Manually specify LMS files that are not converted to RequireJS 'history': 'js/vendor/history', + 'js/mustache': 'js/mustache', 'js/verify_student/photocapture': 'js/verify_student/photocapture', 'js/staff_debug_actions': 'js/staff_debug_actions', 'js/vendor/jquery.qubit': 'js/vendor/jquery.qubit', @@ -587,7 +588,11 @@ 'lms/include/js/spec/student_account/enrollment_spec.js', 'lms/include/js/spec/student_account/emailoptin_spec.js', 'lms/include/js/spec/student_account/shoppingcart_spec.js', + 'lms/include/js/spec/student_account/account_settings_factory_spec.js', + 'lms/include/js/spec/student_account/account_settings_fields_spec.js', + 'lms/include/js/spec/student_account/account_settings_view_spec.js', 'lms/include/js/spec/student_profile/profile_spec.js', + 'lms/include/js/spec/views/fields_spec.js', 'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js', 'lms/include/js/spec/verify_student/webcam_photo_view_spec.js', 'lms/include/js/spec/verify_student/image_input_spec.js', diff --git a/lms/static/js/spec/student_account/account_settings_factory_spec.js b/lms/static/js/spec/student_account/account_settings_factory_spec.js new file mode 100644 index 0000000000..d927241e53 --- /dev/null +++ b/lms/static/js/spec/student_account/account_settings_factory_spec.js @@ -0,0 +1,166 @@ +define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/spec/views/fields_helpers', + 'js/spec/student_account/helpers', + 'js/student_account/views/account_settings_factory', + 'js/student_account/views/account_settings_view' + ], + function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers, AccountSettingsPage, AccountSettingsView) { + 'use strict'; + + describe("edx.user.AccountSettingsFactory", function () { + + var FIELDS_DATA = { + 'country': { + 'options': Helpers.FIELD_OPTIONS, + }, 'gender': { + 'options': Helpers.FIELD_OPTIONS, + }, 'language': { + 'options': Helpers.FIELD_OPTIONS, + }, 'level_of_education': { + 'options': Helpers.FIELD_OPTIONS, + }, 'password': { + 'url': '/password_reset', + }, 'year_of_birth': { + 'options': Helpers.FIELD_OPTIONS, + }, 'preferred_language': { + 'options': Helpers.FIELD_OPTIONS, + } + }; + + var requests; + + beforeEach(function () { + setFixtures('
'); + TemplateHelpers.installTemplate('templates/fields/field_readonly'); + TemplateHelpers.installTemplate('templates/fields/field_dropdown'); + TemplateHelpers.installTemplate('templates/fields/field_link'); + TemplateHelpers.installTemplate('templates/fields/field_text'); + TemplateHelpers.installTemplate('templates/student_account/account_settings'); + }); + + it("shows loading error when UserAccountModel fails to load", function() { + + requests = AjaxHelpers.requests(this); + + var context = AccountSettingsPage( + FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL + ); + var accountSettingsView = context.accountSettingsView; + + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + var request = requests[0]; + expect(request.method).toBe('GET'); + expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); + + AjaxHelpers.respondWithError(requests, 500); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + }); + + + it("shows loading error when UserPreferencesModel fails to load", function() { + + requests = AjaxHelpers.requests(this); + + var context = AccountSettingsPage( + FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL + ); + var accountSettingsView = context.accountSettingsView; + + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + var request = requests[0]; + expect(request.method).toBe('GET'); + expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL); + + AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + var request = requests[1]; + expect(request.method).toBe('GET'); + expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL); + + AjaxHelpers.respondWithError(requests, 500); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + }); + + it("renders fields after the models are successfully fetched", function() { + + requests = AjaxHelpers.requests(this); + + var context = AccountSettingsPage( + FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL + ); + var accountSettingsView = context.accountSettingsView; + + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA); + AjaxHelpers.respondWithJson(requests, Helpers.USER_PREFERENCES_DATA); + + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView) + }); + + it("expects all fields to behave correctly", function () { + + requests = AjaxHelpers.requests(this); + + var context = AccountSettingsPage( + FIELDS_DATA, Helpers.USER_ACCOUNTS_API_URL, Helpers.USER_PREFERENCES_API_URL + ); + var accountSettingsView = context.accountSettingsView; + + AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA); + AjaxHelpers.respondWithJson(requests, Helpers.USER_PREFERENCES_DATA); + + var sectionsData = accountSettingsView.options.sectionsData; + + expect(sectionsData[0].fields.length).toBe(5); + + var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]]; + for (var i = 0; i < textFields ; i++) { + + var view = textFields[i].view; + FieldViewsSpecHelpers.verifyTextField(view, { + title: view.options.title, + valueAttribute: view.options.valueAttribute, + helpMessage: view.options.helpMessage, + validValue: 'My Name', + invalidValue1: '', + invalidValue2: '@', + validationError: "Think again!" + }, requests); + } + + expect(sectionsData[1].fields.length).toBe(5); + for (var i = 0; i < 4; i++) { + + var view = sectionsData[1].fields[i].view; + FieldViewsSpecHelpers.verifyDropDownField(view, { + title: view.options.title, + valueAttribute: view.options.valueAttribute, + helpMessage: '', + validValue: Helpers.FIELD_OPTIONS[0][0], + invalidValue1: Helpers.FIELD_OPTIONS[1][0], + invalidValue2: Helpers.FIELD_OPTIONS[2][0], + validationError: "Nope, this will not do!" + }, requests); + } + + }); + }); + }); diff --git a/lms/static/js/spec/student_account/account_settings_fields_spec.js b/lms/static/js/spec/student_account/account_settings_fields_spec.js new file mode 100644 index 0000000000..4910689f39 --- /dev/null +++ b/lms/static/js/spec/student_account/account_settings_fields_spec.js @@ -0,0 +1,96 @@ +define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/views/fields', + 'js/spec/views/fields_helpers', + 'js/student_account/views/account_settings_fields', + 'js/student_account/models/user_account_model', + 'string_utils'], + function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers, AccountSettingsFieldViews, UserAccountModel) { + 'use strict'; + + describe("edx.AccountSettingsFieldViews", function () { + + var requests, + timerCallback; + + var USERNAME = 'Legolas', + FULLNAME = 'Legolas Thranduil', + EMAIL = 'legolas@woodland.middlearth'; + + beforeEach(function () { + TemplateHelpers.installTemplate('templates/fields/field_readonly'); + TemplateHelpers.installTemplate('templates/fields/field_dropdown'); + TemplateHelpers.installTemplate('templates/fields/field_link'); + TemplateHelpers.installTemplate('templates/fields/field_text'); + + timerCallback = jasmine.createSpy('timerCallback'); + jasmine.Clock.useMock(); + }); + + it("sends request to reset password on clicking link in PasswordFieldView", function() { + requests = AjaxHelpers.requests(this); + + var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.PasswordFieldView, { + linkHref: '/password_reset', + emailAttribute: 'email' + }); + + var view = new AccountSettingsFieldViews.PasswordFieldView(fieldData).render(); + view.$('.u-field-value > a').click(); + AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', "email=legolas%40woodland.middlearth"); + AjaxHelpers.respondWithJson(requests, {"success": "true"}) + FieldViewsSpecHelpers.expectMessageContains(view, + "We've sent a message to legolas@woodland.middlearth. Click the link in the message to reset your password." + ); + }); + + it("sends request to /i18n/setlang/ after changing language preference in LanguagePreferenceFieldView", function() { + requests = AjaxHelpers.requests(this); + + var selector = '.u-field-value > select'; + var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { + valueAttribute: 'language', + options: FieldViewsSpecHelpers.SELECT_OPTIONS + }); + + var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render(); + + var data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]}; + view.$(selector).val(data[fieldData.valueAttribute]).change(); + FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); + AjaxHelpers.respondWithNoContent(requests); + + AjaxHelpers.expectRequest(requests, 'POST', '/i18n/setlang/', 'language=' + data[fieldData.valueAttribute]); + AjaxHelpers.respondWithNoContent(requests); + FieldViewsSpecHelpers.expectMessageContains(view, "Your changes have been saved."); + + var data = {'language': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}; + view.$(selector).val(data[fieldData.valueAttribute]).change(); + FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); + AjaxHelpers.respondWithNoContent(requests); + + AjaxHelpers.expectRequest(requests, 'POST', '/i18n/setlang/', 'language=' + data[fieldData.valueAttribute]); + AjaxHelpers.respondWithError(requests, 500); + FieldViewsSpecHelpers.expectMessageContains(view, "You must sign out of edX and sign back in before your language changes take effect."); + }); + + it("reads and saves the value correctly for LanguageProficienciesFieldView", function() { + requests = AjaxHelpers.requests(this); + + var selector = '.u-field-value > select'; + var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, { + valueAttribute: 'language_proficiencies', + options: FieldViewsSpecHelpers.SELECT_OPTIONS + }); + fieldData.model.set({'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]}); + + var view = new AccountSettingsFieldViews.LanguageProficienciesFieldView(fieldData).render(); + + expect(view.modelValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]); + + var data = {'language_proficiencies': [{'code': FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]}; + view.$(selector).val(FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]).change(); + FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data); + AjaxHelpers.respondWithNoContent(requests); + }); + }); + }); diff --git a/lms/static/js/spec/student_account/account_settings_view_spec.js b/lms/static/js/spec/student_account/account_settings_view_spec.js new file mode 100644 index 0000000000..b09de46aae --- /dev/null +++ b/lms/static/js/spec/student_account/account_settings_view_spec.js @@ -0,0 +1,101 @@ +define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/spec/student_account/helpers', + 'js/views/fields', + 'js/student_account/models/user_account_model', + 'js/student_account/views/account_settings_view' + ], + function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, FieldViews, UserAccountModel, AccountSettingsView) { + 'use strict'; + + describe("edx.user.AccountSettingsView", function () { + + var createAccountSettingsView = function () { + + var model = new UserAccountModel(); + model.set(Helpers.USER_ACCOUNTS_DATA); + + var sectionsData = [ + { + title: "Basic Account Information", + fields: [ + { + view: new FieldViews.ReadonlyFieldView({ + model: model, + title: "Username", + valueAttribute: "username" + }) + }, + { + view: new FieldViews.TextFieldView({ + model: model, + title: "Full Name", + valueAttribute: "name" + }) + } + ] + }, + { + title: "Additional Information", + fields: [ + { + view: new FieldViews.DropdownFieldView({ + model: model, + title: "Education Completed", + valueAttribute: "level_of_education", + options: Helpers.FIELD_OPTIONS + }) + } + ] + } + ] + + var accountSettingsView = new AccountSettingsView({ + el: $('.wrapper-account-settings'), + model: model, + sectionsData : sectionsData + }); + + return accountSettingsView; + }; + + beforeEach(function () { + setFixtures('
'); + TemplateHelpers.installTemplate('templates/fields/field_readonly'); + TemplateHelpers.installTemplate('templates/fields/field_dropdown'); + TemplateHelpers.installTemplate('templates/fields/field_link'); + TemplateHelpers.installTemplate('templates/fields/field_text'); + TemplateHelpers.installTemplate('templates/student_account/account_settings'); + }); + + it("shows loading error correctly", function() { + + var accountSettingsView = createAccountSettingsView(); + + accountSettingsView.render(); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + accountSettingsView.showLoadingError(); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, true); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + }); + + it("renders all fields as expected", function() { + + var accountSettingsView = createAccountSettingsView(); + + accountSettingsView.render(); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, true); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsButNotFieldsToBeRendered(accountSettingsView); + + accountSettingsView.renderFields(); + Helpers.expectLoadingIndicatorIsVisible(accountSettingsView, false); + Helpers.expectLoadingErrorIsVisible(accountSettingsView, false); + Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView) + }); + + }); + }); diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js new file mode 100644 index 0000000000..a19bda2c52 --- /dev/null +++ b/lms/static/js/spec/student_account/helpers.js @@ -0,0 +1,100 @@ +define(['underscore'], function(_) { + 'use strict'; + + var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student'; + var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student'; + + var USER_ACCOUNTS_DATA = { + username: 'student', + name: 'Student', + email: 'student@edx.org', + + level_of_education: '1', + gender: '2', + year_of_birth: '3', + country: '1', + language: '2' + }; + + var USER_PREFERENCES_DATA = { + 'pref-lang': '2' + }; + + var FIELD_OPTIONS = [ + ['1', 'Option 1'], + ['2', 'Option 2'], + ['3', 'Option 3'], + ]; + + var expectLoadingIndicatorIsVisible = function (view, visible) { + if (visible) { + expect(view.$('.ui-loading-indicator')).not.toHaveClass('is-hidden'); + } else { + expect(view.$('.ui-loading-indicator')).toHaveClass('is-hidden'); + } + }; + + var expectLoadingErrorIsVisible = function (view, visible) { + if (visible) { + expect(view.$('.ui-loading-error')).not.toHaveClass('is-hidden'); + } else { + expect(view.$('.ui-loading-error')).toHaveClass('is-hidden'); + } + }; + + var expectElementContainsField = function(element, field) { + var view = field.view; + + var fieldTitle = $(element).find('.u-field-title').text().trim(); + expect(fieldTitle).toBe(view.options.title); + + if ('fieldValue' in view) { + expect(view.model.get(view.options.valueAttribute)).toBeTruthy(); + expect(view.fieldValue()).toBe(view.modelValue()); + } else if (view.fieldType === 'link') { + expect($(element).find('a').length).toBe(1); + } else { + throw new Error('Unexpected field type: ' + view.fieldType); + } + }; + + var expectSettingsSectionsButNotFieldsToBeRendered = function (accountSettingsView) { + expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView, false) + }; + + var expectSettingsSectionsAndFieldsToBeRendered = function (accountSettingsView, fieldsAreRendered) { + var sectionsData = accountSettingsView.options.sectionsData; + + var sectionElements = accountSettingsView.$('.section'); + expect(sectionElements.length).toBe(sectionsData.length); + + _.each(sectionElements, function(sectionElement, sectionIndex) { + expect($(sectionElement).find('.section-header').text().trim()).toBe(sectionsData[sectionIndex].title); + + var sectionFieldElements = $(sectionElement).find('.u-field'); + + if (fieldsAreRendered === false) { + expect(sectionFieldElements.length).toBe(0); + } else { + expect(sectionFieldElements.length).toBe(sectionsData[sectionIndex].fields.length); + + _.each(sectionFieldElements, function (sectionFieldElement, fieldIndex) { + expectElementContainsField(sectionFieldElement, sectionsData[sectionIndex].fields[fieldIndex]); + }); + } + }); + }; + + return { + USER_ACCOUNTS_API_URL: USER_ACCOUNTS_API_URL, + USER_PREFERENCES_API_URL: USER_PREFERENCES_API_URL, + USER_ACCOUNTS_DATA: USER_ACCOUNTS_DATA, + USER_PREFERENCES_DATA: USER_PREFERENCES_DATA, + FIELD_OPTIONS: FIELD_OPTIONS, + expectLoadingIndicatorIsVisible: expectLoadingIndicatorIsVisible, + expectLoadingErrorIsVisible: expectLoadingErrorIsVisible, + expectElementContainsField: expectElementContainsField, + expectSettingsSectionsButNotFieldsToBeRendered: expectSettingsSectionsButNotFieldsToBeRendered, + expectSettingsSectionsAndFieldsToBeRendered: expectSettingsSectionsAndFieldsToBeRendered + }; +}); diff --git a/lms/static/js/spec/views/fields_helpers.js b/lms/static/js/spec/views/fields_helpers.js new file mode 100644 index 0000000000..a1f4e1dc0b --- /dev/null +++ b/lms/static/js/spec/views/fields_helpers.js @@ -0,0 +1,195 @@ +define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/views/fields', + 'string_utils'], + function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews) { + 'use strict'; + + var API_URL = '/api/end_point/v1'; + + var USERNAME = 'Legolas', + FULLNAME = 'Legolas Thranduil', + EMAIL = 'legolas@woodland.middlearth', + SELECT_OPTIONS = [['si', 'sindarin'], ['el', 'elvish'], ['na', 'nandor']]; + + var UserAccountModel = Backbone.Model.extend({ + idAttribute: 'username', + defaults: { + username: USERNAME, + name: FULLNAME, + email: EMAIL, + language: SELECT_OPTIONS[0][0] + }, + url: API_URL + }); + + var createFieldData = function (fieldType, fieldData) { + var data = { + model: fieldData.model || new UserAccountModel({}), + title: fieldData.title || 'Field Title', + valueAttribute: fieldData.valueAttribute, + helpMessage: fieldData.helpMessage || 'I am a field message' + }; + + switch (fieldType) { + case FieldViews.DropdownFieldView: + data['required'] = fieldData.required || false; + data['options'] = fieldData.options || SELECT_OPTIONS; + break; + case FieldViews.LinkFieldView: + case FieldViews.PasswordFieldView: + data['linkTitle'] = fieldData.linkTitle || "Link Title"; + data['linkHref'] = fieldData.linkHref || "/path/to/resource"; + data['emailAttribute'] = 'email'; + break; + } + + _.extend(data, fieldData); + + return data; + }; + + var createErrorMessage = function(attribute, user_message) { + var field_errors = {} + field_errors[attribute] = { + "user_message": user_message + } + return { + "field_errors": field_errors + } + }; + + var expectTitleAndMessageToBe = function(view, expectedTitle, expectedMessage) { + expect(view.$('.u-field-title').text().trim()).toBe(expectedTitle); + expect(view.$('.u-field-message').text().trim()).toBe(expectedMessage); + }; + + var expectMessageContains = function(view, expectedText) { + expect(view.$('.u-field-message').html()).toContain(expectedText); + }; + + var expectAjaxRequestWithData = function(requests, data) { + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', API_URL, data + ); + }; + + var verifyMessageUpdates = function (view, data, timerCallback) { + + var message = 'Here to help!' + + view.message(message); + expectMessageContains(view, message); + + view.showHelpMessage(); + expectMessageContains(view, view.helpMessage); + + view.showInProgressMessage(); + expectMessageContains(view, view.indicators['inProgress']); + expectMessageContains(view, view.messages['inProgress']); + + view.showSuccessMessage(); + expectMessageContains(view, view.indicators['success']); + expectMessageContains(view, view.getMessage('success')); + + expect(timerCallback).not.toHaveBeenCalled(); + + view.showErrorMessage({ + responseText: JSON.stringify(createErrorMessage(data.valueAttribute, 'Ops, try again!.')), + status: 400 + }); + expectMessageContains(view, view.indicators['validationError']); + + view.showErrorMessage({status: 500}); + expectMessageContains(view, view.indicators['error']); + expectMessageContains(view, view.indicators['error']); + }; + + var verifySuccessMessageReset = function (view, data, timerCallback) { + view.showHelpMessage(); + expectMessageContains(view, view.helpMessage); + view.showSuccessMessage(); + expectMessageContains(view, view.indicators['success']); + jasmine.Clock.tick(5000); + // Message gets reset + expectMessageContains(view, view.helpMessage); + + view.showSuccessMessage(); + expectMessageContains(view, view.indicators['success']); + // But if we change the message, it should not get reset. + view.message("Do not reset this!"); + jasmine.Clock.tick(5000); + expectMessageContains(view, "Do not reset this!"); + }; + + var verifyEditableField = function (view, data, requests) { + var request_data = {}; + var url = view.model.url; + + expectTitleAndMessageToBe(view, data.title, data.helpMessage); + + view.$(data.valueElementSelector).val(data.validValue).change(); + // When the value in the field is changed + expect(view.fieldValue()).toBe(data.validValue); + expectMessageContains(view, view.indicators['inProgress']); + expectMessageContains(view, view.messages['inProgress']); + request_data[data.valueAttribute] = data.validValue; + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', url, request_data + ); + + AjaxHelpers.respondWithNoContent(requests); + // When server returns success. + expectMessageContains(view, view.indicators['success']); + + view.$(data.valueElementSelector).val(data.invalidValue1).change(); + request_data[data.valueAttribute] = data.invalidValue1; + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', url, request_data + ); + AjaxHelpers.respondWithError(requests, 500); + // When server returns a 500 error + expectMessageContains(view, view.indicators['error']); + expectMessageContains(view, view.messages['error']); + + view.$(data.valueElementSelector).val(data.invalidValue2).change(); + request_data[data.valueAttribute] = data.invalidValue2; + AjaxHelpers.expectJsonRequest( + requests, 'PATCH', url, request_data + ); + AjaxHelpers.respondWithError(requests, 400, createErrorMessage(data.valueAttribute, data.validationError)); + // When server returns a validation error + expectMessageContains(view, view.indicators['validationError']); + expectMessageContains(view, data.validationError); + }; + + var verifyTextField = function (view, data, requests) { + var selector = '.u-field-value > input'; + verifyEditableField(view, _.extend({ + valueElementSelector: selector, + }, data + ), requests); + } + + var verifyDropDownField = function (view, data, requests) { + var selector = '.u-field-value > select'; + verifyEditableField(view, _.extend({ + valueElementSelector: selector, + }, data + ), requests); + } + + return { + SELECT_OPTIONS: SELECT_OPTIONS, + UserAccountModel: UserAccountModel, + createFieldData: createFieldData, + createErrorMessage: createErrorMessage, + expectTitleAndMessageToBe: expectTitleAndMessageToBe, + expectMessageContains: expectMessageContains, + expectAjaxRequestWithData: expectAjaxRequestWithData, + verifyMessageUpdates: verifyMessageUpdates, + verifySuccessMessageReset: verifySuccessMessageReset, + verifyEditableField: verifyEditableField, + verifyTextField: verifyTextField, + verifyDropDownField: verifyDropDownField + }; + }); diff --git a/lms/static/js/spec/views/fields_spec.js b/lms/static/js/spec/views/fields_spec.js new file mode 100644 index 0000000000..eee02c9886 --- /dev/null +++ b/lms/static/js/spec/views/fields_spec.js @@ -0,0 +1,161 @@ +define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', + 'js/views/fields', + 'js/spec/views/fields_helpers', + 'string_utils'], + function (Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViews, FieldViewsSpecHelpers) { + 'use strict'; + + var USERNAME = 'Legolas', + FULLNAME = 'Legolas Thranduil', + EMAIL = 'legolas@woodland.middlearth'; + + describe("edx.FieldViews", function () { + + var requests, + timerCallback; + + var fieldViewClasses = [ + FieldViews.ReadonlyFieldView, + FieldViews.TextFieldView, + FieldViews.DropdownFieldView, + FieldViews.LinkFieldView, + ]; + + beforeEach(function () { + TemplateHelpers.installTemplate('templates/fields/field_readonly'); + TemplateHelpers.installTemplate('templates/fields/field_dropdown'); + TemplateHelpers.installTemplate('templates/fields/field_link'); + TemplateHelpers.installTemplate('templates/fields/field_text'); + + timerCallback = jasmine.createSpy('timerCallback'); + jasmine.Clock.useMock(); + }); + + it("updates messages correctly for all fields", function() { + + for (var i = 0; i < fieldViewClasses.length; i++) { + + var fieldViewClass = fieldViewClasses[i]; + var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, { + title: 'Username', + valueAttribute: 'username', + helpMessage: 'The username that you use to sign in to edX.' + }); + + var view = new fieldViewClass(fieldData).render(); + FieldViewsSpecHelpers.verifyMessageUpdates(view, fieldData, timerCallback); + } + }); + + it("resets to help message some time after success message is set", function() { + + for (var i = 0; i < fieldViewClasses.length; i++) { + var fieldViewClass = fieldViewClasses[i]; + var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, { + title: 'Username', + valueAttribute: 'username', + helpMessage: 'The username that you use to sign in to edX.' + }) + + var view = new fieldViewClass(fieldData).render(); + FieldViewsSpecHelpers.verifySuccessMessageReset(view, fieldData, timerCallback); + } + }); + + it("sends a PATCH request when saveAttributes is called", function() { + + requests = AjaxHelpers.requests(this); + + var fieldViewClass = FieldViews.FieldView; + var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, { + title: 'Preferred Language', + valueAttribute: 'language', + helpMessage: 'Your preferred language.' + }) + + var view = new fieldViewClass(fieldData); + view.saveAttributes( + {'language': 'ur'}, + {'headers': {'Priority': 'Urgent'}} + ); + + var request = requests[0]; + expect(request.method).toBe('PATCH'); + expect(request.requestHeaders['Content-Type']).toBe('application/merge-patch+json;charset=utf-8'); + expect(request.requestHeaders['Priority']).toBe('Urgent'); + expect(request.requestBody).toBe('{"language":"ur"}'); + }); + + it("correctly renders and updates ReadonlyFieldView", function() { + var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.ReadonlyFieldView, { + title: 'Username', + valueAttribute: 'username', + helpMessage: 'The username that you use to sign in to edX.' + }); + var view = new FieldViews.ReadonlyFieldView(fieldData).render(); + + FieldViewsSpecHelpers.expectTitleAndMessageToBe(view, fieldData.title, fieldData.helpMessage); + expect(view.$('.u-field-value input').val().trim()).toBe(USERNAME); + + view.model.set({'username': 'bookworm'}); + expect(view.$('.u-field-value input').val().trim()).toBe('bookworm'); + }); + + it("correctly renders, updates and persists changes to TextFieldView", function() { + + requests = AjaxHelpers.requests(this); + + var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextFieldView, { + title: 'Full Name', + valueAttribute: 'name', + helpMessage: 'How are you?' + }); + var view = new FieldViews.TextFieldView(fieldData).render(); + + FieldViewsSpecHelpers.verifyTextField(view, { + title: fieldData.title, + valueAttribute: fieldData.valueAttribute, + helpMessage: fieldData.helpMessage, + validValue: 'My Name', + invalidValue1: 'Your Name', + invalidValue2: 'Her Name', + validationError: "Think again!" + }, requests); + }); + + it("correctly renders, updates and persists changes to DropdownFieldView", function() { + + requests = AjaxHelpers.requests(this); + + var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, { + title: 'Full Name', + valueAttribute: 'name', + helpMessage: 'edX full name' + }); + var view = new FieldViews.DropdownFieldView(fieldData).render(); + + FieldViewsSpecHelpers.verifyDropDownField(view, { + title: fieldData.title, + valueAttribute: fieldData.valueAttribute, + helpMessage: fieldData.helpMessage, + validValue: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0], + invalidValue1: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0], + invalidValue2: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0], + validationError: "Nope, this will not do!" + }, requests); + }); + + it("correctly renders LinkFieldView", function() { + var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, { + title: 'Title', + linkTitle: 'Link title', + helpMessage: 'Click the link.' + }); + var view = new FieldViews.LinkFieldView(fieldData).render(); + + FieldViewsSpecHelpers.expectTitleAndMessageToBe(view, fieldData.title, fieldData.helpMessage); + expect(view.$('.u-field-value > a').text().trim()).toBe(fieldData.linkTitle); + }); + }); + + }); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index c40ece2626..2b9606a4db 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -29,7 +29,7 @@ model: userAccountModel, title: gettext('Username'), valueAttribute: 'username', - helpMessage: '' + helpMessage: 'The name that identifies you on the edX site. You cannot change your username.' }) }, { @@ -37,13 +37,13 @@ model: userAccountModel, title: gettext('Full Name'), valueAttribute: 'name', - helpMessage: gettext('The name that appears on your edX certificates.') + helpMessage: gettext('The name that appears on your edX certificates. Other learners never see your full name.') }) }, { view: new AccountSettingsFieldViews.EmailFieldView({ model: userAccountModel, - title: gettext('Email'), + title: gettext('Email Address'), valueAttribute: 'email', helpMessage: gettext('The email address you use to sign in to edX. Communications from edX and your courses are sent to this address.') }) diff --git a/lms/static/js/views/fields.js b/lms/static/js/views/fields.js index 6ad1319592..d643cc1edd 100644 --- a/lms/static/js/views/fields.js +++ b/lms/static/js/views/fields.js @@ -35,7 +35,7 @@ initialize: function (options) { - this.template = _.template($(this.templateSelector).text()), + this.template = _.template($(this.templateSelector).text()); this.helpMessage = this.options.helpMessage || ''; this.showMessages = _.isUndefined(this.options.showMessages) ? true : this.options.showMessages; @@ -221,9 +221,9 @@ title: this.options.title, required: this.options.required, selectOptions: this.options.options, - message: this.helpMessage, + message: this.helpMessage })); - this.updateValueInField() + this.updateValueInField(); return this; }, diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 054e828e05..53badbe193 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -83,6 +83,7 @@ fixture_paths: - templates/instructor/instructor_dashboard_2 - templates/dashboard - templates/edxnotes + - templates/fields - templates/student_account - templates/student_profile - templates/verify_student