diff --git a/common/static/common/js/components/views/paginated_view.js b/common/static/common/js/components/views/paginated_view.js index 4bf085d227..947ce83088 100644 --- a/common/static/common/js/components/views/paginated_view.js +++ b/common/static/common/js/components/views/paginated_view.js @@ -68,6 +68,12 @@ return this; }, + renderError: function () { + this.$el.text( + gettext('Your request could not be completed. Reload the page and try again. If the issue persists, click the Help tab to report the problem.') // jshint ignore: line + ); + }, + assign: function (view, selector) { view.setElement(this.$(selector)).render(); } diff --git a/common/test/acceptance/pages/lms/learner_profile.py b/common/test/acceptance/pages/lms/learner_profile.py index 16177cfefb..a24ebf1bc6 100644 --- a/common/test/acceptance/pages/lms/learner_profile.py +++ b/common/test/acceptance/pages/lms/learner_profile.py @@ -45,6 +45,20 @@ class Badge(PageObject): """ self.q(css=".share-button").click() EmptyPromise(self.modal_displayed, "Share modal displayed").fulfill() + EmptyPromise(self.modal_focused, "Focus handed to modal").fulfill() + + def modal_focused(self): + """ + Return True if the badges model has focus, False otherwise. + """ + return BrowserQuery(self.full_view, css=".badges-modal").is_focused() + + def close_modal(self): + """ + Close the badges modal and check that it is no longer displayed. + """ + BrowserQuery(self.full_view, css=".badges-modal .close").click() + EmptyPromise(lambda: not self.modal_displayed(), "Share modal dismissed").fulfill() class LearnerProfilePage(FieldsMixin, PageObject): diff --git a/common/test/acceptance/tests/lms/test_learner_profile.py b/common/test/acceptance/tests/lms/test_learner_profile.py index 6c69662f7f..72ec3c6431 100644 --- a/common/test/acceptance/tests/lms/test_learner_profile.py +++ b/common/test/acceptance/tests/lms/test_learner_profile.py @@ -750,6 +750,15 @@ class DifferentUserLearnerProfilePageTest(LearnerProfileTestMixin, WebAppTest): self.verify_profile_page_is_public(profile_page, is_editable=False) self.verify_profile_page_view_event(username, different_user_id, visibility=self.PRIVACY_PUBLIC) + def test_badge_share_modal(self): + username = 'testcert' + AutoAuthPage(self.browser, username=username).visit() + profile_page = self.visit_profile_page(username) + profile_page.display_accomplishments() + badge = profile_page.badges[0] + badge.display_modal() + badge.close_modal() + @attr('a11y') class LearnerProfileA11yTest(LearnerProfileTestMixin, WebAppTest): diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 89dda4ecc4..41df1cfffd 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -667,6 +667,11 @@ 'lms/include/js/spec/student_profile/learner_profile_factory_spec.js', 'lms/include/js/spec/student_profile/learner_profile_view_spec.js', 'lms/include/js/spec/student_profile/learner_profile_fields_spec.js', + 'lms/include/js/spec/student_profile/share_modal_view_spec.js', + 'lms/include/js/spec/student_profile/badge_view_spec.js', + 'lms/include/js/spec/student_profile/section_two_tab_spec.js', + 'lms/include/js/spec/student_profile/badge_list_view_spec.js', + 'lms/include/js/spec/student_profile/badge_list_container_spec.js', 'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js', 'lms/include/js/spec/verify_student/reverify_view_spec.js', 'lms/include/js/spec/verify_student/webcam_photo_view_spec.js', diff --git a/lms/static/js/spec/student_account/helpers.js b/lms/static/js/spec/student_account/helpers.js index 67a33b7f9b..159d903628 100644 --- a/lms/static/js/spec/student_account/helpers.js +++ b/lms/static/js/spec/student_account/helpers.js @@ -3,7 +3,7 @@ define(['underscore'], function(_) { var USER_ACCOUNTS_API_URL = '/api/user/v0/accounts/student'; var USER_PREFERENCES_API_URL = '/api/user/v0/preferences/student'; - var BADGES_API_URL = '/api/badges/v1/assertions/student/'; + var BADGES_API_URL = '/api/badges/v1/assertions/user/student/'; var IMAGE_UPLOAD_API_URL = '/api/profile_images/v0/staff/upload'; var IMAGE_REMOVE_API_URL = '/api/profile_images/v0/staff/remove'; var FIND_COURSES_URL = '/courses'; diff --git a/lms/static/js/spec/student_profile/badge_list_container_spec.js b/lms/static/js/spec/student_profile/badge_list_container_spec.js new file mode 100644 index 0000000000..b385073f95 --- /dev/null +++ b/lms/static/js/spec/student_profile/badge_list_container_spec.js @@ -0,0 +1,85 @@ +define(['backbone', 'jquery', 'underscore', 'URI', 'common/js/spec_helpers/ajax_helpers', + 'js/spec/student_profile/helpers', + 'js/student_profile/views/badge_list_container', + 'common/js/components/collections/paging_collection' + ], + function (Backbone, $, _, URI, AjaxHelpers, LearnerProfileHelpers, BadgeListContainer, PagingCollection) { + 'use strict'; + describe('edx.user.BadgeListContainer', function () { + + var view, requests; + + var createView = function (requests, badge_list_object) { + var badgeCollection = new PagingCollection(); + badgeCollection.url = '/api/badges/v1/assertions/user/staff/'; + var models = []; + _.each(_.range(badge_list_object.count), function (idx) { + models.push(LearnerProfileHelpers.makeBadge(idx)); + }); + badge_list_object.results = models; + badgeCollection.fetch(); + var request = AjaxHelpers.currentRequest(requests); + var path = new URI(request.url).path(); + expect(path).toBe('/api/badges/v1/assertions/user/staff/'); + AjaxHelpers.respondWithJson(requests, badge_list_object); + var badge_list_container = new BadgeListContainer({ + 'collection': badgeCollection + + }); + badge_list_container.render(); + return badge_list_container; + }; + + afterEach(function () { + view.$el.remove(); + }); + + it('displays all badges', function () { + requests = AjaxHelpers.requests(this); + view = createView(requests, { + count: 30, + previous: '/arbitrary/url', + num_pages: 3, + next: null, + start: 20, + current_page: 1, + results: [] + }); + var badges = view.$el.find('div.badge-display'); + expect(badges.length).toBe(30); + }); + + it('displays placeholder on last page', function () { + requests = AjaxHelpers.requests(this); + view = createView(requests, { + count: 30, + previous: '/arbitrary/url', + num_pages: 3, + next: null, + start: 20, + current_page: 3, + results: [] + }); + var placeholder = view.$el.find('span.accomplishment-placeholder'); + expect(placeholder.length).toBe(1); + }); + + it('does not display placeholder on first page', function () { + requests = AjaxHelpers.requests(this); + view = createView(requests, { + count: 30, + previous: '/arbitrary/url', + num_pages: 3, + next: null, + start: 0, + current_page: 1, + results: [] + }); + var placeholder = view.$el.find('span.accomplishment-placeholder'); + expect(placeholder.length).toBe(0); + }); + + }); + } +); + diff --git a/lms/static/js/spec/student_profile/badge_list_view_spec.js b/lms/static/js/spec/student_profile/badge_list_view_spec.js new file mode 100644 index 0000000000..927c0a7376 --- /dev/null +++ b/lms/static/js/spec/student_profile/badge_list_view_spec.js @@ -0,0 +1,74 @@ +define(['backbone', 'jquery', 'underscore', + 'js/spec/student_profile/helpers', + 'js/student_profile/views/badge_list_view', + 'common/js/components/collections/paging_collection' + ], + function (Backbone, $, _, LearnerProfileHelpers, BadgeListView, PagingCollection) { + "use strict"; + describe("edx.user.BadgeListView", function () { + + var view; + + var createView = function (badges, pages, page, hasNextPage) { + var badgeCollection = new PagingCollection(); + badgeCollection.url = "/api/badges/v1/assertions/user/staff/"; + var models = []; + _.each(badges, function (element) { + models.push(new Backbone.Model(element)); + }); + badgeCollection.models = models; + badgeCollection.length = badges.length; + badgeCollection.currentPage = page; + badgeCollection.totalPages = pages; + badgeCollection.hasNextPage = function () { + return hasNextPage; + }; + var badge_list = new BadgeListView({ + 'collection': badgeCollection + + }); + return badge_list; + }; + + afterEach(function () { + view.$el.remove(); + }); + + it("there is a single row if there is only one badge", function () { + view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 1, false); + view.render(); + var rows = view.$el.find('div.row'); + expect(rows.length).toBe(1); + }); + + it("accomplishments placeholder is visible on a last page", function () { + view = createView([LearnerProfileHelpers.makeBadge(1)], 2, 2, false); + view.render(); + var placeholder = view.$el.find('span.accomplishment-placeholder'); + expect(placeholder.length).toBe(1); + }); + + it("accomplishments placeholder to be not visible on a first page", function () { + view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 2, true); + view.render(); + var placeholder = view.$el.find('span.accomplishment-placeholder'); + expect(placeholder.length).toBe(0); + }); + + it("badges are in two columns (checked by counting rows for a known number of badges)", function () { + var badges = []; + _.each(_.range(4), function (item) { + badges.push(LearnerProfileHelpers.makeBadge(item)); + }); + view = createView(badges, 1, 2, true); + view.render(); + var placeholder = view.$el.find('span.accomplishment-placeholder'); + expect(placeholder.length).toBe(0); + var rows = view.$el.find('div.row'); + expect(rows.length).toBe(2); + }); + + }); + } +); + diff --git a/lms/static/js/spec/student_profile/badge_view_spec.js b/lms/static/js/spec/student_profile/badge_view_spec.js new file mode 100644 index 0000000000..41159423d8 --- /dev/null +++ b/lms/static/js/spec/student_profile/badge_view_spec.js @@ -0,0 +1,104 @@ +define(['backbone', 'jquery', 'underscore', + 'js/spec/student_profile/helpers', + 'js/student_profile/views/badge_view' + ], + function (Backbone, $, _, LearnerProfileHelpers, BadgeView) { + "use strict"; + describe("edx.user.BadgeView", function () { + + var view, badge; + + var createView = function (ownProfile) { + badge = LearnerProfileHelpers.makeBadge(1); + var options = { + 'model': new Backbone.Model(badge), + 'ownProfile': ownProfile, + 'badgeMeta': {} + }; + var view = new BadgeView(options); + view.render(); + $('body').append(view.$el); + view.$el.show(); + expect(view.$el.is(':visible')).toBe(true); + return view; + }; + + afterEach(function () { + view.$el.remove(); + $('.badges-modal').remove(); + }); + + it("profile of other has no share button", function () { + view = createView(false); + expect(view.context.ownProfile).toBeFalsy(); + expect(view.$el.find('button.share-button').length).toBe(0); + }); + + it("own profile has share button", function () { + view = createView(true); + expect(view.context.ownProfile).toBeTruthy(); + expect(view.$el.find('button.share-button').length).toBe(1); + }); + + it("click on share button calls createModal function", function () { + view = createView(true); + spyOn(view, "createModal"); + view.delegateEvents(); + expect(view.context.ownProfile).toBeTruthy(); + var shareButton = view.$el.find('button.share-button'); + expect(shareButton.length).toBe(1); + expect(view.createModal).not.toHaveBeenCalled(); + shareButton.click(); + expect(view.createModal).toHaveBeenCalled(); + }); + + it("click on share button calls shows the dialog", function () { + view = createView(true); + expect(view.context.ownProfile).toBeTruthy(); + var shareButton = view.$el.find('button.share-button'); + expect(shareButton.length).toBe(1); + var modalElement = $('.badges-modal'); + expect(modalElement.length).toBe(0); + expect(modalElement.is(":visible")).toBeFalsy(); + shareButton.click(); + // Note: this element should have appeared in the dom during: shareButton.click(); + modalElement = $('.badges-modal'); + waitsFor(function () { + return modalElement.is(":visible"); + }, '', 1000); + }); + + var testBadgeNameIsDisplayed = function (ownProfile) { + view = createView(ownProfile); + var badgeDiv = view.$el.find(".badge-name"); + expect(badgeDiv.length).toBeTruthy(); + expect(badgeDiv.is(':visible')).toBe(true); + expect(_.count(badgeDiv.html(), badge.badge_class.display_name)).toBeTruthy(); + }; + + it("test badge name is displayed for own profile", function () { + testBadgeNameIsDisplayed(true); + }); + + it("test badge name is displayed for other profile", function () { + testBadgeNameIsDisplayed(false); + }); + + var testBadgeIconIsDisplayed = function (ownProfile) { + view = createView(ownProfile); + var badgeImg = view.$el.find("img.badge"); + expect(badgeImg.length).toBe(1); + expect(badgeImg.attr('src')).toEqual(badge.image_url); + }; + + it("test badge icon is displayed for own profile", function () { + testBadgeIconIsDisplayed(true); + }); + + it("test badge icon is displayed for other profile", function () { + testBadgeIconIsDisplayed(false); + }); + + }); + } +); diff --git a/lms/static/js/spec/student_profile/helpers.js b/lms/static/js/spec/student_profile/helpers.js index b5ca701a4a..daf00378f1 100644 --- a/lms/static/js/spec/student_profile/helpers.js +++ b/lms/static/js/spec/student_profile/helpers.js @@ -1,4 +1,4 @@ -define(['underscore'], function(_) { +define(['underscore', 'URI', 'common/js/spec_helpers/ajax_helpers'], function(_, URI, AjaxHelpers) { 'use strict'; var expectProfileElementContainsField = function(element, view) { @@ -148,6 +148,21 @@ define(['underscore'], function(_) { }); }; + var expectBadgeLoadingErrorIsRendered = function(learnerProfileView) { + var errorMessage = learnerProfileView.$el.find(".badge-set-display").text(); + expect(errorMessage).toBe( + 'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' + + 'Help tab to report the problem.' + ); + }; + + var breakBadgeLoading = function(learnerProfileView, requests) { + var request = AjaxHelpers.currentRequest(requests); + var path = new URI(request.url).path(); + expect(path).toBe('/api/badges/v1/assertions/user/student/'); + AjaxHelpers.respondWithError(requests, 500); + }; + var firstPageBadges = { count: 30, previous: null, @@ -220,7 +235,8 @@ define(['underscore'], function(_) { expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered, expectTabbedViewToBeHidden: expectTabbedViewToBeHidden, expectTabbedViewToBeShown: expectTabbedViewToBeShown, expectBadgesDisplayed: expectBadgesDisplayed, expectBadgesHidden: expectBadgesHidden, + expectBadgeLoadingErrorIsRendered: expectBadgeLoadingErrorIsRendered, breakBadgeLoading: breakBadgeLoading, firstPageBadges: firstPageBadges, secondPageBadges: secondPageBadges, thirdPageBadges: thirdPageBadges, - emptyBadges: emptyBadges, expectPage: expectPage + emptyBadges: emptyBadges, expectPage: expectPage, makeBadge: makeBadge }; }); diff --git a/lms/static/js/spec/student_profile/learner_profile_view_spec.js b/lms/static/js/spec/student_profile/learner_profile_view_spec.js index 263a8e5dd2..06eb830602 100644 --- a/lms/static/js/spec/student_profile/learner_profile_view_spec.js +++ b/lms/static/js/spec/student_profile/learner_profile_view_spec.js @@ -206,5 +206,15 @@ define(['backbone', 'jquery', 'underscore', 'common/js/spec_helpers/ajax_helpers LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true); }); + it("renders an error if the badges can't be fetched", function () { + var learnerProfileView = createLearnerProfileView(false, 'all_users', true); + learnerProfileView.options.accountSettingsModel.set({'accomplishments_shared': true}); + var requests = AjaxHelpers.requests(this); + + learnerProfileView.render(); + + LearnerProfileHelpers.breakBadgeLoading(learnerProfileView, requests); + LearnerProfileHelpers.expectBadgeLoadingErrorIsRendered(learnerProfileView); + }); }); }); diff --git a/lms/static/js/spec/student_profile/section_two_tab_spec.js b/lms/static/js/spec/student_profile/section_two_tab_spec.js new file mode 100644 index 0000000000..b553a0735f --- /dev/null +++ b/lms/static/js/spec/student_profile/section_two_tab_spec.js @@ -0,0 +1,112 @@ +define(['backbone', 'jquery', 'underscore', + 'js/spec/student_account/helpers', + 'js/student_profile/views/section_two_tab', + 'js/views/fields', + 'js/student_account/models/user_account_model' + ], + function (Backbone, $, _, Helpers, SectionTwoTabView, FieldViews, UserAccountModel) { + "use strict"; + describe("edx.user.SectionTwoTab", function () { + + var createSectionTwoView = function (ownProfile, profileIsPublic) { + + var accountSettingsModel = new UserAccountModel(); + accountSettingsModel.set(Helpers.createAccountSettingsData()); + accountSettingsModel.set({'profile_is_public': profileIsPublic}); + accountSettingsModel.set({'profile_image': Helpers.PROFILE_IMAGE}); + + var editable = ownProfile ? 'toggle' : 'never'; + + var sectionTwoFieldViews = [ + new FieldViews.TextareaFieldView({ + model: accountSettingsModel, + editable: editable, + showMessages: false, + title: 'About me', + placeholderValue: "Tell other edX learners a little about yourself: where you live, " + + "what your interests are, why you're taking courses on edX, or what you hope to learn.", + valueAttribute: "bio", + helpMessage: '', + messagePosition: 'header' + }) + ]; + + return new SectionTwoTabView({ + viewList: sectionTwoFieldViews, + showFullProfile: function(){ + return profileIsPublic; + }, + ownProfile: ownProfile + }); + }; + + it("full profile displayed for public profile", function () { + var view = createSectionTwoView(false, true); + view.render(); + var bio = view.$el.find('.u-field-bio'); + expect(bio.length).toBe(1); + }); + + it("profile field parts are actually rendered for public profile", function () { + var view = createSectionTwoView(false, true); + _.each(view.options.viewList, function (fieldView) { + spyOn(fieldView, "render").andCallThrough(); + }); + view.render(); + _.each(view.options.viewList, function (fieldView) { + expect(fieldView.render).toHaveBeenCalled(); + }); + }); + + var testPrivateProfile = function (ownProfile, msg_string) { + var view = createSectionTwoView(ownProfile, false); + view.render(); + var bio = view.$el.find('.u-field-bio'); + expect(bio.length).toBe(0); + var msg = view.$el.find('span.profile-private--message'); + expect(msg.length).toBe(1); + expect(_.count(msg.html(), msg_string)).toBeTruthy(); + }; + + it("no profile when profile is private for other people", function () { + testPrivateProfile(false, "This learner is currently sharing a limited profile"); + }); + + it("no profile when profile is private for the user herself", function () { + testPrivateProfile(true, "You are currently sharing a limited profile"); + }); + + var testProfilePrivatePartsDoNotRender = function (ownProfile) { + var view = createSectionTwoView(ownProfile, false); + _.each(view.options.viewList, function (fieldView) { + spyOn(fieldView, "render"); + }); + view.render(); + _.each(view.options.viewList, function (fieldView) { + expect(fieldView.render).not.toHaveBeenCalled(); + }); + }; + + it("profile field parts are not rendered for private profile for owner", function () { + testProfilePrivatePartsDoNotRender(true); + }); + + it("profile field parts are not rendered for private profile for other people", function () { + testProfilePrivatePartsDoNotRender(false); + }); + + it("does not allow fields to be edited when visiting a profile for other people", function () { + var view = createSectionTwoView(false, true); + var bio = view.options.viewList[0]; + expect(bio.editable).toBe("never"); + }); + + it("allows fields to be edited when visiting one's own profile", function () { + var view = createSectionTwoView(true, true); + var bio = view.options.viewList[0]; + expect(bio.editable).toBe("toggle"); + }); + + }); + } +); diff --git a/lms/static/js/spec/student_profile/share_modal_view_spec.js b/lms/static/js/spec/student_profile/share_modal_view_spec.js new file mode 100644 index 0000000000..a86bfe4916 --- /dev/null +++ b/lms/static/js/spec/student_profile/share_modal_view_spec.js @@ -0,0 +1,60 @@ +define(['backbone', 'jquery', 'underscore', 'moment', + 'js/spec/student_account/helpers', + 'js/spec/student_profile/helpers', + 'js/student_profile/views/share_modal_view', + 'jquery.simulate' + ], + function (Backbone, $, _, Moment, Helpers, LearnerProfileHelpers, ShareModalView) { + "use strict"; + describe("edx.user.ShareModalView", function () { + var keys = $.simulate.keyCode; + + var view; + + var createModalView = function () { + var badge = LearnerProfileHelpers.makeBadge(1); + var context = _.extend(badge, { + 'created': new Moment(badge.created), + 'ownProfile': true, + 'badgeMeta': {} + }); + return new ShareModalView({ + model: new Backbone.Model(context), + shareButton: $("